The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
/* We need strdup */
#define _XOPEN_SOURCE 600

#include "tickit.h"

#include <stdio.h>  // vsnprintf
#include <stdlib.h>
#include <string.h>

#include "linechars.inc"

#define RECT_PRINTF_FMT     "[(%d,%d)..(%d,%d)]"
#define RECT_PRINTF_ARGS(r) (r).left, (r).top, tickit_rect_right(&(r)), tickit_rect_bottom(&(r))

/* must match .pm file */
enum TickitRenderBufferCellState {
  SKIP  = 0,
  TEXT  = 1,
  ERASE = 2,
  CONT  = 3,
  LINE  = 4,
  CHAR  = 5,
};

enum {
  NORTH_SHIFT = 0,
  EAST_SHIFT  = 2,
  SOUTH_SHIFT = 4,
  WEST_SHIFT  = 6,
};

// Internal cell structure definition
typedef struct {
  enum TickitRenderBufferCellState state;
  int cols; // or "startcol" for state == CONT
  int maskdepth; // -1 if not masked
  TickitPen *pen; // state -> {TEXT, ERASE, LINE, CHAR}
  union {
    struct { int idx; int offs; } text; // state == TEXT
    struct { int mask;          } line; // state == LINE
    struct { int codepoint;     } chr;  // state == CHAR
  } v;
} RBCell;

typedef struct RBStack RBStack;
struct RBStack {
  RBStack *prev;

  int vc_line, vc_col;
  int xlate_line, xlate_col;
  TickitRect clip;
  TickitPen *pen;
  unsigned int pen_only : 1;
};

struct TickitRenderBuffer {
  int lines, cols; // Size
  RBCell **cells;

  unsigned int vc_pos_set : 1;
  int vc_line, vc_col;
  int xlate_line, xlate_col;
  TickitRect clip;
  TickitPen *pen;

  int depth;
  RBStack *stack;

  char **texts;
  size_t n_texts;    // number actually valid
  size_t size_texts; // size of allocated buffer

  char *tmp;
  size_t tmplen;  // actually valid
  size_t tmpsize; // allocated size

  int refcount;
};

static void free_stack(RBStack *stack)
{
  while(stack) {
    RBStack *prev = stack->prev;
    if(stack->pen)
      tickit_pen_unref(stack->pen);
    free(stack);

    stack = prev;
  }
}

static void free_texts(TickitRenderBuffer *rb)
{
  for(int i = 0; i < rb->n_texts; i++)
    free(rb->texts[i]);

  // Prevent the buffer growing too big
  if(rb->size_texts > 4 && rb->size_texts > rb->n_texts * 2) {
    rb->size_texts /= 2;
    free(rb->texts);
    rb->texts = malloc(rb->size_texts * sizeof(char *));
  }

  rb->n_texts = 0;
}

static int xlate_and_clip(TickitRenderBuffer *rb, int *line, int *col, int *cols, int *startcol)
{
  *line += rb->xlate_line;
  *col  += rb->xlate_col;

  const TickitRect *clip = &rb->clip;

  if(!clip->lines)
    return 0;

  if(*line < clip->top ||
      *line >= tickit_rect_bottom(clip) ||
      *col  >= tickit_rect_right(clip))
    return 0;

  if(startcol)
    *startcol = 0;

  if(*col < clip->left) {
    *cols      -= clip->left - *col;
    if(startcol)
      *startcol += clip->left - *col;
    *col = clip->left;
  }
  if(*cols <= 0)
    return 0;

  if(*cols > tickit_rect_right(clip) - *col)
    *cols = tickit_rect_right(clip) - *col;

  return 1;
}

static void cont_cell(RBCell *cell, int startcol)
{
  switch(cell->state) {
    case TEXT:
    case ERASE:
    case LINE:
    case CHAR:
      tickit_pen_unref(cell->pen);
      break;
    case SKIP:
    case CONT:
      /* ignore */
      break;
  }

  cell->state     = CONT;
  cell->maskdepth = -1;
  cell->cols      = startcol;
  cell->pen       = NULL;
}

static void debug_logf(TickitRenderBuffer *rb, const char *flag, const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  char fmt_with_indent[strlen(fmt) + 3 * rb->depth + 1];
  {
    char *s = fmt_with_indent;
    for(int i = 0; i < rb->depth; i++)
      s += sprintf(s, "|  ");
    strcpy(s, fmt);
  }

  tickit_debug_vlogf(flag, fmt_with_indent, args);

  va_end(args);
}

#define DEBUG_LOGF  if(tickit_debug_enabled) debug_logf

