/* vi.c - You can't spell "evil" without "vi".
 *
 * Copyright 2015 Rob Landley <rob@landley.net>
 * Copyright 2019 Jarno Mäkipää <jmakip87@gmail.com>
 *
 * See http://pubs.opengroup.org/onlinepubs/9699919799/utilities/vi.html

USE_VI(NEWTOY(vi, "<1>1", TOYFLAG_USR|TOYFLAG_BIN))

config VI
  bool "vi"
  default n
  help
    usage: vi FILE
    Visual text editor. Predates the existence of standardized cursor keys,
    so the controls are weird and historical.
*/

#define FOR_vi
#include "toys.h"

GLOBALS(
    int cur_col;
    int cur_row;
    int scr_row;
    int drawn_row;
    int drawn_col;
    unsigned screen_height;
    unsigned screen_width;
    int vi_mode;
    int count0;
    int count1;
    int vi_mov_flag;
    int modified;
    char vi_reg;
    char *last_search;
    int tabstop;
    int list;
)

struct str_line {
  int alloc;
  int len;
  char *data;
};
//yank buffer
struct yank_buf {
  char reg;
  int alloc;
  char* data;
};


//lib dllist uses next and prev kinda opposite what im used to so I just
//renamed both ends to up and down
struct linelist {
  struct linelist *up;//next
  struct linelist *down;//prev
  struct str_line *line;
};

static void draw_page();

//utf8 support
static int utf8_lnw(int* width, char* str, int bytes);
static int utf8_dec(char key, char *utf8_scratch, int *sta_p);
static int utf8_len(char *str);
static int utf8_width(char *str, int bytes);
static char* utf8_last(char* str, int size);


static int cur_left(int count0, int count1, char* unused);
static int cur_right(int count0, int count1, char* unused);
static int cur_up(int count0, int count1, char* unused);
static int cur_down(int count0, int count1, char* unused);
static void check_cursor_bounds();
static void adjust_screen_buffer();
static int search_str(char *s);

static int vi_yank(char reg, struct linelist *row, int col, int flags);
static int vi_delete(char reg, struct linelist *row, int col, int flags);

//inserted line not yet pushed to buffer
struct str_line *il;
struct linelist *text; //file loaded into buffer
struct linelist *screen;//current screen coord 0 row
struct linelist *c_r;//cursor position row

struct yank_buf yank; //single yank

// TT.vi_mov_flag is used for special cases when certain move
// acts differently depending is there DELETE/YANK or NOP
// Also commands such as G does not default to count0=1
// 0x1 = Command needs argument (f,F,r...)
// 0x2 = Move 1 right on yank/delete/insert (e, $...)
// 0x4 = yank/delete last line fully
// 0x10000000 = redraw after cursor needed
// 0x20000000 = full redraw needed
// 0x40000000 = count0 not given
// 0x80000000 = move was reverse

void dlist_insert_nomalloc(struct double_list **list, struct double_list *new)
{
  if (*list) {
    new->next = *list;
    new->prev = (*list)->prev;
    if ((*list)->prev) (*list)->prev->next = new;
    (*list)->prev = new;
  } else *list = new->next = new->prev = new;
}


// Add an entry to the end of a doubly linked list
struct double_list *dlist_insert(struct double_list **list, char *data)
{
  struct double_list *new = xmalloc(sizeof(struct double_list));
  new->data = data;
  dlist_insert_nomalloc(list, new);

  return new;
}

void linelist_free(void *node)
{
  struct linelist *lst = (struct linelist *)node;
  free(lst->line->data), free(lst->line), free(lst);
}

void linelist_unload()
{
  void* list = 0;
  for (;text->down; text = text->down);
  list = (void*)text;
  text = screen = c_r = 0;
  llist_traverse(list, linelist_free);
}

void write_file(char *filename)
{
  struct linelist *lst = text;
  FILE *fp = 0;
  if (!filename) filename = (char*)*toys.optargs;
  if (!(fp = fopen(filename, "w")) ) return;

  for (;lst; lst = lst->down)
    fprintf(fp, "%s\n", lst->line->data);

  fclose(fp);
}

int linelist_load(char *filename)
{
  struct linelist *lst = c_r;//cursor position or 0
  FILE *fp = 0;
  if (!filename)
    filename = (char*)*toys.optargs;

  fp = fopen(filename, "r");
  if (!fp) {
    char *line = xzalloc(80);
    ssize_t alc = 80;
    lst = (struct linelist*)dlist_add((struct double_list**)&lst,
        xzalloc(sizeof(struct str_line)));
    lst->line->alloc = alc;
    lst->line->len = 0;
    lst->line->data = line;
    text = lst;
    dlist_terminate(text->up);
    return 1;
  }

  for (;;) {
    char *line = xzalloc(80);
    ssize_t alc = 80;
    ssize_t len;
    if ((len = getline(&line, (void *)&alc, fp)) == -1) {
      if (errno == EINVAL || errno == ENOMEM) {
        printf("error %d\n", errno);
      }
      free(line);
      break;
    }
    lst = (struct linelist*)dlist_add((struct double_list**)&lst,
        xzalloc(sizeof(struct str_line)));
    lst->line->alloc = alc;
    lst->line->len = len;
    lst->line->data = line;

    if (lst->line->data[len-1] == '\n') {
      lst->line->data[len-1] = 0;
      lst->line->len--;
    }
    if (text == 0) text = lst;
  }

  if (text) dlist_terminate(text->up);

  fclose(fp);
  return 1;

}

