The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#define _XOPEN_SOURCE 700

#include "tickit.h"

#include "tickit-mockterm.h"
#include "tickit-termdrv.h"

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

#define BOUND(var,min,max) \
  if(var < (min)) var = (min); \
  if(var > (max)) var = (max)

typedef struct
{
  char      *str;
  TickitPen *pen;
} MockTermCell;

typedef struct
{
  TickitTermDriver super;

  int lines;
  int cols;
  MockTermCell ***cells;

  TickitMockTermLogEntry *log;
  size_t            logsize;
  size_t            logi;

  TickitPen *pen;
  int line;
  int col;
  int cursorvis;
  int cursorshape;
} MockTermDriver;

static void mtd_free_cell(MockTermDriver *mtd, int line, int col)
{
  MockTermCell *cell = mtd->cells[line][col];

  if(cell->str)
    free(cell->str);
  if(cell->pen)
    tickit_pen_destroy(cell->pen);

  free(cell);
}

static void mtd_free_line(MockTermDriver *mtd, int line)
{
  for(int col = 0; col < mtd->cols; col++)
    mtd_free_cell(mtd, line, col);

  free(mtd->cells[line]);
}

static void mtd_clear_cells(MockTermDriver *mtd, int line, int startcol, int stopcol)
{
  /* This code is also used to initialise brand new cells in the structure, so
   * it should be careful to vivify them correctly
   */

  MockTermCell **linecells = mtd->cells[line];
  if(!linecells) {
    linecells = malloc(mtd->cols * sizeof(MockTermCell *));
    mtd->cells[line] = linecells;

    for(int col = 0; col < mtd->cols; col++)
      linecells[col] = NULL;
  }

  for(int col = startcol; col < stopcol; col++) {
    MockTermCell *cell = linecells[col];

    if(!cell) {
      cell = malloc(sizeof(MockTermCell));
      linecells[col] = cell;

      cell->str = NULL;
      cell->pen = NULL;
    }

    if(cell->str)
      free(cell->str);
    if(cell->pen)
      tickit_pen_destroy(cell->pen);

    cell->str = strdup(" ");
    cell->pen = tickit_pen_clone(mtd->pen);
  }
}

static TickitMockTermLogEntry *mtd_nextlog(MockTermDriver *mtd)
{
  if(mtd->logi == mtd->logsize) {
    mtd->logsize *= 2;
    mtd->log = realloc(mtd->log, mtd->logsize * sizeof(TickitMockTermLogEntry));
  }

  TickitMockTermLogEntry *entry = mtd->log + mtd->logi++;

  entry->str = NULL;
  entry->pen = NULL;
  return entry;
}

static void mtd_free_logentry(TickitMockTermLogEntry *entry)
{
  if(entry->str)
    free((void *)entry->str);
  entry->str = NULL;

  if(entry->pen)
    tickit_pen_destroy(entry->pen);
  entry->pen = NULL;
}

static void mtd_destroy(TickitTermDriver *ttd)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  for(int i = 0; i < mtd->logi; i++)
    mtd_free_logentry(mtd->log + i);
  free(mtd->log);

  for(int line = 0; line < mtd->lines; line++)
    mtd_free_line(mtd, line);
  free(mtd->cells);

  tickit_pen_destroy(mtd->pen);

  free(mtd);
}

