#include "cpp-terminal/color.hpp"
#include "cpp-terminal/exception.hpp"
#include "cpp-terminal/input.hpp"
#include "cpp-terminal/key.hpp"
#include "cpp-terminal/options.hpp"
#include "cpp-terminal/screen.hpp"
#include "cpp-terminal/style.hpp"
#include "cpp-terminal/terminal.hpp"
#include "cpp-terminal/tty.hpp"

#include <cstdarg>
#include <cstring>
#include <fstream>
#include <iostream>

const std::string KILO_VERSION{"0.0.1"};
const int         KILO_TAB_STOP{8};
const int         KILO_QUIT_TIMES{3};

enum editorHighlight
{
  HL_NORMAL = 0,
  HL_COMMENT,
  HL_MLCOMMENT,
  HL_KEYWORD1,
  HL_KEYWORD2,
  HL_STRING,
  HL_NUMBER,
  HL_MATCH
};

#define HL_HIGHLIGHT_NUMBERS (1 << 0)
#define HL_HIGHLIGHT_STRINGS (1 << 1)

/*** data ***/

struct editorSyntax
{
  const char*  filetype;
  const char** filematch;
  const char** keywords;
  const char*  singleline_comment_start;
  const char*  multiline_comment_start;
  const char*  multiline_comment_end;
  int          flags;
};

typedef struct erow
{
  int            idx;
  int            size;
  int            rsize;
  char*          chars;
  char*          render;
  unsigned char* hl;
  int            hl_open_comment;
} erow;

struct editorConfig
{
  int                  cx, cy;
  int                  rx;
  int                  rowoff;
  int                  coloff;
  int                  screenrows;
  int                  screencols;
  int                  numrows;
  erow*                row;
  int                  dirty;
  char*                filename;
  char                 statusmsg[80];
  time_t               statusmsg_time;
  struct editorSyntax* syntax;
};

struct editorConfig E;

/*** filetypes ***/

const char* C_HL_extensions[] = {".c", ".h", ".cpp", nullptr};
const char* C_HL_keywords[]   = {"switch", "if", "while", "for", "break", "continue", "return", "else", "struct", "union", "typedef", "static", "enum", "class", "case", "int|", "long|", "double|", "float|", "char|", "unsigned|", "signed|", "void|", nullptr};

struct editorSyntax HLDB[] = {
  {"c", C_HL_extensions, C_HL_keywords, "//", "/*", "*/", HL_HIGHLIGHT_NUMBERS | HL_HIGHLIGHT_STRINGS},
};

#define HLDB_ENTRIES (sizeof(HLDB) / sizeof(HLDB[0]))

/*** prototypes ***/

char* editorPrompt(const char* prompt, void (*callback)(char*, int));

/*** syntax highlighting ***/

int is_separator(int c) { return isspace(c) || c == '\0' || strchr(",.()+-/*=~%<>[];", c) != nullptr; }