int vi_yy(char reg, int count0, int count1)
{
  struct linelist *pos = c_r;
  int col = TT.cur_col;
  TT.cur_col = 0;
  TT.vi_mov_flag |= 0x4;

  if (count0>1) cur_down(count0-1, 1, 0);

  vi_yank(reg, pos, 0, 0);

  TT.cur_col = col, c_r = pos;
  return 1;
}

int vi_dd(char reg, int count0, int count1)
{
  struct linelist *pos = c_r;
  TT.cur_col = 0;
  TT.vi_mov_flag |= 0x4;
  if (count0>1) cur_down(count0-1, 1, 0);

  vi_delete(reg, pos, 0, 0);
  check_cursor_bounds();
  return 1;
}

static int vi_x(char reg, int count0, int count1)
{
  char *last = 0, *cpos = 0, *start = 0;
  int len = 0;
  struct linelist *pos = c_r;
  int col = TT.cur_col;
  if (!c_r) return 0;

  start = c_r->line->data;
  len = c_r->line->len;

  last = utf8_last(start, len);
  cpos = start+TT.cur_col;
  if (cpos == last) {
    cur_left(count0-1, 1, 0);
    col = strlen(start);
  }
  else {
    cur_right(count0-1, 1, 0);
    cpos = start+TT.cur_col;
    if (cpos == last) TT.vi_mov_flag |= 2;
    else cur_right(1, 1, 0);
  }

  vi_delete(reg, pos, col, 0);
  check_cursor_bounds();
  return 1;
}

//move commands does not behave correct way yet.
int vi_movw(int count0, int count1, char* unused)
{
  int count = count0*count1;
  const char *empties = " \t\n\r";
  const char *specials = ",.=-+*/(){}<>[]";
//  char *current = 0;
  if (!c_r)
    return 0;
  if (TT.cur_col == c_r->line->len-1 || !c_r->line->len)
    goto next_line;
  if (strchr(empties, c_r->line->data[TT.cur_col]))
    goto find_non_empty;
  if (strchr(specials, c_r->line->data[TT.cur_col])) {
    for (;strchr(specials, c_r->line->data[TT.cur_col]); ) {
      TT.cur_col++;
      if (TT.cur_col == c_r->line->len-1)
        goto next_line;
    }
  } else for (;!strchr(specials, c_r->line->data[TT.cur_col]) &&
      !strchr(empties, c_r->line->data[TT.cur_col]);) {
      TT.cur_col++;
      if (TT.cur_col == c_r->line->len-1)
        goto next_line;
  }

  for (;strchr(empties, c_r->line->data[TT.cur_col]); ) {
    TT.cur_col++;
find_non_empty:
    if (TT.cur_col == c_r->line->len-1) {
next_line:
      //we could call j and g0
      if (!c_r->down) return 0;
      c_r = c_r->down;
      TT.cur_col = 0;
      if (!c_r->line->len) break;
    }
  }
  count--;
  if (count>0)
    return vi_movw(count, 1, 0);

  check_cursor_bounds();
  return 1;
}

static int vi_movb(int count0, int count1, char* unused)
{
  int count = count0*count1;
  if (!c_r)
    return 0;
  if (!TT.cur_col) {
      if (!c_r->up) return 0;
      c_r = c_r->up;
      TT.cur_col = (c_r->line->len) ? c_r->line->len-1 : 0;
      goto exit_function;
  }
  if (TT.cur_col)
      TT.cur_col--;
  while (c_r->line->data[TT.cur_col] <= ' ') {
    if (TT.cur_col) TT.cur_col--;
    else goto exit_function;
  }
  while (c_r->line->data[TT.cur_col] > ' ') {
    if (TT.cur_col)TT.cur_col--;
    else goto exit_function;
  }
  TT.cur_col++;
exit_function:
  count--;
  if (count>1)
    return vi_movb(count, 1, 0);
  TT.vi_mov_flag |= 0x80000000;
  check_cursor_bounds();
  return 1;
}

static int vi_move(int count0, int count1, char *unused)
{
  int count = count0*count1;
  if (!c_r)
    return 0;
  if (TT.cur_col < c_r->line->len)
    TT.cur_col++;
  if (c_r->line->data[TT.cur_col] <= ' ' || count > 1)
    vi_movw(count, 1, 0); //find next word;
  while (c_r->line->data[TT.cur_col] > ' ')
    TT.cur_col++;
  if (TT.cur_col) TT.cur_col--;

  TT.vi_mov_flag |= 2;
  check_cursor_bounds();
  return 1;
}


static void i_insert(char* str, int len)
{
  char *t = xzalloc(c_r->line->alloc);
  char *s = c_r->line->data;
  int sel = c_r->line->len-TT.cur_col;
  strncpy(t, &s[TT.cur_col], sel);
  t[sel+1] = 0;
  if (c_r->line->alloc < c_r->line->len+len+5) {
    c_r->line->data = xrealloc(c_r->line->data,
      (c_r->line->alloc+len)<<1);

    c_r->line->alloc = (c_r->line->alloc+len)<<1;
    memset(&c_r->line->data[c_r->line->len], 0,
        c_r->line->alloc-c_r->line->len);

    s = c_r->line->data;
  }
  strncpy(&s[TT.cur_col], str, len);
  strcpy(&s[TT.cur_col+len], t);
  TT.cur_col += len;
  if (TT.cur_col) TT.cur_col--;

  c_r->line->len += len;
  free(t);

  TT.vi_mov_flag |= 0x30000000;
}