static bool mtd_print(TickitTermDriver *ttd, const char *str, size_t len)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
  entry->type = LOG_PRINT;
  entry->str  = strndup(str, len);
  entry->val1 = len;

  TickitStringPos pos;
  tickit_stringpos_zero(&pos);
  pos.columns = mtd->col;

  MockTermCell **linecells = mtd->cells[mtd->line];

  TickitStringPos limit;
  tickit_stringpos_limit_columns(&limit, pos.columns);
  limit.bytes = len;

  while(pos.bytes < len) {
    TickitStringPos start = pos;

    limit.columns++;
    tickit_string_ncountmore(str, len, &pos, &limit);

    if(pos.columns == start.columns)
      continue;

    // Wrap but don't scroll - for now. This shouldn't cause scrolling anyway
    if(start.columns >= mtd->cols) {
      start.columns = 0;
      if(mtd->line < mtd->lines-1) {
        mtd->line++;
        linecells = mtd->cells[mtd->line];
      }
    }

    MockTermCell *cell = linecells[start.columns];

    if(cell->str)
      free(cell->str);
    if(cell->pen)
      tickit_pen_destroy(cell->pen);

    cell->str = strndup(str + start.bytes, pos.bytes - start.bytes);
    cell->pen = tickit_pen_clone(mtd->pen);

    // Empty out the other cells for doublewidth
    for(start.columns++; start.columns < pos.columns; start.columns++) {
      cell = linecells[start.columns];

      if(cell->str)
        free(cell->str);
      if(cell->pen)
        tickit_pen_destroy(cell->pen);

      cell->str = NULL; /* empty */
      cell->pen = tickit_pen_clone(mtd->pen);
    }
  }

  mtd->col = pos.columns;

  return true;
}

static bool mtd_goto_abs(TickitTermDriver *ttd, int line, int col)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  BOUND(line, 0, mtd->lines-1);
  BOUND(col,  0, mtd->cols-1);

  TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
  entry->type = LOG_GOTO;
  entry->val1 = line;
  entry->val2 = col;

  mtd->line = line;
  mtd->col  = col;

  return true;
}

static bool mtd_move_rel(TickitTermDriver *ttd, int downward, int rightward)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  mtd_goto_abs(ttd, mtd->line + downward, mtd->col + rightward);

  return true;
}

static bool mtd_scrollrect(TickitTermDriver *ttd, const TickitRect *rect, int downward, int rightward)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  if(!downward && !rightward)
    return true;

  int top    = rect->top;
  int left   = rect->left;
  int bottom = tickit_rect_bottom(rect);
  int right  = tickit_rect_right(rect);

  BOUND(top,    0,   mtd->lines-1);
  BOUND(bottom, top, mtd->lines);
  BOUND(left,  0,    mtd->cols-1);
  BOUND(right, left, mtd->cols);

  if((abs(downward) >= (bottom - top)) || (abs(rightward) >= (right - left)))
    return false;

  if(left == 0 && right == mtd->cols && rightward == 0) {
    MockTermCell ***cells = mtd->cells;

    TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
    entry->type = LOG_SCROLLRECT;
    entry->val1 = downward;
    entry->val2 = rightward;
    entry->rect = *rect;

    if(downward > 0) {
      int line;
      for(line = top; line < top + downward; line++)
        mtd_free_line(mtd, line);

      for(line = top; line < bottom - downward; line++)
        cells[line] = cells[line + downward];

      for(/* line */; line < bottom; line++) {
        cells[line] = NULL;
        mtd_clear_cells(mtd, line, 0, mtd->cols);
      }
    }
    else {
      int upward = -downward;

      int line;
      for(line = bottom-1; line >= bottom - upward; line--)
        mtd_free_line(mtd, line);

      for(line = bottom-1; line >= top + upward; line--)
        cells[line] = cells[line - upward];

      for(/* line */;    line >= top; line--) {
        cells[line] = NULL;
        mtd_clear_cells(mtd, line, 0, mtd->cols);
      }
    }

    return true;
  }

  if(right == mtd->cols && downward == 0) {
    TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
    entry->type = LOG_SCROLLRECT;
    entry->val1 = downward;
    entry->val2 = rightward;
    entry->rect = *rect;

    for(int line = top; line < bottom; line++) {
      MockTermCell **linecells = mtd->cells[line];

      if(rightward > 0) {
        int col;
        for(col = left; col < left + rightward; col++)
          mtd_free_cell(mtd, line, col);

        for(col = left; col < right - rightward; col++)
          linecells[col] = linecells[col + rightward];

        for(/* col */; col < right; col++)
          linecells[col] = NULL;
        mtd_clear_cells(mtd, line, right - rightward, right);
      }
      else {
        int leftward = -rightward;

        int col;
        for(col = right-1; col >= right - leftward; col--)
          mtd_free_cell(mtd, line, col);

        for(col = right-1; col >= left + leftward; col--)
          linecells[col] = linecells[col - leftward];

        for(/* col */;    col >= left; col--)
          linecells[col] = NULL;
        mtd_clear_cells(mtd, line, left, left + leftward);
      }
    }

    return true;
  }

  return false;
}