void editorUpdateSyntax(erow* row)
{
  row->hl = (unsigned char*)realloc(row->hl, row->rsize);
  memset(row->hl, HL_NORMAL, row->rsize);

  if(E.syntax == nullptr) return;

  const char** keywords = E.syntax->keywords;

  const char* scs = E.syntax->singleline_comment_start;
  const char* mcs = E.syntax->multiline_comment_start;
  const char* mce = E.syntax->multiline_comment_end;

  int scs_len = scs ? strlen(scs) : 0;
  int mcs_len = mcs ? strlen(mcs) : 0;
  int mce_len = mce ? strlen(mce) : 0;

  int prev_sep   = 1;
  int in_string  = 0;
  int in_comment = (row->idx > 0 && E.row[row->idx - 1].hl_open_comment);

  int i = 0;
  while(i < row->rsize)
  {
    char          c       = row->render[i];
    unsigned char prev_hl = (i > 0) ? row->hl[i - 1] : (char)HL_NORMAL;

    if(scs_len && !in_string && !in_comment)
    {
      if(!strncmp(&row->render[i], scs, scs_len))
      {
        memset(&row->hl[i], HL_COMMENT, row->rsize - i);
        break;
      }
    }

    if(mcs_len && mce_len && !in_string)
    {
      if(in_comment)
      {
        row->hl[i] = HL_MLCOMMENT;
        if(!strncmp(&row->render[i], mce, mce_len))
        {
          memset(&row->hl[i], HL_MLCOMMENT, mce_len);
          i += mce_len;
          in_comment = 0;
          prev_sep   = 1;
          continue;
        }
        else
        {
          i++;
          continue;
        }
      }
      else if(!strncmp(&row->render[i], mcs, mcs_len))
      {
        memset(&row->hl[i], HL_MLCOMMENT, mcs_len);
        i += mcs_len;
        in_comment = 1;
        continue;
      }
    }

    if(E.syntax->flags & HL_HIGHLIGHT_STRINGS)
    {
      if(in_string)
      {
        row->hl[i] = HL_STRING;
        if(c == '\\' && i + 1 < row->rsize)
        {
          row->hl[i + 1] = HL_STRING;
          i += 2;
          continue;
        }
        if(c == in_string) in_string = 0;
        i++;
        prev_sep = 1;
        continue;
      }
      else
      {
        if(c == '"' || c == '\'')
        {
          in_string  = c;
          row->hl[i] = HL_STRING;
          i++;
          continue;
        }
      }
    }

    if(E.syntax->flags & HL_HIGHLIGHT_NUMBERS)
    {
      if((isdigit(c) && (prev_sep || prev_hl == HL_NUMBER)) || (c == '.' && prev_hl == HL_NUMBER))
      {
        row->hl[i] = HL_NUMBER;
        i++;
        prev_sep = 0;
        continue;
      }
    }

    if(prev_sep)
    {
      int j;
      for(j = 0; keywords[j]; j++)
      {
        int klen = strlen(keywords[j]);
        int kw2  = keywords[j][klen - 1] == '|';
        if(kw2) klen--;

        if(!strncmp(&row->render[i], keywords[j], klen) && is_separator(row->render[i + klen]))
        {
          memset(&row->hl[i], kw2 ? HL_KEYWORD2 : HL_KEYWORD1, klen);
          i += klen;
          break;
        }
      }
      if(keywords[j] != nullptr)
      {
        prev_sep = 0;
        continue;
      }
    }

    prev_sep = is_separator(c);
    i++;
  }

  int changed          = (row->hl_open_comment != in_comment);
  row->hl_open_comment = in_comment;
  if(changed && row->idx + 1 < E.numrows) editorUpdateSyntax(&E.row[row->idx + 1]);
}

Term::Color editorSyntaxToColor(int hl)
{
  switch(hl)
  {
    case HL_COMMENT:
    case HL_MLCOMMENT: return Term::Color::Name::Cyan;
    case HL_KEYWORD1: return Term::Color::Name::Yellow;
    case HL_KEYWORD2: return Term::Color::Name::Green;
    case HL_STRING: return Term::Color::Name::Magenta;
    case HL_NUMBER: return Term::Color::Name::Red;
    case HL_MATCH: return Term::Color::Name::Blue;
    default: return Term::Color::Name::Gray;
  }
}

void editorSelectSyntaxHighlight()
{
  E.syntax = nullptr;
  if(E.filename == nullptr) return;

  char* ext = strrchr(E.filename, '.');

  for(std::size_t j = 0; j != HLDB_ENTRIES; ++j)
  {
    struct editorSyntax* s = &HLDB[j];
    unsigned int         i = 0;
    while(s->filematch[i])
    {
      int is_ext = (s->filematch[i][0] == '.');
      if((is_ext && ext && !strcmp(ext, s->filematch[i])) || (!is_ext && strstr(E.filename, s->filematch[i])))
      {
        E.syntax = s;

        int filerow;
        for(filerow = 0; filerow < E.numrows; filerow++) { editorUpdateSyntax(&E.row[filerow]); }

        return;
      }
      i++;
    }
  }
}

/*** row operations ***/

