/* cc  -g -Wall -Wextra -std=c11 demo.c -lncursesw -o demo */

/*
    I've just acquired one of those very low cost logic analyzer cards that uses
   a raspberry pi pico to capture signals.  I looked around for a way to display
   the captured data on a text console but although there is sigrok-cli, it does
   not use pretty box characters to display traces, so I hacked up this demo in
   an afternoon to show what such an interface might look like.  This isn't usable
   code, it's literally just a demo of the appearance, as a suggestion for folks
   working in that area to consider.  When I was a student in the mid 70' we had
   something similar on our University systems that displayed on our Visual 200
   terminals.

   Although this uses the UTF-8 character set, the same principle could be used
   with the old VT100 terminal character set or the Microsoft font encoding.
   Indeed a good system would implement all of those and use whichever was the
   best one available at run time.  This code uses the wide character version
   of curses.

   Graham Toal, 16 Jan 2026.
 */

#define OPEN_SOURCE_EXTENDED 1
#include <locale.h>
#include <wchar.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#include <time.h>
#define NCURSES_WIDECHAR 1
#include <ncursesw/ncurses.h>

/* --------- Configuration --------- */

#define NUM_SIGNALS 12
#define NUM_SAMPLES 301

#define ROWS_PER_SIGNAL 2   // Recommend only between 1 and 3
#define WIDTH 2
int width = WIDTH;

/* Minimum characters reserved on the left for signal names */
#define NAME_COL_WIDTH 8
#define RIGHT_MARGIN 8
/* Keys (fall back to 'q' for quit if no special keys) */

/* --------- Data generation --------- */

static int signals[NUM_SIGNALS][NUM_SAMPLES];
static const char *signal_names[NUM_SIGNALS] = {"   E", "SIG1", "SIG2", " EN'", "D0:7"};

int x_start, x_end, y_high, y_low;


/* Simple demo patterns: square waves with different periods and phases */
static void generate_demo_data(void) {
  int s, i;
  for (s = 0; s < NUM_SIGNALS; ++s) {
    for (i = 0; i < NUM_SAMPLES; ++i) {
      int D = rand();
      if (s == 0) {
        signals[s][i] = i&1;            // E
      } else if (s >= 1 && s <= 3) {
        signals[s][i] = rand()&1;       // SIG1..EN'
      } else if (s >= 4 && s <= 12) {
        signals[s][i] = (D>>(i-4))&1;            // D0..D7
      } else {
        exit(1);
      }
    }
  }
}

/* --------- UTF-8 / box drawing selection --------- */

typedef struct {
  bool use_utf8;
  wchar_t *h;     /* horizontal */
  wchar_t *v;     /* vertical */
  wchar_t *tl;    /* top-left corner */
  wchar_t *tr;    /* top-right corner */
  wchar_t *bl;    /* bottom-left corner */
  wchar_t *br;    /* bottom-right corner */
  wchar_t *tee_l; /* tee to the left ( ┤ ) */
  wchar_t *tee_r; /* tee to the right ( ├ ) */
  wchar_t *tee_u; /* tee up ( ┴ ) */
  wchar_t *tee_d; /* tee down ( ┬ ) */
} boxchars_t;

static boxchars_t mbox;

/* crude UTF-8 detection: check current locale string for "UTF-8" or "utf8" */
static bool locale_is_utf8(void) {
  const char *loc = setlocale(LC_CTYPE, NULL);
  if (!loc) return false;
  return (strstr(loc, "UTF-8") != NULL) || (strstr(loc, "utf8") != NULL);
}

static void init_boxchars(void) {
  mbox.use_utf8 = locale_is_utf8();

  if (mbox.use_utf8) {
    /* light box drawing set (Unicode) */
    if (width==1) {
    mbox.h = L"─";
    mbox.v = L"│";
    mbox.tl = L"┌";
    mbox.tr = L"┐";
    mbox.bl = L"└";
    mbox.br = L"┘";
    mbox.tee_l = L"┤";
    mbox.tee_r = L"├";
    mbox.tee_u = L"┴";
    mbox.tee_d = L"┬";
    } else if (width==2) {
    mbox.h = L"──";
    mbox.v = L"│ ";
    mbox.tl = L"┌─";
    mbox.tr = L"┐ ";
    mbox.bl = L"└─";
    mbox.br = L"┘ ";
    mbox.tee_l = L"┤ ";
    mbox.tee_r = L"├─";
    mbox.tee_u = L"┴─";
    mbox.tee_d = L"┬─";
    }
  } else {
    /* ASCII / VT100-safe approximations; all stored as wide chars */
    mbox.h = L"-";
    mbox.v = L"|";
    mbox.tl = L"+";
    mbox.tr = L"+";
    mbox.bl = L"+";
    mbox.br = L"+";
    mbox.tee_l = L"+";
    mbox.tee_r = L"+";
    mbox.tee_u = L"+";
    mbox.tee_d = L"+";
  }
}