static RBCell *make_span(TickitRenderBuffer *rb, int line, int col, int cols)
{
  int end = col + cols;
  RBCell **cells = rb->cells;

  // If the following cell is a CONT, it needs to become a new start
  if(end < rb->cols && cells[line][end].state == CONT) {
    int spanstart = cells[line][end].cols;
    RBCell *spancell = &cells[line][spanstart];
    int spanend = spanstart + spancell->cols;
    int afterlen = spanend - end;
    RBCell *endcell = &cells[line][end];

    switch(spancell->state) {
      case SKIP:
        endcell->state = SKIP;
        endcell->cols  = afterlen;
        break;
      case TEXT:
        endcell->state       = TEXT;
        endcell->cols        = afterlen;
        endcell->pen         = tickit_pen_ref(spancell->pen);
        endcell->v.text.idx  = spancell->v.text.idx;
        endcell->v.text.offs = spancell->v.text.offs + end - spanstart;
        break;
      case ERASE:
        endcell->state = ERASE;
        endcell->cols  = afterlen;
        endcell->pen   = tickit_pen_ref(spancell->pen);
        break;
      case LINE:
      case CHAR:
      case CONT:
        abort();
    }

    // We know these are already CONT cells
    for(int c = end + 1; c < spanend; c++)
      cells[line][c].cols = end;
  }

  // If the initial cell is a CONT, shorten its start
  if(cells[line][col].state == CONT) {
    int beforestart = cells[line][col].cols;
    RBCell *spancell = &cells[line][beforestart];
    int beforelen = col - beforestart;

    switch(spancell->state) {
      case SKIP:
      case TEXT:
      case ERASE:
        spancell->cols = beforelen;
        break;
      case LINE:
      case CHAR:
      case CONT:
        abort();
    }
  }

  // cont_cell() also frees any pens in the range
  for(int c = col; c < end; c++)
    cont_cell(&cells[line][c], col);

  cells[line][col].cols = cols;

  return &cells[line][col];
}

static void tmp_cat_utf8(TickitRenderBuffer *rb, long codepoint)
{
  int seqlen = tickit_string_seqlen(codepoint);
  if(rb->tmpsize < rb->tmplen + seqlen) {
    rb->tmpsize *= 2;
    rb->tmp = realloc(rb->tmp, rb->tmpsize);
  }

  tickit_string_putchar(rb->tmp + rb->tmplen, rb->tmpsize - rb->tmplen, codepoint);
  rb->tmplen += seqlen;

  /* rb->tmp remains NOT nul-terminated */
}

static void tmp_alloc(TickitRenderBuffer *rb, size_t len)
{
  if(rb->tmpsize < len) {
    free(rb->tmp);

    while(rb->tmpsize < len)
      rb->tmpsize *= 2;
    rb->tmp = malloc(rb->tmpsize);
  }
}

static int put_text(TickitRenderBuffer *rb, int line, int col, const char *text, size_t len)
{
  TickitStringPos endpos;
  len = tickit_string_ncount(text, len, &endpos, NULL);
  if(1 + len == 0)
    return -1;

  int cols = endpos.columns;
  int ret = cols;

  int startcol;
  if(!xlate_and_clip(rb, &line, &col, &cols, &startcol))
    return ret;

  if(rb->n_texts == rb->size_texts) {
    rb->size_texts *= 2;
    rb->texts = realloc(rb->texts, rb->size_texts * sizeof(char *));
  }

  rb->texts[rb->n_texts] = malloc(len + 1);
  memcpy(rb->texts[rb->n_texts], text, len);
  rb->texts[rb->n_texts][len] = '\0';

  RBCell *linecells = rb->cells[line];

  while(cols) {
    while(cols && linecells[col].maskdepth > -1) {
      col++;
      cols--;
      startcol++;
    }
    if(!cols)
      break;

    int spanlen = 0;
    while(cols && linecells[col + spanlen].maskdepth == -1) {
      spanlen++;
      cols--;
    }
    if(!spanlen)
      break;

    RBCell *cell = make_span(rb, line, col, spanlen);
    cell->state       = TEXT;
    cell->pen         = tickit_pen_ref(rb->pen);
    cell->v.text.idx  = rb->n_texts;
    cell->v.text.offs = startcol;

    col      += spanlen;
    startcol += spanlen;
  }

  rb->n_texts++;

  return ret;
}

static int put_vtextf(TickitRenderBuffer *rb, int line, int col, const char *fmt, va_list args)
{
  /* It's likely the string will fit in, say, 64 bytes */
  char buffer[64];
  size_t len;
  {
    va_list args_for_size;
    va_copy(args_for_size, args);

    len = vsnprintf(buffer, sizeof buffer, fmt, args_for_size);

    va_end(args_for_size);
  }

  if(len < sizeof buffer)
    return put_text(rb, line, col, buffer, len);

  tmp_alloc(rb, len + 1);
  vsnprintf(rb->tmp, rb->tmpsize, fmt, args);
  return put_text(rb, line, col, rb->tmp, len);
}