int editorRowCxToRx(erow* row, int cx)
{
  int rx = 0;
  for(int j = 0; j < cx; j++)
  {
    if(row->chars[j] == '\t') rx += (KILO_TAB_STOP - 1) - (rx % KILO_TAB_STOP);
    rx++;
  }
  return rx;
}

int editorRowRxToCx(erow* row, int rx)
{
  int cur_rx = 0;
  int cx{};
  for(cx = 0; cx < row->size; cx++)
  {
    if(row->chars[cx] == '\t') cur_rx += (KILO_TAB_STOP - 1) - (cur_rx % KILO_TAB_STOP);
    cur_rx++;

    if(cur_rx > rx) return cx;
  }
  return cx;
}

void editorUpdateRow(erow* row)
{
  int tabs = 0;
  for(int j = 0; j < row->size; j++)
    if(row->chars[j] == '\t') tabs++;

  free(row->render);
  row->render = (char*)malloc(row->size + tabs * (KILO_TAB_STOP - 1) + 1);

  int idx = 0;
  for(int j = 0; j < row->size; j++)
  {
    if(row->chars[j] == '\t')
    {
      row->render[idx++] = ' ';
      while(idx % KILO_TAB_STOP != 0) row->render[idx++] = ' ';
    }
    else { row->render[idx++] = row->chars[j]; }
  }
  row->render[idx] = '\0';
  row->rsize       = idx;

  editorUpdateSyntax(row);
}

void editorInsertRow(int at, const char* s, std::size_t len)
{
  if(at < 0 || at > E.numrows) return;

  E.row = (erow*)realloc(E.row, sizeof(erow) * (E.numrows + 1));
  memmove(&E.row[at + 1], &E.row[at], sizeof(erow) * (E.numrows - at));
  for(int j = at + 1; j <= E.numrows; j++) E.row[j].idx++;

  E.row[at].idx = at;

  E.row[at].size  = len;
  E.row[at].chars = (char*)malloc(len + 1);
  memcpy(E.row[at].chars, s, len);
  E.row[at].chars[len] = '\0';

  E.row[at].rsize           = 0;
  E.row[at].render          = nullptr;
  E.row[at].hl              = nullptr;
  E.row[at].hl_open_comment = 0;
  editorUpdateRow(&E.row[at]);

  E.numrows++;
  E.dirty++;
}

void editorFreeRow(erow* row)
{
  free(row->render);
  free(row->chars);
  free(row->hl);
}

void editorDelRow(int at)
{
  if(at < 0 || at >= E.numrows) return;
  editorFreeRow(&E.row[at]);
  memmove(&E.row[at], &E.row[at + 1], sizeof(erow) * (E.numrows - at - 1));
  for(int j = at; j < E.numrows - 1; j++) E.row[j].idx--;
  E.numrows--;
  E.dirty++;
}

void editorRowInsertChar(erow* row, int at, int c)
{
  if(at < 0 || at > row->size) at = row->size;
  row->chars = (char*)realloc(row->chars, row->size + 2);
  memmove(&row->chars[at + 1], &row->chars[at], row->size - at + 1);
  row->size++;
  row->chars[at] = c;
  editorUpdateRow(row);
  E.dirty++;
}

void editorRowAppendString(erow* row, char* s, std::size_t len)
{
  row->chars = (char*)realloc(row->chars, row->size + len + 1);
  memcpy(&row->chars[row->size], s, len);
  row->size += len;
  row->chars[row->size] = '\0';
  editorUpdateRow(row);
  E.dirty++;
}

void editorRowDelChar(erow* row, int at)
{
  if(at < 0 || at >= row->size) return;
  memmove(&row->chars[at], &row->chars[at + 1], row->size - at);
  row->size--;
  editorUpdateRow(row);
  E.dirty++;
}

/*** editor operations ***/

void editorInsertChar(int c)
{
  if(E.cy == E.numrows) { editorInsertRow(E.numrows, "", 0); }
  editorRowInsertChar(&E.row[E.cy], E.cx, c);
  E.cx++;
}

