// todo esc handling slightly broken

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

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

int keydebug = TRUE;

// 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] = { // A single byte in the file can be represented
                      // on screen by an arbitrary multi-byte string
#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 ( sp < pp || sp >= fp )
  char *st; // partial text of multi-char expansion from
            // previous line. (This text is NOT to be freed by the display code)
} 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;

// 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;
int target_col = -1;


// ECCE variables.  Eventually ecinner will be rewritten so that all inputs are
// parameters and all other variables are off the stack.  Currently there are
// enough globals that it is impossible for ecinner to call itself recursively.

char *a;
char *fbeg, *lbeg, *pp, *fp, *lend, *fend;

// display algorithm at the moment does too much work.  However it is meant to
// be code that we can be very sure is correct, and it is built in such a way
// that optimisations can be added later.  I don't want to fall into the trap
// that Hamish did of spending most of the development time in fixing slightly
// broken screen displays.  The call below locates the first byte in each
// screen display line - plus all the lines off the screen, were they to be
// scrolled into display.  Characters which expand into more than one position
// on-screen, part of which wraps to the next line, have a pointer to the
// wrapped part of the text stored with each line.  This was the simplest
// scheme I could think of.  The wrapped text is considered to be the last
// character of the previous line.
void generate_screen_line_start_pointers(char *pp, int *this_line, int *this_col);

#include "ecinner.h"

// a trivial hack to allow inserting a number in repeat commands before calling
// ecinner.  I know I can do this better with vsprintf but so far the trivial
// case is all I've used.
void ecinnerf(char *format, int num) // HACK
{
  char command[256];
  sprintf(command, format, num);
  ecinner(command);
}

// Design decision: my version of ecce does not have a sliding RAM window
// on a file (necessitating pushing and pulling off each end - I did this
// in my "COED" editor at Acorn and did not like it); nor does it dynamically
// expand the buffer (eg with realloc).  My assumption is that memory sizes
// are big enough nowadays to grab it all at startup and err on the high
// side.
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;
}

// still to do - write file on exit.  Not done during development to avoid
// accidentally trashing the source of the editor. (Been there, done that :-( )

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;

    // first expand anything that maps to > 1 character
    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') {
      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
      }
    }

    // handle end of line?
    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) { // This is 'The Gap' - skip over.
          p = fp;
          cursor_col = DISPLAY_LEFTMOST_COLUMN+col;
          pref_display_line = y-first_virtual_screen_line; // note cursor pos to return to at end
          highlight();
        }
        if (p == fend) { // We have hot end of file
          normal();
          clrtobot();
          break;
        }
        c = (*p++)&255;
        if (c == '\t') { // Expand tabs. (this duplicates the code above :-/ )
          int spaces = ((col+8)&(~7))-col; // at least 1
          if (col+spaces > DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) { // wrapped tab
            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;
            while (this_part > 0) {
              addch(' '); this_part -= 1;
            }
            break;
          } else { // regular non-wrapping tab
            col += spaces;
            while (spaces > 0) {
              addch(' '); spaces -= 1;
            }
//            move(y, DISPLAY_LEFTMOST_COLUMN+col); // is this now redundant?
          }
        } else if (c != '\n') {  // regular char.  *All* chars are treated as an
          char *exp = expand[c]; // expansion sequence, even if that sequence is
          while (*exp != '\0') { // just a trivial 1:1 mapping...
            addch(*exp++);
            if (++col == DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN) break;
          }
        }
        if (c == '\n' || (col == DISPLAY_RIGHTMOST_COLUMN-DISPLAY_LEFTMOST_COLUMN)) {
          clrtoeol(); normal(); // end of line.  (The 'normal()' is probably redundant)
          col = 0;
          break;
        }
      }
  }
  move(pref_display_line, cursor_col); // place the flashing cursor at 'point'.
}

int learn = FALSE;
int waiting_for_key = FALSE;

#include "execute.h"

#include "screen.h"

