/**
 * @file src/editline.c
 * @brief Define `editline()`
 */

#include "editline.h"
#include "error.h"
#include "exproriented.h"
#include "mem.h"
#include "term.h"
#include "testing.h"
#include <ctype.h>

#define getchar  (char)getchar
#define is_space (bool)isspace
#define is_alnum (bool)isalnum

mut_cursor_t movecur(int n, mut_cursor_t cur) {
  size_t len = cur.base->len;
  size_t pos = cur.pos;
  cur.pos = n > (int)(len - pos) ? len : -n > (int)pos ? 0 : pos + (size_t)n;
  return cur;
}

test (movecur) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  mut_cursor_t ms = FROM_SLICE(s);

  ms = movecur(1, ms);
  expecteq(1, ms.pos);
  ms = movecur(3, ms);
  expecteq(4, ms.pos);
  ms = movecur(-4, ms);
  expecteq(0, ms.pos);
  ms = movecur(-1, ms);
  expecteq(0, ms.pos);
  ms = movecur(999, ms);
  expecteq(ms.base->len, ms.pos);
}

mut_cursor_t findmove(char ch, int dir, mut_cursor_t c) {
  size_t new
    = (size_t)((dir > 0) ? memchr(c.base->s + c.pos, ch, c.base->len - c.pos)
                         : memrchr(c.base->s, ch, c.pos))
      ?: $return(c);
  return (mut_cursor_t){
    .base = c.base,
    .pos = new - (size_t)c.base->s,
  };
}

test (findmove) {
  mut_slice_t s = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);

#define CASE(ch, d) \
  c = findmove(ch, d, c); \
  expecteq(ch, c.base->s[c.pos]);

  MAP_PAIR(CASE, (' ', 1), ('e', 1), ('s', -1), ('.', 1))

  size_t prev = c.pos;
  c = findmove('z', -1, c);
  expecteq(prev, c.pos);
}

mut_cursor_t fw(mut_cursor_t c) {
  char const *s = c.base->s + c.pos;
  if (is_space(*s)) return movecur((int)skipByte(s, ' '), c);
  for (bool was_alnum = is_alnum(*s);
       !is_space(*s) && s < c.base->s + c.base->len
       && (is_alnum(*s) == was_alnum);
       s++);
  s += skipByte(s, ' ');
  return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}

test (fw) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);

  c = fw(c);
  expecteq(7, c.pos);
  c = fw(c);
  expecteq(11, c.pos);
}

mut_cursor_t bw(mut_cursor_t c) {
  char const *s = c.base->s + c.pos - 1;
  for (; is_space(*s); s--);
  for (bool was_alnum = is_alnum(*s);
       !is_space(s[-1]) && c.base->s < s && (is_alnum(s[-1]) == was_alnum);
       s--);
  return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}

test (bw) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);
  c.pos = s.len;

  c = bw(c);
  expecteq(11, c.pos);
  c = bw(c);
  expecteq(7, c.pos);
}

mut_cursor_t fW(mut_cursor_t c) {
  char const *s = memchr(c.base->s + c.pos, ' ', c.base->len - c.pos)
                  ?: c.base->s + c.base->len;
  s += skipByte(s, ' ');
  return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s)};
}

test (fW) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);

  c = fW(c);
  expecteq(7, c.pos);
  c = fW(c);
  expecteq(12, c.pos);
}

mut_cursor_t bW(mut_cursor_t c) {
  char const *s = c.base->s + c.pos - 1;
  for (; is_space(*s) && c.base->s < s; s--);
  s = memrchr(c.base->s, ' ', (size_t)(s - c.base->s)) ?: c.base->s - 1;
  return (mut_cursor_t){.base = c.base, .pos = (size_t)(s - c.base->s + 1)};
}

test (bW) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);
  c.pos = s.len;

  c = bW(c);
  expecteq(7, c.pos);
  c = bW(c);
  expecteq(0, c.pos);
}

void deletes(mut_slice_t *ms, range_t range) {
  memmove(ms->s + range.begin, ms->s + range.end, ms->len - range.end);
  ms->len -= range.end - range.begin;
}

test (deletes) {
  mut_slice_t s dropslice = mutFromString("sample text.");
  deletes(&s, (range_t){0, 7});
  expecteq(mutFromString("text."), s);
}

/**
 * @brief Handle escape sequence
 * @param[in] key Escape sequence
 * @param[in,out] c Cursor
 */
static void handleEs(char key, mut_cursor_t *c) {
  switch (key) {
  case '3': // delete key
    if (getchar() != '~') break;
    if (c->pos == c->base->len) break;
    char *s = c->base->s + c->pos;
    memmove(s, s + 1, c->base->len - c->pos - 1);
    c->base->len--;
    break;
  case 'C': // right arrow
    *c = movecur(1, *c);
    break;
  case 'D': // left arrow
    *c = movecur(-1, *c);
    break;
  case 'F': // END
    c->pos = c->base->len;
    break;
  case 'H': // HOME
    c->pos = 0;
    break;
  default:
    DISPERR("unknown escape sequence: ", key);
  }
}