void editorInsertNewline()
{
  if(E.cx == 0) { editorInsertRow(E.cy, "", 0); }
  else
  {
    erow* row = &E.row[E.cy];
    editorInsertRow(E.cy + 1, &row->chars[E.cx], row->size - E.cx);
    row                   = &E.row[E.cy];
    row->size             = E.cx;
    row->chars[row->size] = '\0';
    editorUpdateRow(row);
  }
  E.cy++;
  E.cx = 0;
}

void editorDelChar()
{
  if(E.cy == E.numrows) return;
  if(E.cx == 0 && E.cy == 0) return;

  erow* row = &E.row[E.cy];
  if(E.cx > 0)
  {
    editorRowDelChar(row, E.cx - 1);
    E.cx--;
  }
  else
  {
    E.cx = E.row[E.cy - 1].size;
    editorRowAppendString(&E.row[E.cy - 1], row->chars, row->size);
    editorDelRow(E.cy);
    E.cy--;
  }
}

/*** file i/o ***/

char* editorRowsToString(int* buflen)
{
  int totlen = 0;
  for(int j = 0; j < E.numrows; j++) totlen += E.row[j].size + 1;
  *buflen = totlen;

  char* buf = (char*)malloc(totlen);
  char* p   = buf;
  for(int j = 0; j < E.numrows; j++)
  {
    memcpy(p, E.row[j].chars, E.row[j].size);
    p += E.row[j].size;
    *p = '\n';
    p++;
  }

  return buf;
}

void editorOpen(char* filename)
{
  free(E.filename);
#ifdef _WIN32
  E.filename = _strdup(filename);
#else
  E.filename = strdup(filename);
#endif
  editorSelectSyntaxHighlight();

  std::ifstream f(filename);
  if(f.fail()) throw std::runtime_error("File failed to open.");
  std::string line;
  std::getline(f, line);
  while(f.rdstate() == std::ios_base::goodbit)
  {
    int linelen = line.size();
    while(linelen > 0 && (line[linelen - 1] == '\n' || line[linelen - 1] == '\r')) linelen--;
    editorInsertRow(E.numrows, line.c_str(), linelen);
    std::getline(f, line);
  }
  E.dirty = 0;
}

void editorSetStatusMessage(const char* fmt, ...)
{
  va_list ap;
  va_start(ap, fmt);
  vsnprintf(E.statusmsg, sizeof(E.statusmsg), fmt, ap);
  va_end(ap);
  E.statusmsg_time = time(nullptr);
}

void editorSave()
{
  if(E.filename == nullptr)
  {
    E.filename = editorPrompt("Save as: %s (ESC to cancel)", nullptr);
    if(E.filename == nullptr)
    {
      editorSetStatusMessage("Save aborted");
      return;
    }
    editorSelectSyntaxHighlight();
  }

  int         len;
  char*       buf = editorRowsToString(&len);
  std::string s   = std::string(buf, len);
  free(buf);

  std::ofstream out;
  out.open(E.filename);
  out << s;
  out.close();
  E.dirty = 0;
  editorSetStatusMessage("%d bytes written to disk", len);
}

/*** find ***/

void editorFindCallback(char* query, int key)
{
  static int last_match = -1;
  static int direction  = 1;

  static int   saved_hl_line;
  static char* saved_hl = nullptr;

  if(saved_hl)
  {
    memcpy(E.row[saved_hl_line].hl, saved_hl, E.row[saved_hl_line].rsize);
    free(saved_hl);
    saved_hl = nullptr;
  }

  if(key == Term::Key::ENTER || key == Term::Key::ESC)
  {
    last_match = -1;
    direction  = 1;
    return;
  }
  else if(key == Term::Key::ARROW_RIGHT || key == Term::Key::ARROW_DOWN) { direction = 1; }
  else if(key == Term::Key::ARROW_LEFT || key == Term::Key::ARROW_UP) { direction = -1; }
  else
  {
    last_match = -1;
    direction  = 1;
  }

  if(last_match == -1) direction = 1;
  int current = last_match;
  for(int i = 0; i < E.numrows; i++)
  {
    current += direction;
    if(current == -1) current = E.numrows - 1;
    else if(current == E.numrows)
      current = 0;

    erow* row   = &E.row[current];
    char* match = strstr(row->render, query);
    if(match)
    {
      last_match = current;
      E.cy       = current;
      E.cx       = editorRowRxToCx(row, match - row->render);
      E.rowoff   = E.numrows;

      saved_hl_line = current;
      saved_hl      = (char*)malloc(row->rsize);
      memcpy(saved_hl, row->hl, row->rsize);
      memset(&row->hl[match - row->render], HL_MATCH, strlen(query));
      break;
    }
  }
}