int main(int argc, char **argv)
{
  struct NODE *base = NULL;
  int *macro_trigger = NULL;
  int *expansion = NULL;
  int macro_trigger_length = 0;
  int expansion_length = 0;
  int macro_trigger_max_length = 0;
  int expansion_max_length = 0;
  int command = -127, lastcommand, count = 0;
  int assigning = FALSE, insequence = FALSE;

  int handle_key(int i)
  {
    void optionally_learn_then_execute(int i) {
       if (learn && (i != ERR)) {
          assert(expansion != NULL);
          if (expansion_length == expansion_max_length) {
            // the macro we are learning outgrew the space available, so double it!
            expansion = realloc(expansion, sizeof(int) * (expansion_max_length *= 2));
            assert(expansion != NULL);
          }
          assert(expansion_max_length > expansion_length);
          expansion[expansion_length++] = i;
          if (keydebug) fprintf(stderr, "execute+remember %d (%d)\r\n", i, expansion_length);
       }
       screen_edit(i); // next step in the display/execute pipeline. (Could be cleaned up)
       // in ecce mode, "screen_edit" should just record the command until the \n when it executes
    }

    if (ecce_mode) {
      optionally_learn_then_execute(command); return;
    }

    if (command == ('L'&31)) {
      // design decision: to avoid the hassles of recursive macro learning, ^L is
      // both hard-wired and a toggle.  Should never get into a macro at least with
      // the meaning of 'learn'.  May get into an ecce string as a literal.
      // Two consecutive ^L^L might be a convention to do a screen refresh. (TO DO)
      if (learn == FALSE) {
        // First ^L: start learning, but execute as you learn...
        if (keydebug) fprintf(stderr, "start recording\r\n");
        if (expansion == NULL) {
          // first time out, initialise the storage for macros.
          expansion = malloc(sizeof(int) * (expansion_max_length=1));
	}
        assert(expansion_max_length >= 1);
        expansion_length = 0; // should already be 0
        learn = TRUE;
        update_required = TRUE;
        //inverted_screen(); -- at one point I inverted the screen as a reminder that we are learning.
        //                      but now I put up a message in the corner of the screen.  Ideally I
        //                      would reserve a line for that, but currently it's just stolen! (overlaid)
      } else {
        // Second ^L: assign current sequence to the next key
        if (keydebug) {int i;
        fprintf(stderr, "end recording, wait for keypress\r\n");
        fprintf(stderr, "macro expansion sequence:");
        for (i=0; i<expansion_length; i++) fprintf(stderr, " %d", expansion[i]);
        fprintf(stderr, "\r\n");
	}
        learn = FALSE;
        update_required = TRUE;
        waiting_for_key = TRUE;
      }
    } else if (waiting_for_key) {
      // We've learned a macro, now we assign it to the following key.
      // However nothing is ever simple.  *Some* keys come through as a
      // single extended keycode under ncurses, *but* others come as
      // multiple keys.  Current assumption is that multiple keys
      // have no "ERR" codes between them, so the first ERR terminates
      // the key sequence.  A nice side-effect of this with the way that
      // ncurses handles <ESC> is that you can type multi-key sequences
      // using an <ESC> prefix, which come through as a unit, eg
      // <ESC><ESC><CURSOR-RIGHT>
      // Perhaps in the future I may want to allow completely arbitrary
      // sequences to be assigned, at which point I'll need an 'end of
      // trigger sequence' key as well.
      static int firsttime = 0;
      if (command == ERR) {
        // ignore only if nothing has been pressed at all yet.
        if (firsttime == 0) redraw_all(); firsttime = 1;
        // small annoyance: our general purpose mechanism for updating
        // on idle is messed up by the special significance of ERR in this area.
      } else if (command == 27) {
        // pressing a single <ESC> at this point throws away the learned
        // sequence entirely (i.e. you messed up and don't want to assign
        // it to any keys)
        strcpy(ecce_err, "Macro discarded!");
        update_required = TRUE;
        firsttime = 0;
        assigning = FALSE;
        waiting_for_key = FALSE;
        macro_trigger_length = 0;
      } else {
        firsttime = 0;
        // record this char as the first of the execute sequence
        if (keydebug) fprintf(stderr, "macro trigger start %d\r\n", command);
        if (macro_trigger == NULL) {
          macro_trigger = malloc(sizeof(int) * (macro_trigger_max_length=1));
	}
        assert(macro_trigger_max_length >= 1);
        macro_trigger_length = 0;
        macro_trigger[macro_trigger_length++] = command;
        assigning = TRUE;
        waiting_for_key = FALSE;
        update_required = TRUE;
      }
    } else if (assigning) {
      if (command == ERR) {
        // lack of keystrokes means multi-key sequence is complete
        if (keydebug) {int i;
        fprintf(stderr, "macro trigger sequence:");
        for (i=0; i<macro_trigger_length; i++) fprintf(stderr, " %d", macro_trigger[i]);
        fprintf(stderr, "\r\n");
	}
        base = add_sequence(base, macro_trigger, macro_trigger_length, expansion, expansion_length);
        macro_trigger_length = 0;
        expansion_length = 0;
        if (keydebug) fprintf(stderr, "macro trigger done\r\n");
        assigning = FALSE;
      } else {
        // add one more char to the execute sequence
        if (keydebug) fprintf(stderr, "macro trigger key %d\r\n", command);
        assert(macro_trigger != NULL);
        if (macro_trigger_length == macro_trigger_max_length) {
          macro_trigger = realloc(macro_trigger, sizeof(int) * (macro_trigger_max_length *= 2));
	}
        assert(macro_trigger_max_length > macro_trigger_length);
        macro_trigger[macro_trigger_length++] = command;
      }
    } else {
      // regular keypresses, so special cases.
      // execute incoming keys, possibly multi-byte sequence
      // which will trigger a macro expansion, which in turn
      // may be a multi-byte sequence.  However expanded macros
      // do *not* invoke other macros.  The expansion is done
      // at record time.  This also allows easy re-assignment
      // of keys.
      if (command == ERR) {
          // keyboard idle.  Good time to refresh the screen if it needs it.
          screen_edit(ERR);
          if (lastcommand == ('C'&31)) return(FALSE);  // special case for hard-wired QUIT (may change)
      } else {
       static struct NODE *current_node = NULL; // if we are in the middle of a multi-key trigger
                                                // sequence, this points to the possible next states
                                                // in the trie.  Some work does need to be done here
                                                // for error recovery when we go down the garden path too far
       {
          if (insequence) { // does this continue a sequence???
            struct NODE *each;
            int found = FALSE;
            each = current_node;
            while (each != NULL) {
              if (each->c == command) {
                found = TRUE;
                current_node = each;
                break;
	      }
              each = each->sibling;
	    }
            if (found) { // yes...
              if (each->is_macro) {
                // alpha version - execute each->expansion ... final version, only if no children
                if (keydebug) fprintf(stderr, "execute stored sequence ending in %d\r\n", command);
                {int i;
                for (i = 0; i < each->expansion_length; i++) {
                  if (keydebug) fprintf(stderr, "recursively execute %d\r\n", each->expansion[i]);
                  optionally_learn_then_execute(each->expansion[i]);
		}
		}
                current_node = NULL;
                insequence = FALSE;
	      } else {
                current_node = each->child; // still in the same expansion
                insequence = TRUE;
	      }
	    } else {
              // the first few bytes look like the start of a sequence but it turns out it wasn't.
              // if we had an allowed sequence in the lead-up to this byte then execute it first,
              // but since we don't do that yet, just execute the individual keys one by one
              // as if they were not part of a macro.
              if (keydebug) if (current_node != NULL) fprintf(stderr, "execute stored sequence up to %d\r\n", command);
              insequence = FALSE;
              current_node = NULL;
              if (keydebug) fprintf(stderr, "execute %d\r\n", command); // just go round again???
              optionally_learn_then_execute(command);
	    }
	  } else { // we are not in the middle of a trigger sequence...
            struct NODE *each;
            int found = FALSE;
            // so does this start a new sequence???
            each = base;
            while (each != NULL) {
              if (each->c == command) {
                found = TRUE;
                current_node = each;
                break;
              }
              each = each->sibling;
            }
            if (found) { // yes, it does...
              if (each->is_macro) {
                // alpha version - execute each->expansion ... final version, only if no children
                // currently we execute the first valid sequence we find.  So if we had
                // ABC triggering event A and AB triggering event B, then only B would ever be triggered
                if (keydebug) fprintf(stderr, "execute stored sequence ending in %d\r\n", command);
                {int i;
                for (i = 0; i < each->expansion_length; i++) {
                  if (keydebug) fprintf(stderr, "recursively execute %d\r\n", each->expansion[i]);
                  optionally_learn_then_execute(each->expansion[i]);
		}
		}
                current_node = NULL;
                insequence = FALSE;
	      } else {
                current_node = each->child;
                insequence = TRUE;
	      }
	    } else {
              // default for non-macro single keypresses
              if (keydebug) if (current_node != NULL) fprintf(stderr, "execute stored sequence %d\r\n", command);
              insequence = FALSE;
              current_node = NULL;
              if (keydebug) fprintf(stderr, "execute %d\r\n", command); // just go round again???
              optionally_learn_then_execute(command);
	    }
	  }
	}
      }
    }
  }


  int MAX_BUF_BYTES, MAX_DISPLAY_LINES;
  int i, this_line;
  char *p;
  if (argc == 111) { // set to 111 for debugging - otherwise normally 1
    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); // note debug hack
  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);
  }

  load_database(&base);
  initscr(); raw();/*cbreak();*/ noecho(); nonl();  // use cbreak if debugging, to allow use of ^\ break-in
  intrflush(stdscr, FALSE); keypad(stdscr, TRUE); nodelay(stdscr, TRUE);
  LAST_DISPLAY_LINE = LINES-1;  // these are sort of constants hence the upper case convention.
  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"); // can search case insensitive, but "c" has its normal meaning.
                                 // 'old-style' ecce default (case sensitive) is "%n"
  for (;;) {
    lastcommand = command;
    command = getch();
    if (!handle_key(command)) break;  // breaks on ^C if in raw mode.
  }

  free_buffers();
  sleep(1); move(0, 0); clear(); refresh(); sleep(1); endwin();
  dump_database(base);

  exit(0);
}