static void put_char(TickitRenderBuffer *rb, int line, int col, long codepoint)
{
  int cols = 1;

  if(!xlate_and_clip(rb, &line, &col, &cols, NULL))
    return;

  if(rb->cells[line][col].maskdepth > -1)
    return;

  RBCell *cell = make_span(rb, line, col, cols);
  cell->state           = CHAR;
  cell->pen             = tickit_pen_ref(rb->pen);
  cell->v.chr.codepoint = codepoint;
}

static void skip(TickitRenderBuffer *rb, int line, int col, int cols)
{
  if(!xlate_and_clip(rb, &line, &col, &cols, NULL))
    return;

  RBCell *linecells = rb->cells[line];

  while(cols) {
    while(cols && linecells[col].maskdepth > -1) {
      col++;
      cols--;
    }
    if(!cols)
      break;

    int spanlen = 0;
    while(cols && linecells[col + spanlen].maskdepth == -1) {
      spanlen++;
      cols--;
    }
    if(!spanlen)
      break;

    RBCell *cell = make_span(rb, line, col, spanlen);
    cell->state = SKIP;

    col += spanlen;
  }
}

static void erase(TickitRenderBuffer *rb, int line, int col, int cols)
{
  if(!xlate_and_clip(rb, &line, &col, &cols, NULL))
    return;

  RBCell *linecells = rb->cells[line];

  while(cols) {
    while(cols && linecells[col].maskdepth > -1) {
      col++;
      cols--;
    }
    if(!cols)
      break;

    int spanlen = 0;
    while(cols && linecells[col + spanlen].maskdepth == -1) {
      spanlen++;
      cols--;
    }
    if(!spanlen)
      break;

    RBCell *cell = make_span(rb, line, col, spanlen);
    cell->state = ERASE;
    cell->pen   = tickit_pen_ref(rb->pen);

    col += spanlen;
  }
}

TickitRenderBuffer *tickit_renderbuffer_new(int lines, int cols)
{
  TickitRenderBuffer *rb = malloc(sizeof(TickitRenderBuffer));

  rb->lines = lines;
  rb->cols  = cols;

  rb->cells = malloc(rb->lines * sizeof(RBCell *));
  for(int line = 0; line < rb->lines; line++) {
    rb->cells[line] = malloc(rb->cols * sizeof(RBCell));

    rb->cells[line][0].state     = SKIP;
    rb->cells[line][0].maskdepth = -1;
    rb->cells[line][0].cols      = rb->cols;
    rb->cells[line][0].pen       = NULL;

    for(int col = 1; col < rb->cols; col++) {
      rb->cells[line][col].state     = CONT;
      rb->cells[line][col].maskdepth = -1;
      rb->cells[line][col].cols      = 0;
    }
  }

  rb->vc_pos_set = 0;

  rb->xlate_line = 0;
  rb->xlate_col  = 0;

  tickit_rect_init_sized(&rb->clip, 0, 0, rb->lines, rb->cols);

  rb->pen = tickit_pen_new();

  rb->stack = NULL;
  rb->depth = 0;

  rb->n_texts = 0;
  rb->size_texts = 4;
  rb->texts = malloc(rb->size_texts * sizeof(char *));

  rb->tmpsize = 256; // hopefully enough but will grow if required
  rb->tmp = malloc(rb->tmpsize);
  rb->tmplen = 0;

  rb->refcount = 1;

  return rb;
}

void tickit_renderbuffer_destroy(TickitRenderBuffer *rb)
{
  for(int line = 0; line < rb->lines; line++) {
    for(int col = 0; col < rb->cols; col++) {
      RBCell *cell = &rb->cells[line][col];
      switch(cell->state) {
        case TEXT:
        case ERASE:
        case LINE:
        case CHAR:
          tickit_pen_unref(cell->pen);
          break;
        case SKIP:
        case CONT:
          break;
      }
    }
    free(rb->cells[line]);
  }

  free(rb->cells);
  rb->cells = NULL;

  tickit_pen_unref(rb->pen);

  if(rb->stack)
    free_stack(rb->stack);

  free_texts(rb);
  free(rb->texts);

  free(rb->tmp);

  free(rb);
}

TickitRenderBuffer *tickit_renderbuffer_ref(TickitRenderBuffer *rb)
{
  rb->refcount++;
  return rb;
}