void editorFind()
{
  int saved_cx     = E.cx;
  int saved_cy     = E.cy;
  int saved_coloff = E.coloff;
  int saved_rowoff = E.rowoff;

  char* query = editorPrompt("Search: %s (Use ESC/Arrows/Enter)", editorFindCallback);

  if(query) { free(query); }
  else
  {
    E.cx     = saved_cx;
    E.cy     = saved_cy;
    E.coloff = saved_coloff;
    E.rowoff = saved_rowoff;
  }
}

/*** output ***/

void editorScroll()
{
  E.rx = 0;
  if(E.cy < E.numrows) { E.rx = editorRowCxToRx(&E.row[E.cy], E.cx); }

  if(E.cy < E.rowoff) { E.rowoff = E.cy; }
  if(E.cy >= E.rowoff + E.screenrows) { E.rowoff = E.cy - E.screenrows + 1; }
  if(E.rx < E.coloff) { E.coloff = E.rx; }
  if(E.rx >= E.coloff + E.screencols) { E.coloff = E.rx - E.screencols + 1; }
}

void editorDrawRows(std::string& ab)
{
  for(int y = 0; y < E.screenrows; y++)
  {
    int filerow = y + E.rowoff;
    if(filerow >= E.numrows)
    {
      if(E.numrows == 0 && y == E.screenrows / 3)
      {
        char welcome[80];
        int  welcomelen = snprintf(welcome, sizeof(welcome), "Kilo editor -- version %s", KILO_VERSION.c_str());
        if(welcomelen > E.screencols) welcomelen = E.screencols;
        int padding = (E.screencols - welcomelen) / 2;
        if(padding)
        {
          ab.append("~");
          padding--;
        }
        while(padding--) ab.append(" ");
        ab.append(welcome);
      }
      else { ab.append("~"); }
    }
    else
    {
      int len = E.row[filerow].rsize - E.coloff;
      if(len < 0) len = 0;
      if(len > E.screencols) len = E.screencols;
      char*          c             = &E.row[filerow].render[E.coloff];
      unsigned char* hl            = &E.row[filerow].hl[E.coloff];
      Term::Color    current_color = Term::Color::Name::Black;  // black is not used in editorSyntaxToColor
      int            j;
      for(j = 0; j < len; j++)
      {
        if(iscntrl(c[j]))
        {
          char sym = (c[j] <= 26) ? '@' + c[j] : '?';
          ab.append(style(Term::Style::REVERSED));
          ab.append(std::string(&sym, 1));
          ab.append(style(Term::Style::RESET));
          if(current_color != Term::Color::Name::Default) { ab.append(color_fg(current_color)); }
        }
        else if(hl[j] == HL_NORMAL)
        {
          if(current_color != Term::Color::Name::Black)
          {
            ab.append(color_fg(Term::Color::Name::Default));
            current_color = Term::Color::Name::Black;
          }
          ab.append(std::string(&c[j], 1));
        }
        else
        {
          Term::Color color = editorSyntaxToColor(hl[j]);
          if(color != current_color)
          {
            current_color = color;
            ab.append(Term::color_fg(color));
          }
          ab.append(std::string(&c[j], 1));
        }
      }
      ab.append(color_fg(Term::Color::Name::Default));
    }

    ab.append(Term::clear_eol());
    ab.append("\r\n");
  }
}

