#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ncurses.h>

#include <assert.h>
#include <ctype.h>
#include "trie.h"


// to do - flushinp() to cancel type-ahead when there is an ecce error reported
// (and then wait for the user to see it. (Enter?) (Issue ^G too?)

// current serious bug: entering an ecce command of all spaces (or
// any equivalent ignorable noise characters) causes a lock-up.  probably
// looping looking for more data and getting an EOF it doesn't handle

// entering a number to repeat the last command unfortunately repeats
// the last immediate-mode command too, because things like cursor-right
// are implemented as callbacks to ecce commands.  Fix by recording
// the last entered ecce command, and re-issuing it on a number.
// need to take care re 'hidden' ecce commands contained in keystroke
// macro expansions.

char *expand[256] = {
#include "expand.h"
};

static int default_highlighting = TRUE;

void inverted_screen(void)
{
  default_highlighting = !default_highlighting;
}

void highlight(void)
{
  if (!default_highlighting) standend(); else standout();
}

void normal(void)
{
  if (default_highlighting) standend(); else standout();
}

typedef struct virtual_screen_line {
  char *sp; // pointer to next byte to be displayed on screen
  char *st; // partial text of multi-char expansion from
            // previous line.
} virtual_screen_line;

void getsize(char *fname, int *MAX_BUF_BYTES, int *MAX_DISPLAY_LINES)
{
  FILE *tmp = fopen(fname, "rw");
  *MAX_BUF_BYTES = -1; *MAX_DISPLAY_LINES = -1;
  if (tmp == NULL) return;
  fseek(tmp, 0L, SEEK_END);
  *MAX_BUF_BYTES = (int)ftell(tmp);
  fclose(tmp);
  *MAX_BUF_BYTES = (*MAX_BUF_BYTES + 1024*256) * 3;
  *MAX_DISPLAY_LINES = *MAX_BUF_BYTES / 8; /* approx, worst case eg for dict files */
}

// virtual screen variables
virtual_screen_line *l;

// ECCE variables
char *a;
char *fbeg, *lbeg, *pp, *fp, *lend, *fend;
extern char ecce_err[256];
extern void ecinner(char *command);
void ecinnerf(char *format, int num) // HACK
{
  char command[256];
  sprintf(command, format, num);
  ecinner(command);
}

// display (sliding cursor window) variables
int FIRST_DISPLAY_LINE = 0;
int LAST_DISPLAY_LINE = 23;
static int preferred_display_line = 0;
int DISPLAY_LEFTMOST_COLUMN = 0;
int DISPLAY_RIGHTMOST_COLUMN = 79;

void allocatebuffer(int MAX_BUF_BYTES, int MAX_DISPLAY_LINES)
{
  a = malloc(MAX_BUF_BYTES);
  l = malloc(sizeof(virtual_screen_line)*MAX_DISPLAY_LINES);
}

int lpend = 0; // temp hacks...
int filesize = 0;
void loadfile(char *fname, int MAX_BUF_BYTES)
{
  FILE *tmp = fopen(fname, "r");
  int c;
  a[0] = '\n';
  lbeg = pp = fbeg = &a[1];
  fp = fend = &a[MAX_BUF_BYTES-1];
  *fend = '\n';
  for (;;) {
    c = fgetc(tmp);
    if (c == EOF) break;
    *pp++ = c;
    filesize += 1;
  }
  fclose(tmp);
  while (pp != fbeg) {
    *--fp = *--pp;
  }
  lend = fp;
  while (*lend != '\n') lend += 1;
}