//new line at split pos;
void i_split()
{
  int alloc = 0, len = 0, idx = 0;
  struct str_line *l = xmalloc(sizeof(struct str_line));
  alloc = c_r->line->alloc;

  if (TT.cur_col) len = c_r->line->len-TT.cur_col-1;
  else len = c_r->line->len;
  if (len < 0) len = 0;

  l->data = xzalloc(alloc);
  l->alloc = alloc;
  l->len = len;
  idx = c_r->line->len - len;

  strncpy(l->data, &c_r->line->data[idx], len);
  memset(&l->data[len], 0, alloc-len);

  c_r->line->len -= len;
  if (c_r->line->len <= 0) c_r->line->len = 0;

  len = c_r->line->len;

  memset(&c_r->line->data[len], 0, alloc-len);
  c_r = (struct linelist*)dlist_insert((struct double_list**)&c_r, (char*)l);
  c_r->line = l;
  TT.cur_col = 0;
}


static int vi_zero(int count0, int count1, char *unused)
{
  TT.cur_col = 0;
  TT.vi_mov_flag |= 0x80000000;
  return 1;
}

static int vi_eol(int count0, int count1, char *unused)
{
  int count = count0*count1;
  for (;count > 1 && c_r->down; count--)
    c_r = c_r->down;

  if (c_r && c_r->line->len)
    TT.cur_col = c_r->line->len-1;
  TT.vi_mov_flag |= 2;
  check_cursor_bounds();
  return 1;
}

//TODO check register where to push from
static int vi_push(char reg, int count0, int count1)
{
  char *start = yank.data, *end = yank.data+strlen(yank.data);
  struct linelist *cursor = c_r;
  int col = TT.cur_col;
  //insert into new lines
  if (*(end-1) == '\n') for (;start != end;) {
    TT.vi_mov_flag |= 0x10000000;
    char *next = strchr(start, '\n');
    TT.cur_col = (c_r->line->len) ? c_r->line->len-1: 0;
    i_split();
    if (next) {
      i_insert(start, next-start);
      start = next+1;
    } else start = end; //??
  }

  //insert into cursor
  else for (;start != end;) {
    char *next = strchr(start, '\n');
    if (next) {
      TT.vi_mov_flag |= 0x10000000;
      i_insert(start, next-start);
      i_split();
      start = next+1;
    } else {
      i_insert(start, strlen(start));
      start = end;
    }
  }
  //if row changes during push original cursor position is kept
  //vi inconsistancy
  if (c_r != cursor) c_r = cursor, TT.cur_col = col;

  return 1;
}

static int vi_find_c(int count0, int count1, char *symbol)
{
  int count = count0*count1;
  if (c_r && c_r->line->len) {
    while (count--) {
        char* pos = strstr(&c_r->line->data[TT.cur_col], symbol);
        if (pos) {
          TT.cur_col = pos-c_r->line->data;
          return 1;
        }
    }
  }
  return 0;
}

static int vi_find_cb(int count0, int count1, char *symbol)
{
  //do backward search
  return 1;
}

//if count is not spesified should go to last line
static int vi_go(int count0, int count1, char *symbol)
{
  int prev_row = TT.cur_row;
  c_r = text;

  if (TT.vi_mov_flag&0x40000000) for (;c_r && c_r->down; c_r = c_r->down);
  else for (;c_r && c_r->down && --count0; c_r = c_r->down);

  TT.cur_col = 0;
  check_cursor_bounds();  //adjusts cursor column
  if (prev_row>TT.cur_row) TT.vi_mov_flag |= 0x80000000;

  return 1;
}

//need to refactor when implementing yank buffers
static int vi_delete(char reg, struct linelist *row, int col, int flags)
{
  struct linelist *start = 0, *end = 0;
  int col_s = 0, col_e = 0, bytes = 0;

  vi_yank(reg, row, col, flags);

  if (TT.vi_mov_flag&0x80000000) {
    start = c_r, end = row;
    col_s = TT.cur_col, col_e = col;
  } else {
    start = row, end = c_r;
    col_s = col, col_e = TT.cur_col;
  }
  if (start == end) goto last_line_delete;
  if (!col_s) goto full_line_delete;

  memset(start->line->data+col_s, 0, start->line->len-col_s);
  row->line->len = col_s;
  col_s = 0;
  start = start->down;

full_line_delete:
  TT.vi_mov_flag |= 0x10000000;
  for (;start != end;) {
    struct linelist* lst = start;
    //struct linelist *lst = dlist_pop(&start);
    start = start->down;
    if (lst->down) lst->down->up = lst->up;
    if (lst->up) lst->up->down = lst->down;
    if (screen == lst) screen = lst->down ? lst->down : lst->up;
    if (text == lst) text = lst->down;
    free(lst->line->data);
    free(lst->line);
    free(lst);
  }
last_line_delete:
  TT.vi_mov_flag |= 0x10000000;
  if (TT.vi_mov_flag&2) col_e = start->line->len;
  if (TT.vi_mov_flag&4) {
    if (!end->down && !end->up)
      col_e = start->line->len;
    else {
      col_e = 0, col_s = 0;
      if (end->down) end->down->up = end->up;
      if (end->up) end->up->down = end->down;
      if (screen == end) screen = end->down ? end->down : end->up;
      //if (text == end) text = end->down;
      start = end->down ? end->down : end->up;
      free(end->line->data);
      free(end->line);
      free(end);

    }
  }
  if (col_s < col_e) {
    bytes = col_s + start->line->len - col_e;
    memmove(start->line->data+col_s, start->line->data+col_e,
        start->line->len-col_e);
    memset(start->line->data+bytes, 0, start->line->len-bytes);
    start->line->len = bytes;
  }
  c_r = start;
  TT.cur_col = col_s;
  return 1;
}