void draw_row(int y, int x_min, int x_max, int PIN, int first_sample, int max_sample,
              wchar_t *zero_one, wchar_t *one, wchar_t *one_zero, wchar_t *zero) {
  int start_sample = first_sample, end_sample = max_sample;

  /* Draw the waveform: horizontal levels and vertical transitions */
  int prev_val = signals[PIN][start_sample > 0 ? start_sample - 1 : start_sample];

  if (start_sample < 0) start_sample = 0;
  if (end_sample > NUM_SAMPLES) end_sample = NUM_SAMPLES;

  for (int sample_time = start_sample; sample_time < end_sample; ++sample_time) {
    int val = signals[PIN][sample_time];
    int x = NAME_COL_WIDTH + sample_time - first_sample;

    if (x+((x-x_min)*(width-1)) < x_min || x+((x-x_min)*(width-1)) > x_max) {
      continue;
    }

    if (sample_time == 0) {
      mvaddwstr(y, x+((x-x_min)*(width-1)), L" ");
      if (val == 0) {
        mvaddwstr(y, x+((x-x_min)*(width-1)+1), zero+1);
      } else {
        mvaddwstr(y, x+((x-x_min)*(width-1)+1), one+1);
      }
    } else if (prev_val == 0 && val == 0) {
      mvaddwstr(y, x+((x-x_min)*(width-1)), zero);
    } else if (prev_val == 0 && val == 1) {
      mvaddwstr(y, x+((x-x_min)*(width-1)), zero_one);
    } else if (prev_val == 1 && val == 1) {
      mvaddwstr(y, x+((x-x_min)*(width-1)), one);
    } else if (prev_val == 1 && val == 0) {
      mvaddwstr(y, x+((x-x_min)*(width-1)), one_zero);
    } else {
      exit(1);
    }
    prev_val = val;
  }
}

void draw_top_row(int y, int x_min, int x_max, int PIN, int first_sample, int max_sample) {
  draw_row(y, x_min, x_max, PIN, first_sample, max_sample, mbox.tl, mbox.h, mbox.tr, L" ");
}
void draw_mid_row(int y, int x_min, int x_max, int PIN, int first_sample, int max_sample) {
  draw_row(y, x_min, x_max, PIN, first_sample, max_sample, mbox.v, L" ", mbox.v, L" ");
}
void draw_bot_row(int y, int x_min, int x_max, int PIN, int first_sample, int max_sample) {
  draw_row(y, x_min, x_max, PIN, first_sample, max_sample, mbox.br, L" ", mbox.bl, mbox.h);
}

static void draw_signal_row(int row_y, int PIN, int first_sample, int max_sample, int max_x, int rows) {
  draw_top_row(row_y, NAME_COL_WIDTH, max_x, PIN, first_sample, max_sample);
  for (int y = row_y + 1; y < row_y + rows - 1; y++)
    draw_mid_row(y, NAME_COL_WIDTH, max_x, PIN, first_sample, max_sample);
  draw_bot_row(row_y + rows - 1, NAME_COL_WIDTH, max_x, PIN, first_sample, max_sample);
}

int max_y, max_x;
static void draw_bus_row(int row_y, int x_min, int PIN, int start_sample, int end_sample) {
  if (start_sample < 0) start_sample = 0;
  if (end_sample > NUM_SAMPLES) end_sample = NUM_SAMPLES;
    
  for (int sample_time = start_sample; sample_time < end_sample /*- 10*/; ++sample_time) {
    int D = (signals[PIN+0][sample_time]&1)<<7 |
      (signals[PIN+1][sample_time]&1)<<6 |
      (signals[PIN+2][sample_time]&1)<<5 |
      (signals[PIN+3][sample_time]&1)<<4 |
      (signals[PIN+4][sample_time]&1)<<3 |
      (signals[PIN+5][sample_time]&1)<<2 |
      (signals[PIN+6][sample_time]&1)<<1 |
      (signals[PIN+7][sample_time]&1);
    int x = NAME_COL_WIDTH + sample_time - start_sample;
    //if (sample_time % 10 == 0) {
      if (x+((x-x_min)*(width-1)) <= max_x-RIGHT_MARGIN) {   // NAME_COL_WIDTH + sample_time - RIGHT_MARGIN /*- 4 + 13*/) {
        mvprintw(row_y-1, (width-1) + x+((x-x_min)*(width-1)), "%01x ", (D>>4)&0xF);
        mvprintw(row_y  , (width-1) + x+((x-x_min)*(width-1)), "%01x ",  D    &0xF);
      }
    //}
    
  }
}

static void draw_axis(int row_y, int x_min, int start_sample, int end_sample) {
  char str[64];
  if (start_sample < 0) start_sample = 0;
  if (end_sample > NUM_SAMPLES) end_sample = NUM_SAMPLES;
  
  for (int sample_time = start_sample; sample_time < end_sample; ++sample_time) {
    int x = NAME_COL_WIDTH + sample_time - start_sample;

    if (sample_time % 10 == 0) {
      sprintf(str, "| %d", sample_time);
      if ((width-1) + x+((x-x_min)*(width-1)) + (int)strlen(str) <= max_x -3) mvprintw(row_y, (width-1) + x+((x-x_min)*(width-1)), str);
    }
    
  }
}