void generate_screen_line_start_pointers(char *pp, int *this_line, int *this_col)
{
  // at the moment this is only used for finding the screen coords of the
  // cursor, but the main reason for it is for scrolling upwards.  Eventually
  // it won't be invoked more than it needs to be.  This is just a placeholder.
  int c, col;
  char *p;
  p = fbeg;
  lpend = 0;
  l[lpend].sp = p;
  l[lpend].st = "";
  col = 0;
  for (;;) {
    if (p == pp) {
      p = fp;
      *this_col = col;
      *this_line = lpend;
    }
    if (p == fend) break;
    c = (*p++)&255;
    if (c == '\t') {
      int spaces = ((col+8)&(~7))-col; // at least 1
      if (col+spaces > DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) { // wrap
        char *eight = "        ";
        int wrapped_part = (col+spaces)-(DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN); // 1..8
        int this_part = (DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN)-col;
        col += this_part;
        l[lpend+1].st = &eight[8-(wrapped_part-1)];
      } else col += spaces;
    } else if (c != '\n') { // STILL DOES IT???
      if (col+strlen(expand[c]) > DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) { // wrap
        int extra = col+strlen(expand[c]) - (DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN);
        col = DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN;
        l[lpend+1].st = expand[c]+strlen(expand[c])-extra;
      } else {
        col += 1; /* or add width of expansion of c */
        l[lpend+1].st = "";  // may be able to fold this with the case above
      }
    }
    if (c == '\n') { // AHA!  Oops - should be in the display routine, not the scanner!!!
      // NEED TO PAD WITH SPACES TO EOL IF DOING ICL7502-STYLE HIGHLIGHTING
      l[++lpend].sp = p;
      // safety coding:
      if (l[lpend].st == NULL) l[lpend].st = "";
      l[lpend+1].st = "";
      // if we run out of lines, do a realloc() ...
      col = 0;
    } else if (col == DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) {
      l[++lpend].sp = p;
      // safety coding:
      if (l[lpend].st == NULL) l[lpend].st = "";
      l[lpend+1].st = "";
      // if we run out of lines, do a realloc() ...
      col = 0;
    }
  }
}

void update_display(char *pp, int pref_display_line)
{
  int c, x, y, line, this_virtual_screen_line;
  int first_virtual_screen_line,
      cursor_col = 0;

  if (pref_display_line > LAST_DISPLAY_LINE) pref_display_line = LAST_DISPLAY_LINE;
  if (pref_display_line < FIRST_DISPLAY_LINE) pref_display_line = FIRST_DISPLAY_LINE;

  generate_screen_line_start_pointers(pp, &this_virtual_screen_line, &cursor_col); // SIDE-EFFECT: sets lpend

  first_virtual_screen_line = this_virtual_screen_line-pref_display_line+FIRST_DISPLAY_LINE;
  if (first_virtual_screen_line < 0) first_virtual_screen_line = 0;
  if (first_virtual_screen_line > lpend) first_virtual_screen_line = lpend;

  if (first_virtual_screen_line+LAST_DISPLAY_LINE-FIRST_DISPLAY_LINE > lpend) {
    first_virtual_screen_line = lpend - (LAST_DISPLAY_LINE-FIRST_DISPLAY_LINE);
    pref_display_line = LAST_DISPLAY_LINE;
  }
  if (first_virtual_screen_line < 0) first_virtual_screen_line = 0;

  if (this_virtual_screen_line-first_virtual_screen_line < LAST_DISPLAY_LINE-FIRST_DISPLAY_LINE) {
    // requested line too far down page, so move it up
    pref_display_line = first_virtual_screen_line+FIRST_DISPLAY_LINE; // tie to top
  }

  for (y = first_virtual_screen_line; y <= first_virtual_screen_line+LAST_DISPLAY_LINE-FIRST_DISPLAY_LINE; y++) {
    int eol = FALSE;

      char *p;
      int c, col = 0;

      move(y-first_virtual_screen_line+FIRST_DISPLAY_LINE, DISPLAY_LEFTMOST_COLUMN);

      // Output wrapped part of .st string first, if present ...
      p = l[y].st;
      if (p == NULL) break; // coding error
      while (*p != '\0') {
        addch(*p); p += 1; col += 1;
      }
      p = l[y].sp;
      if (p == NULL) break; // coding error
      for (;;) {
        if (p == pp) {
          p = fp;
          cursor_col = DISPLAY_LEFTMOST_COLUMN+col;
          pref_display_line = y-first_virtual_screen_line;
          highlight();
        }
        if (p == fend) {
          normal();
          clrtobot();
          break;
        }
        c = (*p++)&255;
        if (c == '\t') {
          int spaces = ((col+8)&(~7))-col; // at least 1
          if (col+spaces > DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) { // wrap
            int wrapped_part = (col+spaces)-(DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN); // 1..8
            int this_part = (DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN)-col;
            col += this_part;
// DOESN'T INVERSE!            highlight(); clrtoeol(); normal();
            while (this_part > 0) {
              addch(' '); this_part -= 1;
            }
            break;
          } else {
            col += spaces;
// DOESN'T INVERSE!            highlight(); clrtoeol(); normal();
            while (spaces > 0) {
              addch(' '); spaces -= 1;
            }
//            move(y, DISPLAY_LEFTMOST_COLUMN+col); // is this now redundant?
          }
        } else if (c != '\n') {
          char *exp = expand[c];
          while (*exp != '\0') {
            addch(*exp++);
            col += 1;
             if (col == DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) break;
          }
        }
        if (c == '\n' || (col == DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN)) {
          clrtoeol(); normal();
          col = 0; // output wrapped string here?
          break;
        }
      }
  }
  move(pref_display_line, cursor_col);
}