static int vi_D(char reg, int count0, int count1)
{
  int prev_col = TT.cur_col;
  struct linelist *pos = c_r;
  if (!count0) return 1;
  vi_eol(1, 1, 0);
  vi_delete(reg, pos, prev_col, 0);
  count0--;
  if (count0 && c_r->down) {
    c_r = c_r->down;
    vi_dd(reg, count0, 1);
  }
  check_cursor_bounds();
  return 1;
}

static int vi_join(char reg, int count0, int count1)
{
  while (count0--) {
    if (c_r && c_r->down) {
      int size = c_r->line->len+c_r->down->line->len;
      if (size > c_r->line->alloc) {
        if (size > c_r->down->line->alloc) {
          c_r->line->data = xrealloc(c_r->line->data,
            c_r->line->alloc*2+il->alloc*2);
          memmove(&c_r->line->data[c_r->line->len],
              c_r->down->line->data,c_r->down->line->len);
          c_r->line->len = size;
          c_r = c_r->down;
          c_r->line->alloc = c_r->line->alloc*2+2*il->alloc;
          vi_dd(0,1,1);
        } else {
          memmove(&c_r->down->line->data[c_r->line->len],
              c_r->down->line->data,c_r->down->line->len);
          memmove(c_r->down->line->data,c_r->line->data,
              c_r->line->len);
          c_r->down->line->len = size;
          vi_dd(0,1,1);
        }
      } else {
          memmove(&c_r->line->data[c_r->line->len],
              c_r->down->line->data,c_r->down->line->len);
          c_r->line->len = size;
          c_r = c_r->down;
          vi_dd(0,1,1);
      }
      c_r = c_r->up;

    }
  }
  return 1;
}

static int vi_find_next(char reg, int count0, int count1)
{
  if (TT.last_search) search_str(TT.last_search);
  return 1;
}

static int vi_change(char reg, struct linelist *row, int col, int flags)
{
  vi_delete(reg, row, col, flags);
  TT.vi_mode = 2;
  return 1;
}

//TODO search yank buffer by register
//now only supports default register
static int vi_yank(char reg, struct linelist *row, int col, int flags)
{
  struct linelist *start = 0, *end = 0;
  int col_s = 0, col_e = 0, bytes = 0;

  memset(yank.data, 0, yank.alloc);
  if (TT.vi_mov_flag&0x80000000) {
    start = c_r, end = row;
    col_s = TT.cur_col, col_e = col;
  } else {
    start = row, end = c_r;
    col_s = col, col_e = TT.cur_col;
  }
  if (start == end) goto last_line_yank;
  if (!col_s) goto full_line_yank;

  if (yank.alloc < start->line->alloc) {
    yank.data = xrealloc(yank.data, start->line->alloc*2);
    yank.alloc = start->line->alloc*2;
  }

  sprintf(yank.data, "%s\n", start->line->data+col_s);
  col_s = 0;
  start = start->down;

full_line_yank:
  for (;start != end;) {
    while (yank.alloc-1 < strlen(yank.data)+start->line->len)
      yank.data = xrealloc(yank.data, yank.alloc*2), yank.alloc *= 2;


    sprintf(yank.data+strlen(yank.data), "%s\n", start->line->data);
    start = start->down;
  }
last_line_yank:
  while (yank.alloc-1 < strlen(yank.data)+end->line->len)
    yank.data = xrealloc(yank.data, yank.alloc*2), yank.alloc *= 2;

  if (TT.vi_mov_flag & 0x4)
    sprintf(yank.data+strlen(yank.data), "%s\n", start->line->data);
  else {
    bytes = strlen(yank.data)+col_e-col_s;
    strncpy(yank.data+strlen(yank.data), end->line->data+col_s, col_e-col_s);
    yank.data[bytes] = 0;
  }
  return 1;
}

//NOTES
//vi-mode cmd syntax is
//("[REG])[COUNT0]CMD[COUNT1](MOV)
//where:
//-------------------------------------------------------------
//"[REG] is optional buffer where deleted/yanked text goes REG can be
//  atleast 0-9, a-z or default "
//[COUNT] is optional multiplier for cmd execution if there is 2 COUNT
//  operations they are multiplied together
//CMD is operation to be executed
//(MOV) is movement operation, some CMD does not require MOV and some
//  have special cases such as dd, yy, also movements can work without
//  CMD
//ex commands can be even more complicated than this....
//
struct vi_cmd_param {
  const char* cmd;
  unsigned flags;
  int (*vi_cmd)(char, struct linelist*, int, int);//REG,row,col,FLAGS
};
struct vi_mov_param {
  const char* mov;
  unsigned flags;
  int (*vi_mov)(int, int, char*);//COUNT0,COUNT1,params
};
//special cases without MOV and such
struct vi_special_param {
  const char *cmd;
  int (*vi_special)(char, int, int);//REG,COUNT0,COUNT1
};
struct vi_special_param vi_special[] =
{
  {"dd", &vi_dd},
  {"yy", &vi_yy},
  {"D", &vi_D},
  {"J", &vi_join},
  {"n", &vi_find_next},
  {"x", &vi_x},
  {"p", &vi_push}
};
//there is around ~47 vi moves
//some of them need extra params
//such as f and '
struct vi_mov_param vi_movs[] =
{
  {"0", 0, &vi_zero},
  {"b", 0, &vi_movb},
  {"e", 0, &vi_move},
  {"G", 0, &vi_go},
  {"h", 0, &cur_left},
  {"j", 0, &cur_down},
  {"k", 0, &cur_up},
  {"l", 0, &cur_right},
  {"w", 0, &vi_movw},
  {"$", 0, &vi_eol},
  {"f", 1, &vi_find_c},
  {"F", 1, &vi_find_cb},
};
//change and delete unfortunately behave different depending on move command,
//such as ce cw are same, but dw and de are not...
//also dw stops at w position and cw seem to stop at e pos+1...
//so after movement we need to possibly set up some flags before executing
//command, and command needs to adjust...
struct vi_cmd_param vi_cmds[] =
{
  {"c", 1, &vi_change},
  {"d", 1, &vi_delete},
  {"y", 1, &vi_yank},
};