void editorDrawStatusBar(std::string& ab)
{
  ab.append(style(Term::Style::REVERSED));
  char status[80], rstatus[80];
  int  len  = snprintf(status, sizeof(status), "%.20s - %d lines %s", E.filename ? E.filename : "[No Name]", E.numrows, E.dirty ? "(modified)" : "");
  int  rlen = snprintf(rstatus, sizeof(rstatus), "%s | %d/%d", E.syntax ? E.syntax->filetype : "no ft", E.cy + 1, E.numrows);
  if(len > E.screencols) len = E.screencols;
  ab.append(std::string(status, len));
  while(len < E.screencols)
  {
    if(E.screencols - len == rlen)
    {
      ab.append(std::string(rstatus, rlen));
      break;
    }
    else
    {
      ab.append(" ");
      len++;
    }
  }
  ab.append(style(Term::Style::RESET));
  ab.append("\r\n");
}

void editorDrawMessageBar(std::string& ab)
{
  ab.append(Term::clear_eol());
  int msglen = strlen(E.statusmsg);
  if(msglen > E.screencols) msglen = E.screencols;
  if(msglen && time(nullptr) - E.statusmsg_time < 5) ab.append(std::string(E.statusmsg, msglen));
}

void editorRefreshScreen()
{
  editorScroll();

  std::string ab;
  ab.reserve(16 * 1024);

  ab.append(Term::cursor_off());
  ab.append(Term::cursor_move(1, 1));

  editorDrawRows(ab);
  editorDrawStatusBar(ab);
  editorDrawMessageBar(ab);

  ab.append(Term::cursor_move((E.cy - E.rowoff) + 1, (E.rx - E.coloff) + 1));

  ab.append(Term::cursor_on());

  std::cout << ab << std::flush;
}

/*** input ***/

char* editorPrompt(const char* prompt, void (*callback)(char*, int))
{
  std::size_t bufsize = 128;
  char*       buf     = (char*)malloc(bufsize);

  std::size_t buflen = 0;
  buf[0]             = '\0';

  while(true)
  {
    editorSetStatusMessage(prompt, buf);
    editorRefreshScreen();

    Term::Key c = Term::read_event();

    if(c) continue;
    if(c == Term::Key::DEL || c == Term::Key::CTRL_H || c == Term::Key::BACKSPACE)
    {
      if(buflen != 0) buf[--buflen] = '\0';
    }
    else if(c == Term::Key::ESC)
    {
      editorSetStatusMessage("");
      if(callback) callback(buf, c);
      free(buf);
      return nullptr;
    }
    else if(c == Term::Key::ENTER)
    {
      if(buflen != 0)
      {
        editorSetStatusMessage("");
        if(callback) callback(buf, c);
        return buf;
      }
    }
    else if(!iscntrl(c) && c < 128)
    {
      if(buflen == bufsize - 1)
      {
        bufsize *= 2;
        buf = (char*)realloc(buf, bufsize);
      }
      buf[buflen++] = c;
      buf[buflen]   = '\0';
    }

    if(callback) callback(buf, c);
  }
}

void editorMoveCursor(int key)
{
  erow* row = (E.cy >= E.numrows) ? nullptr : &E.row[E.cy];

  switch(key)
  {
    case Term::Key::ARROW_LEFT:
      if(E.cx != 0) { E.cx--; }
      else if(E.cy > 0)
      {
        E.cy--;
        E.cx = E.row[E.cy].size;
      }
      break;
    case Term::Key::ARROW_RIGHT:
      if(row && E.cx < row->size) { E.cx++; }
      else if(row && E.cx == row->size)
      {
        E.cy++;
        E.cx = 0;
      }
      break;
    case Term::Key::ARROW_UP:
      if(E.cy != 0) { E.cy--; }
      break;
    case Term::Key::ARROW_DOWN:
      if(E.cy < E.numrows) { E.cy++; }
      break;
  }

  row        = (E.cy >= E.numrows) ? nullptr : &E.row[E.cy];
  int rowlen = row ? row->size : 0;
  if(E.cx > rowlen) { E.cx = rowlen; }
}