static bool mtd_erasech(TickitTermDriver *ttd, int count, TickitMaybeBool moveend)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
  entry->type = LOG_ERASECH;
  entry->val1 = count;
  entry->val2 = moveend;

  int right = mtd->col + count;
  BOUND(right, 0, mtd->cols);

  mtd_clear_cells(mtd, mtd->line, mtd->col, right);

  if(moveend != TICKIT_NO)
    mtd->col = right;

  return true;
}

static bool mtd_clear(TickitTermDriver *ttd)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
  entry->type = LOG_CLEAR;

  for(int line = 0; line < mtd->lines; line++)
    mtd_clear_cells(mtd, line, 0, mtd->cols);

  return true;
}

static bool mtd_chpen(TickitTermDriver *ttd, const TickitPen *delta, const TickitPen *final)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  TickitMockTermLogEntry *entry = mtd_nextlog(mtd);
  entry->type = LOG_SETPEN;
  entry->pen  = tickit_pen_clone(final);

  tickit_pen_clear(mtd->pen);
  tickit_pen_copy(mtd->pen, final, 1);

  return true;
}

static bool mtd_getctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int *value)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  switch(ctl) {
    case TICKIT_TERMCTL_CURSORVIS:
      *value = mtd->cursorvis;
      return true;
    case TICKIT_TERMCTL_CURSORSHAPE:
      *value = mtd->cursorshape;
      return true;
    case TICKIT_TERMCTL_COLORS:
      *value = 256;
      return true;

    default:
      return false;
  }
}

static bool mtd_setctl_int(TickitTermDriver *ttd, TickitTermCtl ctl, int value)
{
  MockTermDriver *mtd = (MockTermDriver *)ttd;

  switch(ctl) {
    case TICKIT_TERMCTL_CURSORVIS:
      mtd->cursorvis = !!value; break;
    case TICKIT_TERMCTL_CURSORSHAPE:
      mtd->cursorshape = value; break;
    case TICKIT_TERMCTL_ALTSCREEN:
    case TICKIT_TERMCTL_MOUSE:
      break;
    default:
      return false;
  }

  return true;
}

static TickitTermDriverVTable mtd_vtable = {
  .destroy    = mtd_destroy,
  .print      = mtd_print,
  .goto_abs   = mtd_goto_abs,
  .move_rel   = mtd_move_rel,
  .scrollrect = mtd_scrollrect,
  .erasech    = mtd_erasech,
  .clear      = mtd_clear,
  .chpen      = mtd_chpen,
  .getctl_int = mtd_getctl_int,
  .setctl_int = mtd_setctl_int,
};

TickitMockTerm *tickit_mockterm_new(int lines, int cols)
{
  MockTermDriver *mtd = malloc(sizeof(MockTermDriver));
  mtd->super.vtable = &mtd_vtable;

  mtd->logsize = 16; // should be sufficient; or it will grow
  mtd->log = malloc(mtd->logsize * sizeof(TickitMockTermLogEntry));
  mtd->logi = 0;

  mtd->pen       = tickit_pen_new();

  mtd->lines       = lines;
  mtd->cols        = cols;
  mtd->line        = -1;
  mtd->col         = -1;
  mtd->cursorvis   = 0;
  mtd->cursorshape = 0;

  mtd->cells = malloc(lines * sizeof(MockTermCell **));
  for(int line = 0; line < lines; line++) {
    mtd->cells[line] = NULL;
    mtd_clear_cells(mtd, line, 0, cols);
  }

  TickitMockTerm *mt = (TickitMockTerm *)tickit_term_new_for_driver(&mtd->super);
  if(!mt) {
    mtd_destroy((TickitTermDriver *)mtd);
    return NULL;
  }

  tickit_term_set_size((TickitTerm *)mt, lines, cols);

  return mt;
}