int run_vi_cmd(char *cmd)
{
  int i = 0, val = 0;
  char *cmd_e;
  int (*vi_cmd)(char, struct linelist*, int, int) = 0;
  int (*vi_mov)(int, int, char*) = 0;

  TT.count0 = 0, TT.count1 = 0, TT.vi_mov_flag = 0;
  TT.vi_reg = '"';

  if (*cmd == '"') {
    cmd++;
    TT.vi_reg = *cmd; //TODO check validity
    cmd++;
  }
  errno = 0;
  val = strtol(cmd, &cmd_e, 10);
  if (errno || val == 0) val = 1, TT.vi_mov_flag |= 0x40000000;
  else cmd = cmd_e;
  TT.count0 = val;

  for (i = 0; i < ARRAY_LEN(vi_special); i++) {
    if (strstr(cmd, vi_special[i].cmd)) {
      return vi_special[i].vi_special(TT.vi_reg, TT.count0, TT.count1);
    }
  }

  for (i = 0; i < ARRAY_LEN(vi_cmds); i++) {
    if (!strncmp(cmd, vi_cmds[i].cmd, strlen(vi_cmds[i].cmd))) {
      vi_cmd = vi_cmds[i].vi_cmd;
      cmd += strlen(vi_cmds[i].cmd);
      break;
    }
  }
  errno = 0;
  val = strtol(cmd, &cmd_e, 10);
  if (errno || val == 0) val = 1;
  else cmd = cmd_e;
  TT.count1 = val;

  for (i = 0; i < ARRAY_LEN(vi_movs); i++) {
    if (!strncmp(cmd, vi_movs[i].mov, strlen(vi_movs[i].mov))) {
      vi_mov = vi_movs[i].vi_mov;
      TT.vi_mov_flag |= vi_movs[i].flags;
      cmd++;
      if (TT.vi_mov_flag&1 && !(*cmd)) return 0;
      break;
    }
  }
  if (vi_mov) {
    int prev_col = TT.cur_col;
    struct linelist *pos = c_r;
    if (vi_mov(TT.count0, TT.count1, cmd)) {
      if (vi_cmd) return (vi_cmd(TT.vi_reg, pos, prev_col, TT.vi_mov_flag));
      else return 1;
    } else return 0; //return some error
  }
  return 0;
}

static int search_str(char *s)
{
  struct linelist *lst = c_r;
  char *c = strstr(&c_r->line->data[TT.cur_col+1], s);

  if (TT.last_search != s) {
    free(TT.last_search);
    TT.last_search = xstrdup(s);
  }

  if (c) {
    TT.cur_col = c-c_r->line->data;
  } else for (; !c;) {
    lst = lst->down;
    if (!lst) return 1;
    c = strstr(lst->line->data, s);
  }
  c_r = lst;
  TT.cur_col = c-c_r->line->data;
  check_cursor_bounds();
  return 0;
}

int run_ex_cmd(char *cmd)
{
  if (cmd[0] == '/') {
    search_str(&cmd[1]);
  } else if (cmd[0] == '?') {
    // TODO: backwards search.
  } else if (cmd[0] == ':') {
    if (!strcmp(&cmd[1], "q") || !strcmp(&cmd[1], "q!")) {
      // TODO: if no !, check whether file modified.
      //exit_application;
      return -1;
    }
    else if (strstr(&cmd[1], "wq")) {
      write_file(0);
      return -1;
    }
    else if (strstr(&cmd[1], "w")) {
      write_file(0);
      return 1;
    }
    else if (strstr(&cmd[1], "set list")) {
      TT.list = 1;
      TT.vi_mov_flag |= 0x30000000;
      return 1;
    }
    else if (strstr(&cmd[1], "set nolist")) {
      TT.list = 0;
      TT.vi_mov_flag |= 0x30000000;
      return 1;
    }
  }
  return 0;

}