int execute(int command)
{
  static char command_line[128];
  static int savedmac[128]; // sort out size limitations later
  static int *savedmacp = savedmac;
  static int nextc = 0;
  static int ecce_mode = FALSE;
  static int learning_mode = FALSE;
  static int update_required = (0==0);
  static int correction = 0;
  static int target_col = -1;

  // nasty modeful hack :-(

  if (command == ERR) {
    // *should* repaint the screen any time the keyboard is idle and
    // the display size has changed.  Unfortunately it doesn't until
    // the next keypress.  Not sure why.  But better to leave this in
    // slightly broken than omit resizing altogether, which is severely
    // broken ...

    if ((!ecce_mode) && ((LAST_DISPLAY_LINE != LINES-1) || (DISPLAY_RIGHTMOST_COLUMN != COLS))) {
      LAST_DISPLAY_LINE = LINES-1;
      DISPLAY_RIGHTMOST_COLUMN = COLS;
//      update_display(pp, preferred_display_line);
//      refresh();
//      return;
    }
    usleep(10000); // makes the difference between a load-average of 1 and tiny!
                   // without this, we get ERR calls as fast as the system can generate them
  } else {
    if (learning_mode) {
      if (command != ('E'&31)) {
        *savedmacp++ = command; *savedmacp = ERR; // record keystrokes before all other processing
      }
    }
    if ((command == ('L'&31)) && (!ecce_mode)) {
      if (learning_mode) {
        if (savedmacp != savedmac) savedmacp -= 1; *savedmacp = ERR; // remove final ^L
        // get key sequence and assign learned commands to key sequence.
        // key sequence must be unique path through trie, not a prefix, otherwise can't assign.
        learning_mode = FALSE; inverted_screen();
        if (savedmacp == savedmac) { // ^L^L - treat as a single ^L?
          clear();
          update_required = (0==0);
        }
      } else {
        // Start learning mode!
        learning_mode = TRUE; inverted_screen();
        savedmacp = savedmac;
        *savedmacp = ERR;
      }
      return;
    } else if (command == ('E'&31)) {
      // TEMPORARY PROOF OF CONCEPT: Execute stored macro
      learning_mode = FALSE; // quick hack to avoid recursion problems
      savedmacp = savedmac;
      while (*savedmacp != ERR) {
        execute(*savedmacp++);
      }
      return;
    } else if ((command == '\r') && (!ecce_mode)) {
      // don't special-case enter until in ecce mode
    } else {
      if ((!ecce_mode) && ((' ' <= command) && (command <= '~'))) {
        // although we want redefine characters to take precedence,
        // simply moving this test to the end does not work...
        ecce_mode = TRUE; nextc = 0;
        if (preferred_display_line == LAST_DISPLAY_LINE) preferred_display_line -= (correction = 1);
        LAST_DISPLAY_LINE -= 1;
        update_required = (0==0);
      }
      if (ecce_mode && (command == '\r')) {
        command_line[nextc] = '\0';
        ecce_mode = FALSE;
        preferred_display_line += correction; correction = 0;
        LAST_DISPLAY_LINE += 1;
        nextc = 0;
        // execute the command here
        ecinner(command_line);
        command_line[nextc] = '\0';
        target_col = -1;
        update_display(pp, preferred_display_line);
        if (*ecce_err != '\0') {int x=DISPLAY_LEFTMOST_COLUMN;
         char *s;
         move(LAST_DISPLAY_LINE, 0);
         s=ecce_err;
         highlight();
         for (;;) {
           if (*s == '\0') break;
           if (*s == '\n') break;
//           move(LAST_DISPLAY_LINE+1,x++);
           addch(*s++);
         }
         while (x++ < DISPLAY_RIGHTMOST_COLUMN) addch(' ');
         normal();
         *ecce_err = '\0';
         refresh();
        }
        return(TRUE);

      } else if (ecce_mode) {
        int x, len;

        command_line[nextc++] = command;
        command_line[nextc] = '\0';
        if (nextc == 127) nextc = 126; // truncate

        if (update_required) {
          update_required = (0!=0);
          update_display(pp, preferred_display_line);
        }
        move(LAST_DISPLAY_LINE+1, DISPLAY_LEFTMOST_COLUMN);
        highlight();
        len = strlen(command_line);

        // NEED TO OUTPUT PROMPT.  need generic routine for
        // handling input line.  Must be ready for "F!" so that
        // it prompts for the find/replace strings, for when we
        // bind commands like f// to a single key...

        for (x = 0; x < len; x++) {
          addch(command_line[x]);
        }
        normal();
        for (x = DISPLAY_LEFTMOST_COLUMN+len; x < DISPLAY_RIGHTMOST_COLUMN; x++) {
          addch(' ');
        }

        move(LAST_DISPLAY_LINE+1, DISPLAY_LEFTMOST_COLUMN+strlen(command_line)); // Plus strlen(prompt) when added...
        refresh();
        return(TRUE);
      }
    }
  }
  if (command == '\e' && getch() == ERR) {
    return(FALSE);
  // to add: HOME, END (start/end of file)
  } else if (command == KEY_DOWN || command == '\r') {
    int line, before, after;
    preferred_display_line += 1;
    if (preferred_display_line > LAST_DISPLAY_LINE) preferred_display_line = LAST_DISPLAY_LINE;
    generate_screen_line_start_pointers(pp, &line, &before);
    if (target_col < 0) target_col = before;
    {
      ecinner("m");
      for (;;) {
        generate_screen_line_start_pointers(pp, &line, &after);
        if (after >= target_col) break;
        ecinner("r");
        if (*ecce_err != '\0') break;
      }
    }
    update_required = (0==0);
  } else if (command == KEY_NPAGE || command == ' ' /* "more" clone? */) {
    preferred_display_line += LAST_DISPLAY_LINE;
    if (preferred_display_line > LAST_DISPLAY_LINE) preferred_display_line = LAST_DISPLAY_LINE;
    ecinnerf("m%0d", LAST_DISPLAY_LINE);
    update_required = (0==0);
  } else if (command == KEY_UP) {
    int line, before, after;
    preferred_display_line -= 1;
    if (preferred_display_line < FIRST_DISPLAY_LINE) preferred_display_line = FIRST_DISPLAY_LINE;
    generate_screen_line_start_pointers(pp, &line, &before);
    if (target_col < 0) target_col = before;
    {
      ecinner("m-r0");
      for (;;) {
        generate_screen_line_start_pointers(pp, &line, &after);
        if (after <= target_col) break;
        ecinner("l");
      }
    }
    update_required = (0==0);
  } else if (command == KEY_HOME) {
    ecinner("m-0");
    update_required = (0==0);
  } else if (command == KEY_END) {
    ecinner("m0");
    update_required = (0==0);
  } else if (command == 0x14A) {
    ecinner("e");
    update_required = (0==0);
  } else if (command == 0x14B) {
    ecinner("i/ /l");
    update_required = (0==0);
  } else if (command == 0x107) {
    ecinner("e-");
    update_required = (0==0);
  } else if (command == KEY_PPAGE) {
    preferred_display_line -= LAST_DISPLAY_LINE;
    if (preferred_display_line < FIRST_DISPLAY_LINE) preferred_display_line = FIRST_DISPLAY_LINE;
    ecinnerf("m-%0d", LAST_DISPLAY_LINE);
    update_required = (0==0);
  } else if (command == KEY_LEFT) {
    if (pp == lbeg && pp != fbeg) {
      ecinner("m-r0");
      // see below ... ecinner("(l,m-r0)");
      preferred_display_line -= 1;
      if (preferred_display_line < FIRST_DISPLAY_LINE) preferred_display_line = FIRST_DISPLAY_LINE;
    } else ecinner("l");
    target_col = -1;
    update_required = (0==0);
  } else if (command == KEY_RIGHT) {
    if (fp == lend) {
      ecinner("m");
      // use just ecinner("(r,m)") if ecinner is allowed to update the display info
      preferred_display_line += 1;
      if (preferred_display_line > LAST_DISPLAY_LINE) preferred_display_line = LAST_DISPLAY_LINE;
    } else ecinner("r");
    target_col = -1;
    update_required = (0==0);
  } else if (command == ' ') { // 'next' - re-execute last ecce command
    ecinner("1");
    target_col = -1;
    update_required = (0==0);
  } else if (command == ('L'&31)) { // refresh
    clear();
    update_required = (0==0);
  } else if (command == ERR) {
    // update only when there is no more type-ahead
    if (update_required) {
      update_display(pp, preferred_display_line);
      refresh();
    }
    update_required = (0!=0);
  } else {
    // Debug info: unknown key?
    move(0, DISPLAY_RIGHTMOST_COLUMN-9);
    {char *hex = "0123456789ABCDEF";
     highlight();
     addch(hex[(command>>28)&15]);
     addch(hex[(command>>24)&15]);
     addch(hex[(command>>20)&15]);
     addch(hex[(command>>16)&15]);
     addch(hex[(command>>12)&15]);
     addch(hex[(command>>8)&15]);
     addch(hex[(command>>4)&15]);
     addch(hex[command&15]);
     normal();
    }
  }
  return(TRUE);
}

