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

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;

// 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 ecce_move(int lines)
{
  while (lines > 0) {
    while (fp != lend) *pp++ = *fp++;
    if (fp == fend) break;
    *pp++ = *fp++;
    lbeg = pp; lend = fp;
    while (*lend != '\n') lend++;
    lines -= 1;
  }
}

void ecce_right(int cols)
{
  while (cols > 0) {
    if (fp == fend) break;
    *pp++ = *fp++;
    cols -= 1;
  }
}

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;
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];
  a[MAX_BUF_BYTES-1] = '\n';
  for (;;) {
    c = fgetc(tmp);
    if (c == EOF) break;
    *pp++ = c;
    filesize += 1;
  }
  fclose(tmp);
  while (pp != fbeg) {
    *--fp = *--pp;
  }
  lbeg = pp;
  lend = fp;
  while (*lend != '\n') lend += 1;
}

void generate_screen_line_start_pointers(char *pp, int *this_line, int *this_col)
{
  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++;
    col += 1; /* or add width of expansion of c */
    if (c == '\n') {
      l[++lpend].sp = p;
      l[lpend].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_screen_line = FIRST_DISPLAY_LINE,
      last_screen_line = LAST_DISPLAY_LINE,
      cursor_col = 0;
  int screen_leftmost_column = DISPLAY_LEFTMOST_COLUMN,
      screen_rightmost_column = DISPLAY_RIGHTMOST_COLUMN;

  if (pref_display_line > last_screen_line) pref_display_line = last_screen_line;
  move(0, 0);
  generate_screen_line_start_pointers(pp, &this_virtual_screen_line, &cursor_col);

  first_screen_line = this_virtual_screen_line-pref_display_line;
  if (first_screen_line < FIRST_DISPLAY_LINE) {
    pref_display_line = pref_display_line + first_screen_line;
    first_screen_line = first_screen_line - first_screen_line;
  }
  if (lpend < last_screen_line) {
    first_screen_line = FIRST_DISPLAY_LINE;
  }
  last_screen_line = first_screen_line+LAST_DISPLAY_LINE; // except if file ends earlier
  for (y = first_screen_line; y <= last_screen_line; y++) {
    int eol = FALSE;
    standend();
    if (y >= lpend) {
      move(y-first_screen_line, screen_leftmost_column);
      if (y == this_virtual_screen_line) standout();
      for (x = screen_leftmost_column; x < screen_rightmost_column; x++) addch(' ');
      standend();
    } else {
      for (x = screen_leftmost_column; x < screen_rightmost_column; x++) {
        move(y-first_screen_line, x);
        c = l[y].sp[x];
        if (c == '\n') eol = TRUE;
        if ((y == this_virtual_screen_line) && ((x-screen_leftmost_column) == cursor_col)) standout();
        addch(eol ? ' ' : c);
      }
    }
  }
  move(pref_display_line, cursor_col);
}

void execute(int command)
{
  static int need_refresh = (0==0);

  if (command == '\e' && getch() == ERR) {
    move(0, 0); clear(); refresh(); endwin();
    exit(0);
// to add: HOME, END (start/end of file)
  } else if (command == KEY_DOWN || command == '\r') {
    preferred_display_line += 1;
    if (preferred_display_line > LAST_DISPLAY_LINE) preferred_display_line = LAST_DISPLAY_LINE;
    ecce_move(1);
    need_refresh = (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;
    ecce_move(LAST_DISPLAY_LINE);
    need_refresh = (0==0);
  } else if (command == KEY_UP) {
    need_refresh = (0==0);
  } else if (command == KEY_PPAGE) {
    need_refresh = (0==0);
  } else if (command == KEY_LEFT) {
  } else if (command == KEY_RIGHT) {
    if (fp == lend) {
      ecce_move(1);
      preferred_display_line += 1;
    } else *pp++ = *fp++;
  } else if (command == ERR) {
    // update only when there is no more type-ahead
    if (need_refresh) update_display(pp, preferred_display_line);
    need_refresh = (0!=0);
  }
}

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

  initscr(); cbreak(); noecho();
  nonl();
  intrflush(stdscr, FALSE);
  keypad(stdscr, TRUE);
  nodelay(stdscr, TRUE);

  getsize(argc == 1 ? "vide.c" : argv[1], &MAX_BUF_BYTES, &MAX_DISPLAY_LINES);

  allocatebuffer(MAX_BUF_BYTES, MAX_DISPLAY_LINES);

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

  for (;;) {
    command = getch();
    execute(command);
    update_display(pp, preferred_display_line); refresh(); usleep(400);
  }

  move(0, 0); clear(); refresh(); endwin();

  exit(0);
}