void vi_main(void)
{
  char keybuf[16] = {0};
  char vi_buf[16] = {0};
  char utf8_code[8] = {0};
  int utf8_dec_p = 0, vi_buf_pos = 0;

  il = xzalloc(sizeof(struct str_line));
  il->data = xzalloc(80);
  yank.data = xzalloc(128);

  il->alloc = 80, yank.alloc = 128;

  linelist_load(0);
  screen = c_r = text;

  TT.vi_mov_flag = 0x20000000;
  TT.vi_mode = 1, TT.tabstop = 8;
  TT.screen_width = 80, TT.screen_height = 24;

  terminal_size(&TT.screen_width, &TT.screen_height);
  TT.screen_height -= 2; //TODO this is hack fix visual alignment

  set_terminal(0, 1, 0, 0);
  //writes stdout into different xterm buffer so when we exit
  //we dont get scroll log full of junk
  tty_esc("?1049h");
  tty_esc("H");
  xflush(1);


  draw_page();
  while(1) {
    int key = scan_key(keybuf, -1);

    terminal_size(&TT.screen_width, &TT.screen_height);
    TT.screen_height -= 2; //TODO this is hack fix visual alignment

    // TODO: support cursor keys in ex mode too.
    if (TT.vi_mode && key>=256) {
      key -= 256;
      if (key==KEY_UP) cur_up(1, 1, 0);
      else if (key==KEY_DOWN) cur_down(1, 1, 0);
      else if (key==KEY_LEFT) cur_left(1, 1, 0);
      else if (key==KEY_RIGHT) cur_right(1, 1, 0);
      draw_page();
      continue;
    }

    switch (key) {
      case -1:
      case 3:
      case 4:
        goto cleanup_vi;
    }
    if (TT.vi_mode == 1) { //NORMAL
      switch (key) {
        case '/':
        case '?':
        case ':':
          TT.vi_mode = 0;
          il->data[0]=key;
          il->len++;
          break;
        case 'A':
          vi_eol(1, 1, 0);
          // FALLTHROUGH
        case 'a':
          if (c_r && c_r->line->len) TT.cur_col++;
          // FALLTHROUGH
        case 'i':
          TT.vi_mode = 2;
          break;
        case 27:
          vi_buf[0] = 0;
          vi_buf_pos = 0;
          break;
        default:
          if (key > 0x20 && key < 0x7B) {
            vi_buf[vi_buf_pos] = key;//TODO handle input better
            vi_buf_pos++;
            if (run_vi_cmd(vi_buf)) {
              memset(vi_buf, 0, 16);
              vi_buf_pos = 0;
            }
            else if (vi_buf_pos == 16) {
              vi_buf_pos = 0;
              memset(vi_buf, 0, 16);
            }

          }

          break;
      }
    } else if (TT.vi_mode == 0) { //EX MODE
      switch (key) {
        case 0x7F:
        case 0x08:
          if (il->len > 1) {
            il->data[--il->len] = 0;
            break;
          }
          // FALLTHROUGH
        case 27:
          TT.vi_mode = 1;
          il->len = 0;
          memset(il->data, 0, il->alloc);
          break;
        case 0x0D:
          if (run_ex_cmd(il->data) == -1)
            goto cleanup_vi;
          TT.vi_mode = 1;
          il->len = 0;
          memset(il->data, 0, il->alloc);
          break;
        default: //add chars to ex command until ENTER
          if (key >= 0x20 && key < 0x7F) { //might be utf?
            if (il->len == il->alloc) {
              il->data = realloc(il->data, il->alloc*2);
              il->alloc *= 2;
            }
            il->data[il->len] = key;
            il->len++;
          }
          break;
      }
    } else if (TT.vi_mode == 2) {//INSERT MODE
      switch (key) {
        case 27:
          i_insert(il->data, il->len);
          TT.vi_mode = 1;
          il->len = 0;
          memset(il->data, 0, il->alloc);
          break;
        case 0x7F:
        case 0x08:
          if (il->len) {
            char *last = utf8_last(il->data, il->len);
            int shrink = strlen(last);
            memset(last, 0, shrink);
            il->len -= shrink;
          }
          break;
        case 0x0D:
          //insert newline
          //
          i_insert(il->data, il->len);
          il->len = 0;
          memset(il->data, 0, il->alloc);
          i_split();
          break;
        default:
          if ((key >= 0x20 || key == 0x09) &&
              utf8_dec(key, utf8_code, &utf8_dec_p)) {

            if (il->len+utf8_dec_p+1 >= il->alloc) {
              il->data = realloc(il->data, il->alloc*2);
              il->alloc *= 2;
            }
            strcpy(il->data+il->len, utf8_code);
            il->len += utf8_dec_p;
            utf8_dec_p = 0;
            *utf8_code = 0;

          }
          break;
      }
    }

    draw_page();

  }
cleanup_vi:
  linelist_unload();
  free(il->data), free(il), free(yank.data);
  tty_reset();
  tty_esc("?1049l");
}

int vi_crunch(FILE* out, int cols, int wc)
{
  int ret = 0;
  if (wc < 32 && TT.list) {
    tty_esc("1m");
    ret = crunch_escape(out,cols,wc);
    tty_esc("m");
  } else if (wc == 0x09) {
    if (out) {
      int i = TT.tabstop;
      for (;i--;) fputs(" ", out);
    }
    ret = TT.tabstop;
  }
  return ret;
}

//crunch_str with n bytes restriction for printing substrings or
//non null terminated strings
int crunch_nstr(char **str, int width, int n, FILE *out, char *escmore,
  int (*escout)(FILE *out, int cols, int wc))
{
  int columns = 0, col, bytes;
  char *start, *end;

  for (end = start = *str; *end && n>0; columns += col, end += bytes, n -= bytes) {
    wchar_t wc;

    if ((bytes = utf8towc(&wc, end, 4))>0 && (col = wcwidth(wc))>=0) {
      if (!escmore || wc>255 || !strchr(escmore, wc)) {
        if (width-columns<col) break;
        if (out) fwrite(end, bytes, 1, out);

        continue;
      }
    }

    if (bytes<1) {
      bytes = 1;
      wc = *end;
    }
    col = width-columns;
    if (col<1) break;
    if (escout) {
      if ((col = escout(out, col, wc))<0) break;
    } else if (out) fwrite(end, 1, bytes, out);
  }
  *str = end;

  return columns;
}