int main(int argc, char **argv)
{
  int MAX_BUF_BYTES, MAX_DISPLAY_LINES;
  int i, this_line;
  int command;
  char *p;
  if (argc == 111) {
    char *s;
    fprintf(stderr, "syntax: %s filename\n",
                    (s=strrchr(argv[0], '/')) == NULL ? argv[0] : ++s);
    exit(1);
  }
  getsize(argc == 1 ? "tabtest.txt" : argv[1], &MAX_BUF_BYTES, &MAX_DISPLAY_LINES);
  if (MAX_BUF_BYTES < 0) {
    char *s;
    fprintf(stderr, "%s: cannot edit %s\n",
                    (s=strrchr(argv[0], '/')) == NULL ? argv[0] : ++s, argc == 1 ? "tabtest.txt" : argv[1]);
    exit(1);
  }

  initscr(); cbreak(); noecho();
  nonl();
  intrflush(stdscr, FALSE);
  keypad(stdscr, TRUE);
  nodelay(stdscr, TRUE);
  LAST_DISPLAY_LINE = LINES-1;
  DISPLAY_RIGHTMOST_COLUMN = COLS;

  allocatebuffer(MAX_BUF_BYTES, MAX_DISPLAY_LINES);

  loadfile(argc == 1 ? "tabtest.txt" : argv[1], MAX_BUF_BYTES);

  init_globals(); ecinner("%e");
  for (;;) {
    command = getch();
    if (!execute(command)) break;
  }
  free_buffers();
  move(0, 0); clear(); refresh(); endwin();

  exit(0);
}