/* Draw everything: names + signal rows + status line */
int rows_per_signal = ROWS_PER_SIGNAL+1;
static void draw_screen(int first_sample) {

  /* Clear screen */
  erase();

  /* Header */
  //mvprintw(0, 0, "Timing diagram demo  (Left/Right to scroll, q to quit)");

  /* For each signal, compute row positions.
       Use two rows per signal plus one blank row (if space). */
  int row_base = 2, s;
  //rows_per_signal = ROWS_PER_SIGNAL+1;
  for (s = 0; s < NUM_SIGNALS; ++s) {
    int base_y = row_base + s * (rows_per_signal + 1);
    if (base_y + 1 >= max_y - 1) break; /* no more vertical space */

    /* Signal name on left */
    if (s < 4) {
      mvprintw(base_y + (rows_per_signal-1)/2, 0, "%-*s", NAME_COL_WIDTH - 1, signal_names[s]);
    } else if (s == 4) {
      // D0..7
      mvprintw(base_y + (rows_per_signal-1)/2, 0, "%-*s", NAME_COL_WIDTH - 1, signal_names[s]);
    } else if (s >= 4+8) {
    }

    x_start = NAME_COL_WIDTH;
    x_end = max_x - RIGHT_MARGIN;
    y_high = base_y;
    y_low = base_y + 1;

    //for (int x = x_start; x <= x_end; ++x) {
    //  for (int y = y_high; y >= y_low; y--) mvaddch(y, x, ' ');
    //}

    /* Draw signal waveform */
    if (s < 4) {
      draw_signal_row(base_y, s,
                      first_sample,
                      first_sample + max_x - NAME_COL_WIDTH,
                      max_x - RIGHT_MARGIN,
                      rows_per_signal);
    } else if (s == 4) {
      draw_bus_row(base_y+1, NAME_COL_WIDTH, s,
                   first_sample,
                   first_sample + max_x - NAME_COL_WIDTH);
    }
  }

  draw_axis(0, // row_base + NUM_SIGNALS * (rows_per_signal + 1),
            NAME_COL_WIDTH,
            first_sample,
            first_sample + max_x - NAME_COL_WIDTH);
  
  /* Status line at bottom */
  int status_y = 22; // max_y - 1;
  if (status_y >= 0) {
    //mvhline(status_y, 0, ' ', max_x);
    mvprintw(status_y, 0, "Source of this demo is at http://www.gtoal.com/src/timing_diagram.c");
    mvprintw(status_y+2, 0, "Up/down arrow: change scale");
    mvprintw(status_y+3, 0, "Left/right arrow: move one unit");
    mvprintw(status_y+4, 0, "Page up/page down: move 10 units");
    mvprintw(status_y+5, 0, "Q: quit");
  }

  refresh();
}

/* --------- Main --------- */

int main(void) {
  int first_sample = 0;
  int ch;

  /* Enable locale; required for wide chars in many curses implementations */
  setlocale(LC_ALL, "");

  generate_demo_data();
  initscr();
  keypad(stdscr, TRUE);
  cbreak();
  noecho();
  nodelay(stdscr, FALSE); /* blocking getch for simplicity */
  curs_set(0);            /* hide cursor if possible */

  init_boxchars();

  getmaxyx(stdscr, max_y, max_x);
  draw_screen(first_sample);
  
  while ((ch = getch()) != 'q' && ch != 'Q') {
    int displayable_samples = ((max_x - RIGHT_MARGIN) - NAME_COL_WIDTH)/width + 1;    
    switch (ch) {
      case KEY_UP:
        if (rows_per_signal < 3) rows_per_signal += 1;
        width = 2; init_boxchars();
        displayable_samples = ((max_x - RIGHT_MARGIN) - NAME_COL_WIDTH)/width + 1; 
        break;
      case KEY_DOWN:
        if (rows_per_signal > 2) rows_per_signal -= 1;
        width = 1; init_boxchars();
        displayable_samples = ((max_x - RIGHT_MARGIN) - NAME_COL_WIDTH)/width + 1; 
        if (first_sample + displayable_samples + 1 >= NUM_SAMPLES) {
          first_sample = NUM_SAMPLES - displayable_samples;
        }
        break;
      case KEY_SLEFT:
      case KEY_PPAGE:        first_sample -= 10;        break;
      case KEY_LEFT:         first_sample -=  1;        break;
      case KEY_SRIGHT:
      case KEY_NPAGE:
        for (int i = 0; i < 10; i++) {
          if (first_sample + displayable_samples + 1 > NUM_SAMPLES) break;
                             first_sample +=  1;
        }
        break;
      case KEY_RIGHT:
        if (first_sample + displayable_samples + 1 > NUM_SAMPLES) break;
                             first_sample +=  1;        break;
      default:                                                     break;
    }
    if (first_sample < 0) first_sample = 0;

    getmaxyx(stdscr, max_y, max_x);
    draw_screen(first_sample);
  }

  endwin();
  return 0;
}