static void draw_page()
{
  struct linelist *scr_buf = 0;
  unsigned y = 0;
  int x = 0;

  char *line = 0, *end = 0;
  int utf_l = 0,  bytes = 0;

  //screen coordinates for cursor
  int cy_scr = 0, cx_scr = 0;

  //variables used only for cursor handling
  int aw = 0, iw = 0, clip = 0, margin = 8;

  int scroll = 0, redraw = 0;

  adjust_screen_buffer();
  scr_buf = screen;
  redraw = (TT.vi_mov_flag & 0x30000000)>>28;

  scroll = TT.drawn_row-TT.scr_row;
  if (TT.drawn_row<0 || TT.cur_row<0 || TT.scr_row<0) redraw = 3;
  else if (abs(scroll)>TT.screen_height/2) redraw = 3;

  tty_jump(0, 0);
  if (redraw&2) tty_esc("2J"), tty_esc("H");   //clear screen
  else if (scroll>0) printf("\033[%dL", scroll);  //scroll up
  else if (scroll<0) printf("\033[%dM", -scroll); //scroll down

  //jump until cursor
  for (; y < TT.screen_height; y++ ) {
    if (scr_buf == c_r) break;
    scr_buf = scr_buf->down;
  }
  //draw cursor row
  /////////////////////////////////////////////////////////////
  //for long lines line starts to scroll when cursor hits margin
  line = scr_buf->line->data;
  bytes = TT.cur_col;
  end = line;


  tty_jump(0, y);
  tty_esc("2K");
  //find cursor position
  aw = crunch_nstr(&end, 1024, bytes, 0, "\t", vi_crunch);

  //if we need to render text that is not inserted to buffer yet
  if (TT.vi_mode == 2 && il->len) {
    char* iend = il->data; //input end
    x = 0;
    //find insert end position
    iw = crunch_str(&iend, 1024, 0, "\t", vi_crunch);
    clip = (aw+iw) - TT.screen_width+margin;

    //if clipped area is bigger than text before insert
    if (clip > aw) {
      clip -= aw;
      iend = il->data;

      iw -= crunch_str(&iend, clip, 0, "\t", vi_crunch);
      x = crunch_str(&iend, iw, stdout, "\t", vi_crunch);
    } else {
      iend = il->data;
      end = line;

      //if clipped area is substring from cursor row start
      aw -= crunch_nstr(&end, clip, bytes, 0, "\t", vi_crunch);
      x = crunch_str(&end, aw,  stdout, "\t", vi_crunch);
      x += crunch_str(&iend, iw, stdout, "\t", vi_crunch);
    }
  }
  //when not inserting but still need to keep cursor inside screen
  //margin area
  else if ( aw+margin > TT.screen_width) {
    clip = aw-TT.screen_width+margin;
    end = line;
    aw -= crunch_nstr(&end, clip, bytes, 0, "\t", vi_crunch);
    x = crunch_str(&end, aw,  stdout, "\t", vi_crunch);
  }
  else {
    end = line;
    x = crunch_nstr(&end, aw, bytes, stdout, "\t", vi_crunch);
  }
  cx_scr = x;
  cy_scr = y;
  if (scr_buf->line->len > bytes) {
    x += crunch_str(&end, TT.screen_width-x,  stdout, "\t", vi_crunch);
  }

  if (scr_buf) scr_buf = scr_buf->down;
  // drawing cursor row ends
  ///////////////////////////////////////////////////////////////////

  //start drawing all other rows that needs update
  ///////////////////////////////////////////////////////////////////
  y = 0, scr_buf = screen;

  //if we moved around in long line might need to redraw everything
  if (clip != TT.drawn_col) redraw = 3;

  for (; y < TT.screen_height; y++ ) {
    int draw_line = 0;
    if (scr_buf == c_r) {
      scr_buf = scr_buf->down;
      continue;
    } else if (redraw) draw_line++;
    else if (scroll<0 && TT.screen_height-y-1<-scroll)
      scroll++, draw_line++;
    else if (scroll>0) scroll--, draw_line++;

    tty_jump(0, y);
    if (draw_line) {

      tty_esc("2K");
      if (scr_buf) {
        if (draw_line && scr_buf->line->data && scr_buf->line->len) {
          line = scr_buf->line->data;
          bytes = scr_buf->line->len;

          aw = crunch_nstr(&line, clip, bytes, 0, "\t", vi_crunch);
          crunch_str(&line, TT.screen_width-1, stdout, "\t", vi_crunch);
          if ( *line ) printf("@");

        }
      } else if (draw_line) printf("~");
    }
    if (scr_buf) scr_buf = scr_buf->down;
  }

  TT.drawn_row = TT.scr_row, TT.drawn_col = clip;

  //finished updating visual area

  tty_jump(0, TT.screen_height);
  tty_esc("2K");
  switch (TT.vi_mode) {
    case 0:
    tty_esc("30;44m");
    printf("COMMAND|");
    break;
    case 1:
    tty_esc("30;42m");
    printf("NORMAL|");
    break;
    case 2:
    tty_esc("30;41m");
    printf("INSERT|");
    break;

  }
  //DEBUG
  tty_esc("m");
  utf_l = utf8_len(&c_r->line->data[TT.cur_col]);
  if (utf_l) {
    char t[5] = {0, 0, 0, 0, 0};
    strncpy(t, &c_r->line->data[TT.cur_col], utf_l);
    printf("utf: %d %s", utf_l, t);
  }
  printf("| %d, %d\n", cx_scr, cy_scr); //screen coord

  tty_jump(TT.screen_width-12, TT.screen_height);
  printf("| %d, %d\n", TT.cur_row, TT.cur_col);

  tty_esc("m");
  tty_jump(0, TT.screen_height+1);
  tty_esc("2K");
  if (!TT.vi_mode) {
    tty_esc("1m");
    printf("%s", il->data);
    tty_esc("m");
  } else tty_jump(cx_scr, cy_scr);

  xflush(1);

}