void tickit_renderbuffer_unref(TickitRenderBuffer *rb)
{
  if(rb->refcount < 1) {
    fprintf(stderr, "tickit_renderbuffer_unref: invalid refcount %d\n", rb->refcount);
    abort();
  }
  rb->refcount--;
  if(!rb->refcount)
    tickit_renderbuffer_destroy(rb);
}

void tickit_renderbuffer_get_size(const TickitRenderBuffer *rb, int *lines, int *cols)
{
  if(lines)
    *lines = rb->lines;

  if(cols)
    *cols = rb->cols;
}

void tickit_renderbuffer_translate(TickitRenderBuffer *rb, int downward, int rightward)
{
  DEBUG_LOGF(rb, "Bt", "Translate (%+d,%+d)", rightward, downward);

  rb->xlate_line += downward;
  rb->xlate_col  += rightward;
}

void tickit_renderbuffer_clip(TickitRenderBuffer *rb, TickitRect *rect)
{
  DEBUG_LOGF(rb, "Bt", "Clip " RECT_PRINTF_FMT, RECT_PRINTF_ARGS(*rect));

  TickitRect other;

  other = *rect;
  other.top  += rb->xlate_line;
  other.left += rb->xlate_col;

  if(!tickit_rect_intersect(&rb->clip, &rb->clip, &other))
    rb->clip.lines = 0;
}

void tickit_renderbuffer_mask(TickitRenderBuffer *rb, TickitRect *mask)
{
  DEBUG_LOGF(rb, "Bt", "Mask " RECT_PRINTF_FMT, RECT_PRINTF_ARGS(*mask));

  TickitRect hole;

  hole = *mask;
  hole.top  += rb->xlate_line;
  hole.left += rb->xlate_col;

  if(hole.top < 0) {
    hole.lines += hole.top;
    hole.top = 0;
  }
  if(hole.left < 0) {
    hole.cols += hole.left;
    hole.left = 0;
  }

  for(int line = hole.top; line < hole.top + hole.lines && line < rb->lines; line++) {
    for(int col = hole.left; col < hole.left + hole.cols && col < rb->cols; col++) {
      RBCell *cell = &rb->cells[line][col];
      if(cell->maskdepth == -1)
        cell->maskdepth = rb->depth;
    }
  }
}

bool tickit_renderbuffer_has_cursorpos(const TickitRenderBuffer *rb)
{
  return rb->vc_pos_set;
}

void tickit_renderbuffer_get_cursorpos(const TickitRenderBuffer *rb, int *line, int *col)
{
  if(rb->vc_pos_set && line)
    *line = rb->vc_line;
  if(rb->vc_pos_set && col)
    *col = rb->vc_col;
}

void tickit_renderbuffer_goto(TickitRenderBuffer *rb, int line, int col)
{
  rb->vc_pos_set = 1;
  rb->vc_line = line;
  rb->vc_col  = col;
}

void tickit_renderbuffer_ungoto(TickitRenderBuffer *rb)
{
  rb->vc_pos_set = 0;
}

void tickit_renderbuffer_setpen(TickitRenderBuffer *rb, const TickitPen *pen)
{
  TickitPen *prevpen = rb->stack ? rb->stack->pen : NULL;

  /* never mutate the pen inplace; make a new one */
  TickitPen *newpen = tickit_pen_new();

  if(pen)
    tickit_pen_copy(newpen, pen, 1);
  if(prevpen)
    tickit_pen_copy(newpen, prevpen, 0);

  tickit_pen_unref(rb->pen);
  rb->pen = newpen;
}

void tickit_renderbuffer_reset(TickitRenderBuffer *rb)
{
  for(int line = 0; line < rb->lines; line++) {
    // cont_cell also frees pen
    for(int col = 0; col < rb->cols; col++)
      cont_cell(&rb->cells[line][col], 0);

    rb->cells[line][0].state     = SKIP;
    rb->cells[line][0].maskdepth = -1;
    rb->cells[line][0].cols      = rb->cols;
  }

  rb->vc_pos_set = 0;

  rb->xlate_line = 0;
  rb->xlate_col  = 0;

  tickit_rect_init_sized(&rb->clip, 0, 0, rb->lines, rb->cols);

  tickit_pen_unref(rb->pen);
  rb->pen = tickit_pen_new();

  if(rb->stack) {
    free_stack(rb->stack);
    rb->stack = NULL;
    rb->depth = 0;
  }

  free_texts(rb);
}

void tickit_renderbuffer_clear(TickitRenderBuffer *rb)
{
  DEBUG_LOGF(rb, "Bd", "Clear");

  for(int line = 0; line < rb->lines; line++)
    erase(rb, line, 0, rb->cols);
}