void tickit_mockterm_destroy(TickitMockTerm *mt)
{
  tickit_term_destroy((TickitTerm *)mt);
}

size_t tickit_mockterm_get_display_text(TickitMockTerm *mt, char *buffer, size_t len, int line, int col, int width)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  MockTermCell **linecells = mtd->cells[line];

  size_t ret = 0;
  for(/* col */; width; col++, width--) {
    MockTermCell *cell = linecells[col];
    size_t celllen = cell->str ? strlen(cell->str) : 0;

    if(buffer && celllen && len >= celllen) {
      strcpy(buffer, cell->str);
      buffer += celllen;
      len    -= celllen;
      if(len <= 0)
        buffer = NULL;
    }

    ret += celllen;
  }

  return ret;
}

TickitPen *tickit_mockterm_get_display_pen(TickitMockTerm *mt, int line, int col)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  return mtd->cells[line][col]->pen;
}

void tickit_mockterm_resize(TickitMockTerm *mt, int newlines, int newcols)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  MockTermCell ***newcells = malloc(newlines * sizeof(MockTermCell **));

  int oldlines = mtd->lines;
  int oldcols =  mtd->cols;

  int line;
  for(line = newlines; line < oldlines; line++)
    mtd_free_line(mtd, line);

  for(line = 0; line < newlines && line < oldlines; line++) {
    MockTermCell **newlinecells;

    if(newcols == oldcols)
      newlinecells = mtd->cells[line];
    else {
      newlinecells = malloc(newcols * sizeof(MockTermCell *));

      int col;
      for(col = newcols; col < oldcols; col++)
        mtd_free_cell(mtd, line, col);

      for(col = 0; col < newcols && col < oldcols; col++)
        newlinecells[col] = mtd->cells[line][col];
      for(/* col */; col < newcols; col++)
        newlinecells[col] = NULL;

      free(mtd->cells[line]);
    }

    newcells[line] = newlinecells;
  }
  for(/* line */; line < newlines; line++)
    newcells[line] = NULL;

  free(mtd->cells);
  mtd->cells = newcells;

  mtd->lines = newlines;
  mtd->cols  = newcols;

  if(newcols > oldcols)
    for(line = 0; line < newlines && line < oldlines; line++)
      mtd_clear_cells(mtd, line, oldcols, newcols);

  for(line = oldlines; line < newlines; line++)
    mtd_clear_cells(mtd, line, 0, newcols);

  tickit_term_set_size((TickitTerm *)mt, newlines, newcols);

  BOUND(mtd->line, 0, mtd->lines-1);
  BOUND(mtd->col,  0, mtd->cols-1);
}

int tickit_mockterm_loglen(TickitMockTerm *mt)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  return mtd->logi;
}

TickitMockTermLogEntry *tickit_mockterm_peeklog(TickitMockTerm *mt, int i)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  if(i >= 0 && i < mtd->logi)
    return mtd->log + i;

  return NULL;
}

void tickit_mockterm_clearlog(TickitMockTerm *mt)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  for(int i = 0; i < mtd->logi; i++)
    mtd_free_logentry(mtd->log + i);

  mtd->logi = 0;
}

void tickit_mockterm_get_position(TickitMockTerm *mt, int *line, int *col)
{
  MockTermDriver *mtd = (MockTermDriver *)tickit_term_get_driver((TickitTerm *)mt);

  if(line)
    *line = mtd->line;
  if(col)
    *col = mtd->col;
}

void tickit_mockterm_press_key(TickitMockTerm *mt, int type, const char *str, int mod)
{
  TickitKeyEventInfo info = {
    .type = type,
    .mod = mod,
    .str = str,
  };

  tickit_termdrv_send_key(tickit_term_get_driver((TickitTerm *)mt), &info);
}

void tickit_mockterm_press_mouse(TickitMockTerm *mt, int type, int button, int line, int col, int mod)
{
  TickitMouseEventInfo info = {
    .type = type,
    .button = button,
    .line = line,
    .col = col,
    .mod = mod,
  };

  tickit_termdrv_send_mouse(tickit_term_get_driver((TickitTerm *)mt), &info);
}