static range_t handleTxtObj(char txtobj, mut_cursor_t c) {
  switch (txtobj) {
  case 'w':
    return (range_t){.begin = fw(c).pos, .end = bw(c).pos};
  case 'W':
    return (range_t){.begin = fW(c).pos, .end = bW(c).pos};
  case 'b':
    return (range_t){
      .begin = findmove(')', 1, c).pos,
      .end = findmove('(', -1, c).pos,
    };
  case ']':
    return (range_t){
      .begin = findmove(']', 1, c).pos,
      .end = findmove('[', -1, c).pos,
    };
  default:
    return (range_t){};
  }
}

void inserts(slice_t s, mut_cursor_t *c) {
  size_t len = s.len + c->base->len;
  size_t cap = stdc_bit_ceil(len);
  if (c->base->cap < len) {
    c->base->s = realloc(c->base->s, cap);
    c->base->cap = cap;
  }
  char *str = c->base->s + c->pos;
  memmove(str + s.len, str, c->base->len - c->pos);
  memcpy(str, s.s, s.len);
  c->pos += s.len;
  c->base->len += s.len;
}

test (inserts) {
  auto s = mutFromString("sample text.");
  mut_cursor_t c = FROM_SLICE(s);
  c.pos = s.len - 1;

  inserts(fromString("new string"), &c);
  expecteq(mutFromString("sample textnew string."), *c.base);
  c.pos = 11;
  inserts(fromString("extra string"), &c);
  expecteq(mutFromString("sample textextra stringnew string."), *c.base);
}

/**
 * @brief Behavior in insert mode
 * @param[in] ch Typed character
 * @param[in,out] c Cursor
 */
static void insbind(char ch, mut_cursor_t *c) {
  switch (ch) {
  case '(': // autopair
    inserts(fromString("()"), c);
    c->pos--;
    break;

  case ')': {
    char const *pos = strchr(c->base->s + c->pos, ')') ?: p$(goto dflt);
    c->pos = (size_t)(pos - c->base->s + 1);
  } break;

  case '[': // autopair
    inserts(fromString("[]"), c);
    c->pos--;
    break;

  case ']': {
    char *pos = strchr(c->base->s + c->pos, ']') ?: p$(goto dflt);
    c->pos = (size_t)(pos - c->base->s + 1);
  } break;

dflt:
  default:
    inserts((slice_t){.s = &ch, .len = 1}, c);
  }
}

auto handle_printable = insbind;

/**
 * @brief Behavior in normal mode
 * @param[in] ch Typed character
 * @param[in,out] c Cursor
 * @see `$ vim -c ":h *{char}*<CR>"`
 */
static void nrmbind(char ch, mut_cursor_t *c) {
  switch (ch) {
  case 'h':
    *c = movecur(-1, *c);
    break;
  case 'l':
    *c = movecur(1, *c);
    break;
  case 'w':
    *c = fw(*c);
    break;
  case 'b':
    *c = bw(*c);
    break;
  case 'W':
    *c = fW(*c);
    break;
  case 'B':
    *c = bW(*c);
    break;
  case 'f':
    *c = findmove(getchar(), 1, *c);
    break;
  case 'F':
    *c = findmove(getchar(), -1, *c);
    break;
  case 'A':
    c->pos = c->base->len;
    [[fallthrough]];
  case 'a':
    c->pos += c->pos != c->base->len;
    handle_printable = insbind;
    break;
  case 'I':
    c->pos = 0;
    [[fallthrough]];
  case 'i':
    handle_printable = insbind;
    break;
  case 'c':
    handle_printable = insbind;
    [[fallthrough]];
  case 'd': {
    size_t orig_pos = c->pos;
    char input = getchar();
    range_t range
      = $if(input == 'i' || input == 'a')({ handleTxtObj(getchar(), *c); })
        $else({
          nrmbind(input, c);
          (range_t){
            .begin = less(c->pos, orig_pos),
            .end = more(c->pos, orig_pos),
          };
        });

    if (range.begin == range.end) break;
    deletes(c->base, range);
    c->pos = range.begin;
  } break;
  case 'C':
    handle_printable = insbind;
    [[fallthrough]];
  case 'D':
    c->base->len = c->pos;
    break;
  case 'r':
    c->base->s[c->pos] = getchar();
    break;
  case '[':
    handleEs(getchar(), c);
    handle_printable = insbind;
    break;
  default:
    DISPERR("unknown char: ", ch);
  }
}

/**
 * @brief Multi modal input function
 * @param[in] sz Buffer size
 * @param[out] buf Buffer
 * @return Is not CTRL-D pressed
 */
bool editline(size_t sz, char *buf) {
  mut_slice_t s = mutFromString("");
  mut_cursor_t c = FROM_SLICE(s);

  struct termios __ ondrop(disableRawMode) = enableRawMode();

  char ch;
  while (ch = getchar(), ch != ctrld && ch != '\n' && c.base->len < sz - 1) {
    switch (ch) {
    case es:
      handle_printable = nrmbind;
      break;

    case backspace:
      if (c.pos == 0) continue;
      memmove(c.base->s + c.pos - 1, c.base->s + c.pos, c.base->len - c.pos);
      c.pos--;
      c.base->len--;
      break;

    default:
      handle_printable(ch, &c);
      break;
    }
    PRINT(ESEL(2) "\r", *c.base, "\033[", (int)c.pos + 1, "G");
  }
  putchar('\n');

  memcpy(buf, c.base->s, c.base->len);
  buf[c.base->len] = '\0';

  free(c.base->s);
  return ch == '\n';
}