void tickit_renderbuffer_save(TickitRenderBuffer *rb)
{
  DEBUG_LOGF(rb, "Bs", "+-Save");

  RBStack *stack = malloc(sizeof(struct RBStack));

  stack->vc_line    = rb->vc_line;
  stack->vc_col     = rb->vc_col;
  stack->xlate_line = rb->xlate_line;
  stack->xlate_col  = rb->xlate_col;
  stack->clip       = rb->clip;
  stack->pen        = tickit_pen_ref(rb->pen);
  stack->pen_only   = 0;

  stack->prev = rb->stack;
  rb->stack = stack;
  rb->depth++;
}

void tickit_renderbuffer_savepen(TickitRenderBuffer *rb)
{
  DEBUG_LOGF(rb, "Bs", "+-Savepen");

  RBStack *stack = malloc(sizeof(struct RBStack));

  stack->pen      = tickit_pen_ref(rb->pen);
  stack->pen_only = 1;

  stack->prev = rb->stack;
  rb->stack = stack;
  rb->depth++;
}

void tickit_renderbuffer_restore(TickitRenderBuffer *rb)
{
  RBStack *stack;

  if(!rb->stack)
    return;

  stack = rb->stack;
  rb->stack = stack->prev;

  if(!stack->pen_only) {
    rb->vc_line    = stack->vc_line;
    rb->vc_col     = stack->vc_col;
    rb->xlate_line = stack->xlate_line;
    rb->xlate_col  = stack->xlate_col;
    rb->clip       = stack->clip;
  }

  tickit_pen_unref(rb->pen);
  rb->pen = stack->pen;
  // We've now definitely taken ownership of the old stack frame's pen, so
  //   it doesn't need destroying now

  rb->depth--;

  // TODO: this could be done more efficiently by remembering the edges of masking
  for(int line = 0; line < rb->lines; line++)
    for(int col = 0; col < rb->cols; col++)
      if(rb->cells[line][col].maskdepth > rb->depth)
        rb->cells[line][col].maskdepth = -1;

  free(stack);

  DEBUG_LOGF(rb, "Bs", "+-Restore");
}

void tickit_renderbuffer_skip_at(TickitRenderBuffer *rb, int line, int col, int cols)
{
  DEBUG_LOGF(rb, "Bd", "Skip (%d..%d,%d)", col, col + cols, line);

  skip(rb, line, col, cols);
}

void tickit_renderbuffer_skip(TickitRenderBuffer *rb, int cols)
{
  if(!rb->vc_pos_set)
    return;

  DEBUG_LOGF(rb, "Bd", "Skip (%d..%d,%d) +%d", rb->vc_col, rb->vc_col + cols, rb->vc_line, cols);

  skip(rb, rb->vc_line, rb->vc_col, cols);
  rb->vc_col += cols;
}

void tickit_renderbuffer_skip_to(TickitRenderBuffer *rb, int col)
{
  if(!rb->vc_pos_set)
    return;

  DEBUG_LOGF(rb, "Bd", "Skip (%d..%d,%d) +%d", rb->vc_col, col, rb->vc_line, col - rb->vc_col);

  if(rb->vc_col < col)
    skip(rb, rb->vc_line, rb->vc_col, col - rb->vc_col);

  rb->vc_col = col;
}

int tickit_renderbuffer_text_at(TickitRenderBuffer *rb, int line, int col, const char *text)
{
  return tickit_renderbuffer_textn_at(rb, line, col, text, -1);
}

int tickit_renderbuffer_textn_at(TickitRenderBuffer *rb, int line, int col, const char *text, size_t len)
{
  int cols = put_text(rb, line, col, text, len);

  DEBUG_LOGF(rb, "Bd", "Text (%d..%d,%d)", col, col + cols, line);

  return cols;
}

int tickit_renderbuffer_text(TickitRenderBuffer *rb, const char *text)
{
  return tickit_renderbuffer_textn(rb, text, -1);
}

int tickit_renderbuffer_textn(TickitRenderBuffer *rb, const char *text, size_t len)
{
  if(!rb->vc_pos_set)
    return -1;

  int cols = put_text(rb, rb->vc_line, rb->vc_col, text, len);

  DEBUG_LOGF(rb, "Bd", "Text (%d..%d,%d) +%d", rb->vc_col, rb->vc_col + cols, rb->vc_line, cols);

  rb->vc_col += cols;
  return cols;
}

int tickit_renderbuffer_textf_at(TickitRenderBuffer *rb, int line, int col, const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  int ret = tickit_renderbuffer_vtextf_at(rb, line, col, fmt, args);

  va_end(args);

  return ret;
}