static void check_cursor_bounds()
{
  if (c_r->line->len == 0) {
    TT.cur_col = 0;
    return;
  } else if (c_r->line->len-1 < TT.cur_col) TT.cur_col = c_r->line->len-1;

  if (TT.cur_col && utf8_width(&c_r->line->data[TT.cur_col],
        c_r->line->len-TT.cur_col) <= 0)
    TT.cur_col--, check_cursor_bounds();
}

static void adjust_screen_buffer()
{
  //search cursor and screen
  struct linelist *t = text;
  int c = -1, s = -1, i = 0;
  //searching cursor and screen line numbers
  for (;((c == -1) || (s == -1)) && t != 0; i++, t = t->down) {
    if (t == c_r) c = i;
    if (t == screen) s = i;
  }
  //adjust screen buffer so cursor is on drawing area
  if (c <= s) screen = c_r, s = c; //scroll up
  else {
    //drawing does not have wrapping so no need to check width
    int distance = c-s+1;

    if (distance > (int)TT.screen_height) {
      int adj = distance-TT.screen_height;
      for (;adj; adj--) screen = screen->down, s++; //scroll down

    }
  }
  TT.cur_row = c, TT.scr_row = s;

}

//return 0 if not ASCII nor UTF-8
//this is not fully tested
//naive implementation with branches
//there is better branchless lookup table versions out there
//1 0xxxxxxx
//2 110xxxxx  10xxxxxx
//3 1110xxxx  10xxxxxx  10xxxxxx
//4 11110xxx  10xxxxxx  10xxxxxx  10xxxxxx
static int utf8_len(char *str)
{
  int len = 0;
  int i = 0;
  uint8_t *c = (uint8_t*)str;
  if (!c || !(*c)) return 0;
  if (*c < 0x7F) return 1;
  if ((*c & 0xE0) == 0xc0) len = 2;
  else if ((*c & 0xF0) == 0xE0 ) len = 3;
  else if ((*c & 0xF8) == 0xF0 ) len = 4;
  else return 0;
  c++;
  for (i = len-1; i > 0; i--) {
    if ((*c++ & 0xc0) != 0x80) return 0;
  }
  return len;
}

//get utf8 length and width at same time
static int utf8_lnw(int* width, char* str, int bytes)
{
  wchar_t wc;
  int length = 1;
  *width = 1;
  if (*str == 0x09) {
    *width = TT.tabstop;
    return 1;
  }
  length = mbtowc(&wc, str, bytes);
  switch (length) {
  case -1:
    mbtowc(0,0,4);
  case 0:
    *width = 0;
    length = 0;
    break;
  default:
  *width = wcwidth(wc);
  }
  return length;
}

//try to estimate width of next "glyph" in terminal buffer
//combining chars 0x300-0x36F shall be zero width
static int utf8_width(char *str, int bytes)
{
  wchar_t wc;
  if (*str == 0x09) return TT.tabstop;
  switch (mbtowc(&wc, str, bytes)) {
  case -1:
    mbtowc(0,0,4);
  case 0:
    return -1;
  default:
  return wcwidth(wc);
  }
  return 0;
}

static int utf8_dec(char key, char *utf8_scratch, int *sta_p)
{
  int len = 0;
  char *c = utf8_scratch;
  c[*sta_p] = key;
  if (!(*sta_p))  *c = key;
  if (*c < 0x7F) { *sta_p = 1; return 1; }
  if ((*c & 0xE0) == 0xc0) len = 2;
  else if ((*c & 0xF0) == 0xE0 ) len = 3;
  else if ((*c & 0xF8) == 0xF0 ) len = 4;
  else {*sta_p = 0; return 0; }

  (*sta_p)++;

  if (*sta_p == 1) return 0;
  if ((c[*sta_p-1] & 0xc0) != 0x80) {*sta_p = 0; return 0; }

  if (*sta_p == len) { c[(*sta_p)] = 0; return 1; }

  return 0;
}

static char* utf8_last(char* str, int size)
{
  char* end = str+size;
  int pos = size;
  int len = 0;
  int width = 0;
  while (pos >= 0) {
    len = utf8_lnw(&width, end, size-pos);
    if (len && width) return end;
    end--; pos--;
  }
  return 0;
}

static int cur_left(int count0, int count1, char* unused)
{
  int count = count0*count1;
  TT.vi_mov_flag |= 0x80000000;
  for (;count--;) {
    if (!TT.cur_col) return 1;

    TT.cur_col--;
    check_cursor_bounds();
  }
  return 1;
}

static int cur_right(int count0, int count1, char* unused)
{
  int count = count0*count1;
  for (;count--;) {
    if (c_r->line->len <= 1) return 1;
    if (TT.cur_col >= c_r->line->len-1) {
      TT.cur_col = utf8_last(c_r->line->data, c_r->line->len)
        - c_r->line->data;
      return 1;
    }
    TT.cur_col++;
    if (utf8_width(&c_r->line->data[TT.cur_col],
          c_r->line->len-TT.cur_col) <= 0)
      cur_right(1, 1, 0);
  }
  return 1;
}

static int cur_up(int count0, int count1, char* unused)
{
  int count = count0*count1;
  for (;count-- && c_r->up;)
    c_r = c_r->up;

  TT.vi_mov_flag |= 0x80000000;
  check_cursor_bounds();
  return 1;
}

static int cur_down(int count0, int count1, char* unused)
{
  int count = count0*count1;
  for (;count-- && c_r->down;)
    c_r = c_r->down;

  check_cursor_bounds();
  return 1;
}