bool editorProcessKeypress()
{
  static int quit_times = KILO_QUIT_TIMES;

  Term::Key c;
  while((c = Term::read_event()).empty()) continue;  //NEEDED for windows (FIXME ?)
  switch(c)
  {
    case Term::Key::ENTER: editorInsertNewline(); break;

    case Term::Key::CTRL_Q:
      if(E.dirty && quit_times > 0)
      {
        editorSetStatusMessage("WARNING!!! File has unsaved changes. Press Ctrl-Q %d more times to quit.", quit_times);
        quit_times--;
        return true;
      }
      return false;
      break;

    case Term::Key::CTRL_S: editorSave(); break;

    case Term::Key::HOME: E.cx = 0; break;

    case Term::Key::END:
      if(E.cy < E.numrows) E.cx = E.row[E.cy].size;
      break;

    case Term::Key::CTRL_F: editorFind(); break;

    case Term::Key::BACKSPACE:
    case Term::Key::DEL:
      if(c == Term::Key::DEL) editorMoveCursor(Term::Key::ARROW_RIGHT);
      editorDelChar();
      break;

    case Term::Key::PAGE_UP:
    case Term::Key::PAGE_DOWN:
    {
      if(c == Term::Key::PAGE_UP) { E.cy = E.rowoff; }
      else if(c == Term::Key::PAGE_DOWN)
      {
        E.cy = E.rowoff + E.screenrows - 1;
        if(E.cy > E.numrows) E.cy = E.numrows;
      }

      int times = E.screenrows;
      while(times--) editorMoveCursor(c == Term::Key::PAGE_UP ? Term::Key::ARROW_UP : Term::Key::ARROW_DOWN);
    }
    break;

    case Term::Key::ARROW_UP:
    case Term::Key::ARROW_DOWN:
    case Term::Key::ARROW_LEFT:
    case Term::Key::ARROW_RIGHT: editorMoveCursor(c); break;

    case Term::Key::CTRL_L:
    case Term::Key::ESC: break;

    case Term::Key::TAB: editorInsertChar('\t'); break;

    default: editorInsertChar(c); break;
  }

  quit_times = KILO_QUIT_TIMES;
  return true;
}

/*** init ***/

void initEditor()
{
  E.cx                = 0;
  E.cy                = 0;
  E.rx                = 0;
  E.rowoff            = 0;
  E.coloff            = 0;
  E.numrows           = 0;
  E.row               = nullptr;
  E.dirty             = 0;
  E.filename          = nullptr;
  E.statusmsg[0]      = '\0';
  E.statusmsg_time    = 0;
  E.syntax            = nullptr;
  Term::Screen screen = Term::screen_size();
  E.screenrows        = screen.rows();
  E.screencols        = screen.columns();
  E.screenrows -= 2;
}

int main(int argc, char* argv[])
{
  // We must put all code in try/catch block, otherwise destructors are not
  // being called when exception happens and the terminal is not put into
  // correct state.
  try
  {
    // check if the terminal is capable of handling input
    if(!Term::is_stdin_a_tty())
    {
      std::cout << "The terminal is not attached to a TTY and therefore can't catch user input. Exiting...\n";
      return 1;
    }
    Term::terminal.setOptions(Term::Option::ClearScreen, Term::Option::NoSignalKeys, Term::Option::NoCursor, Term::Option::Raw);
    initEditor();
    if(argc >= 2) { editorOpen(argv[1]); }

    editorSetStatusMessage("HELP: Ctrl-S = save | Ctrl-Q = quit | Ctrl-F = find");

    editorRefreshScreen();
    while(editorProcessKeypress()) { editorRefreshScreen(); }
  }
  catch(const Term::Exception& re)
  {
    std::cerr << "cpp-terminal error: " << re.what() << std::endl;
    return 2;
  }
  catch(...)
  {
    std::cerr << "Unknown error." << std::endl;
    return 1;
  }
  return 0;
}