int tickit_renderbuffer_vtextf_at(TickitRenderBuffer *rb, int line, int col, const char *fmt, va_list args)
{
  int cols = put_vtextf(rb, line, col, fmt, args);

  DEBUG_LOGF(rb, "Bd", "Text (%d..%d,%d)", col, col + cols, line);

  return cols;
}

int tickit_renderbuffer_textf(TickitRenderBuffer *rb, const char *fmt, ...)
{
  va_list args;
  va_start(args, fmt);

  int ret = tickit_renderbuffer_vtextf(rb, fmt, args);

  va_end(args);

  return ret;
}

int tickit_renderbuffer_vtextf(TickitRenderBuffer *rb, const char *fmt, va_list args)
{
  if(!rb->vc_pos_set)
    return -1;

  int cols = put_vtextf(rb, rb->vc_line, rb->vc_col, fmt, args);

  DEBUG_LOGF(rb, "Bd", "Text (%d..%d,%d) +%d", rb->vc_col, rb->vc_col + cols, rb->vc_line, cols);

  rb->vc_col += cols;
  return cols;
}

void tickit_renderbuffer_erase_at(TickitRenderBuffer *rb, int line, int col, int cols)
{
  DEBUG_LOGF(rb, "Bd", "Erase (%d..%d,%d)", col, col + cols, line);

  erase(rb, line, col, cols);
}

void tickit_renderbuffer_erase(TickitRenderBuffer *rb, int cols)
{
  if(!rb->vc_pos_set)
    return;

  DEBUG_LOGF(rb, "Bd", "Erase (%d..%d,%d) +%d", rb->vc_col, rb->vc_col + cols, rb->vc_line, cols);

  erase(rb, rb->vc_line, rb->vc_col, cols);
  rb->vc_col += cols;
}

void tickit_renderbuffer_erase_to(TickitRenderBuffer *rb, int col)
{
  if(!rb->vc_pos_set)
    return;

  DEBUG_LOGF(rb, "Bd", "Erase (%d..%d,%d) +%d", rb->vc_col, col, rb->vc_line, col - rb->vc_col);

  if(rb->vc_col < col)
    erase(rb, rb->vc_line, rb->vc_col, col - rb->vc_col);

  rb->vc_col = col;
}

void tickit_renderbuffer_eraserect(TickitRenderBuffer *rb, TickitRect *rect)
{
  DEBUG_LOGF(rb, "Bd", "Erase [(%d,%d)..(%d,%d)]", rect->left, rect->top, rect->left + rect->cols, rect->top + rect->lines);

  for(int line = rect->top; line < tickit_rect_bottom(rect); line++)
    erase(rb, line, rect->left, rect->cols);
}

void tickit_renderbuffer_char_at(TickitRenderBuffer *rb, int line, int col, long codepoint)
{
  DEBUG_LOGF(rb, "Bd", "Char (%d.,%d,%d)", col, col + 1, line);

  put_char(rb, line, col, codepoint);
}

void tickit_renderbuffer_char(TickitRenderBuffer *rb, long codepoint)
{
  if(!rb->vc_pos_set)
    return;

  DEBUG_LOGF(rb, "Bd", "Char (%d..%d,%d) +%d", rb->vc_col, rb->vc_col + 1, rb->vc_line, 1);

  put_char(rb, rb->vc_line, rb->vc_col, codepoint);
  // TODO: might not be 1; would have to look it up
  rb->vc_col += 1;
}

static void linecell(TickitRenderBuffer *rb, int line, int col, int bits)
{
  int cols = 1;

  if(!xlate_and_clip(rb, &line, &col, &cols, NULL))
    return;

  if(rb->cells[line][col].maskdepth > -1)
    return;

  RBCell *cell = &rb->cells[line][col];
  if(cell->state != LINE) {
    make_span(rb, line, col, cols);
    cell->state       = LINE;
    cell->cols        = 1;
    cell->pen         = tickit_pen_ref(rb->pen);
    cell->v.line.mask = 0;
  }
  else if(!tickit_pen_equiv(cell->pen, rb->pen)) {
    tickit_pen_unref(cell->pen);
    cell->pen   = tickit_pen_ref(rb->pen);
  }

  cell->v.line.mask |= bits;
}

void tickit_renderbuffer_hline_at(TickitRenderBuffer *rb, int line, int startcol, int endcol,
    TickitLineStyle style, TickitLineCaps caps)
{
  DEBUG_LOGF(rb, "Bd", "HLine (%d..%d,%d)", startcol, endcol, line);

  int east = style << EAST_SHIFT;
  int west = style << WEST_SHIFT;

  linecell(rb, line, startcol, east | (caps & TICKIT_LINECAP_START ? west : 0));
  for(int col = startcol + 1; col <= endcol - 1; col++)
    linecell(rb, line, col, east | west);
  linecell(rb, line, endcol, (caps & TICKIT_LINECAP_END ? east : 0) | west);
}

void tickit_renderbuffer_vline_at(TickitRenderBuffer *rb, int startline, int endline, int col,
    TickitLineStyle style, TickitLineCaps caps)
{
  DEBUG_LOGF(rb, "Bd", "VLine (%d,%d..%d)", col, startline, endline);

  int north = style << NORTH_SHIFT;
  int south = style << SOUTH_SHIFT;

  linecell(rb, startline, col, south | (caps & TICKIT_LINECAP_START ? north : 0));
  for(int line = startline + 1; line <= endline - 1; line++)
    linecell(rb, line, col, south | north);
  linecell(rb, endline, col, (caps & TICKIT_LINECAP_END ? south : 0) | north);
}

void tickit_renderbuffer_flush_to_term(TickitRenderBuffer *rb, TickitTerm *tt)
{
  DEBUG_LOGF(rb, "Bf", "Flush to term");

  for(int line = 0; line < rb->lines; line++) {
    int phycol = -1; /* column where the terminal cursor physically is */

    for(int col = 0; col < rb->cols; /**/) {
      RBCell *cell = &rb->cells[line][col];

      if(cell->state == SKIP) {
        col += cell->cols;
        continue;
      }

      if(phycol < col)
        tickit_term_goto(tt, line, col);
      phycol = col;

      switch(cell->state) {
        case TEXT:
          {
            TickitStringPos start, end, limit;
            char *text = rb->texts[cell->v.text.idx];

            tickit_stringpos_limit_columns(&limit, cell->v.text.offs);
            tickit_string_count(text, &start, &limit);

            limit.columns += cell->cols;
            end = start;
            tickit_string_countmore(text, &end, &limit);

            tickit_term_setpen(tt, cell->pen);
            tickit_term_printn(tt, text + start.bytes, end.bytes - start.bytes);

            phycol += cell->cols;
          }
          break;
        case ERASE:
          {
            /* No need to set moveend=true to erasech unless we actually
             * have more content */
            int moveend = col + cell->cols < rb->cols &&
                          rb->cells[line][col + cell->cols].state != SKIP;

            tickit_term_setpen(tt, cell->pen);
            tickit_term_erasech(tt, cell->cols, moveend ? TICKIT_YES : TICKIT_MAYBE);

            if(moveend)
              phycol += cell->cols;
            else
              phycol = -1;
          }
          break;
        case LINE:
          {
            TickitPen *pen = cell->pen;

            do {
              tmp_cat_utf8(rb, linemask_to_char[cell->v.line.mask]);

              col++;
              phycol += cell->cols;
            } while(col < rb->cols &&
                    (cell = &rb->cells[line][col]) &&
                    cell->state == LINE &&
                    tickit_pen_equiv(cell->pen, pen));

            tickit_term_setpen(tt, pen);
            tickit_term_printn(tt, rb->tmp, rb->tmplen);
            rb->tmplen = 0;
          }
          continue; /* col already updated */
        case CHAR:
          {
            tmp_cat_utf8(rb, cell->v.chr.codepoint);

            tickit_term_setpen(tt, cell->pen);
            tickit_term_printn(tt, rb->tmp, rb->tmplen);
            rb->tmplen = 0;

            phycol += cell->cols;
          }
          break;
        case SKIP:
        case CONT:
          /* unreachable */
          abort();
      }

      col += cell->cols;
    }
  }

  tickit_renderbuffer_reset(rb);
}

void tickit_renderbuffer_blit(TickitRenderBuffer *dst, TickitRenderBuffer *src)
{
  for(int line = 0; line < src->lines; line++) {
    for(int col = 0; col < src->cols; /**/) {
      RBCell *cell = &src->cells[line][col];

      if(cell->state != SKIP) {
        tickit_renderbuffer_savepen(dst);
        tickit_renderbuffer_setpen(dst, cell->pen);
      }

      switch(cell->state) {
        case SKIP:
          break;
        case TEXT:
          {
            TickitStringPos start, end, limit;
            char *text = src->texts[cell->v.text.idx];

            tickit_stringpos_limit_columns(&limit, cell->v.text.offs);
            tickit_string_count(text, &start, &limit);

            limit.columns += cell->cols;
            end = start;
            tickit_string_countmore(text, &end, &limit);

            put_text(dst, line, col, text + start.bytes, end.bytes - start.bytes);
          }
          break;
        case ERASE:
          erase(dst, line, col, cell->cols);
          break;
        case LINE:
          linecell(dst, line, col, cell->v.line.mask);
          break;
        case CHAR:
          put_char(dst, line, col, cell->v.chr.codepoint);
          break;
        case CONT:
          /* unreachable */
          abort();
      }

      if(cell->state != SKIP)
        tickit_renderbuffer_restore(dst);

      col += cell->cols;
    }
  }
}

static RBCell *get_span(TickitRenderBuffer *rb, int line, int col, int *offset)
{
  int cols = 1;
  if(!xlate_and_clip(rb, &line, &col, &cols, NULL))
    return NULL;

  *offset = 0;
  RBCell *cell = &rb->cells[line][col];
  if(cell->state == CONT) {
    *offset = col - cell->cols; // startcol
    cell = &rb->cells[line][cell->cols];
  }

  return cell;
}

static size_t get_span_text(TickitRenderBuffer *rb, RBCell *span, int offset, int one_grapheme, char *buffer, size_t len)
{
  size_t bytes;

  switch(span->state) {
    case CONT: // should be unreachable
      return -1;

    case SKIP:
    case ERASE:
      bytes = 0;
      break;

    case TEXT:
      {
        char *text = rb->texts[span->v.text.idx];
        TickitStringPos start, end, limit;

        tickit_stringpos_limit_columns(&limit, span->v.text.offs + offset);
        tickit_string_count(text, &start, &limit);

        if(one_grapheme)
          tickit_stringpos_limit_graphemes(&limit, start.graphemes + 1);
        else
          tickit_stringpos_limit_columns(&limit, span->cols);
        end = start;
        tickit_string_countmore(text, &end, &limit);

        bytes = end.bytes - start.bytes;

        if(buffer) {
          if(len < bytes)
            return -1;
          strncpy(buffer, text + start.bytes, bytes);
          buffer[bytes] = 0;
        }
        break;
      }
    case LINE:
      bytes = tickit_string_putchar(buffer, len, linemask_to_char[span->v.line.mask]);
      break;

    case CHAR:
      bytes = tickit_string_putchar(buffer, len, span->v.chr.codepoint);
      break;
  }

  if(buffer && len > bytes)
    buffer[bytes] = 0;

  return bytes;
}

int tickit_renderbuffer_get_cell_active(TickitRenderBuffer *rb, int line, int col)
{
  int offset;
  RBCell *span = get_span(rb, line, col, &offset);
  if(!span)
    return -1;

  return span->state != SKIP;
}

size_t tickit_renderbuffer_get_cell_text(TickitRenderBuffer *rb, int line, int col, char *buffer, size_t len)
{
  int offset;
  RBCell *span = get_span(rb, line, col, &offset);
  if(!span || span->state == CONT)
    return -1;

  return get_span_text(rb, span, offset, 1, buffer, len);
}

TickitRenderBufferLineMask tickit_renderbuffer_get_cell_linemask(TickitRenderBuffer *rb, int line, int col)
{
  int offset;
  RBCell *span = get_span(rb, line, col, &offset);
  if(!span || span->state != LINE)
    return (TickitRenderBufferLineMask){ 0 };

  return (TickitRenderBufferLineMask){
    .north = (span->v.line.mask >> NORTH_SHIFT) & 0x03,
    .south = (span->v.line.mask >> SOUTH_SHIFT) & 0x03,
    .east  = (span->v.line.mask >> EAST_SHIFT ) & 0x03,
    .west  = (span->v.line.mask >> WEST_SHIFT ) & 0x03,
  };
}

TickitPen *tickit_renderbuffer_get_cell_pen(TickitRenderBuffer *rb, int line, int col)
{
  int offset;
  RBCell *span = get_span(rb, line, col, &offset);
  if(!span || span->state == SKIP)
    return NULL;

  return span->pen;
}

size_t tickit_renderbuffer_get_span(TickitRenderBuffer *rb, int line, int startcol, struct TickitRenderBufferSpanInfo *info, char *text, size_t len)
{
  int offset;
  RBCell *span = get_span(rb, line, startcol, &offset);
  if(!span || span->state == CONT)
    return -1;

  if(info)
    info->n_columns = span->cols - offset;

  if(span->state == SKIP) {
    if(info)
      info->is_active = 0;
    return 0;
  }

  if(info)
    info->is_active = 1;

  if(info && info->pen) {
    tickit_pen_clear(info->pen);
    tickit_pen_copy(info->pen, span->pen, 1);
  }

  size_t retlen = get_span_text(rb, span, offset, 0, text, len);
  if(info) {
    info->len = retlen;
    info->text = text;
  }
  return len;
}