/*  You may distribute under the terms of either the GNU General Public License
 *  or the Artistic License (the same terms as Perl itself)
 *
 *  (C) Paul Evans, 2014-2020 -- leonerd@leonerd.org.uk
 */

#define PERL_NO_GET_CONTEXT

#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include <vterm.h>

#include <string.h>

#ifdef HAVE_DMD_HELPER
#  define WANT_DMD_API_044
#  include "DMD_helper.h"
#endif

#define streq(a,b)  (!strcmp(a,b))

#define FREE_CB(v)  if(self->v) SvREFCNT_dec(self->v)

#ifdef tTHX
#  define dTHXa_FROM_SELF   dTHXa(self->myperl)
#else
#  define dTHXa_FROM_SELF
#endif

typedef struct Term__VTerm {
#ifdef tTHX
  tTHX myperl;
#endif
  VTerm *vt;

  struct {
    CV *text;
    CV *control;
    CV *escape;
    CV *csi;
    CV *osc;
    CV *dcs;
    CV *resize;
  } parser_cb;

  SV *strbuf;

} *Term__VTerm;

typedef VTermColor      *Term__VTerm__Color;
typedef VTermGlyphInfo  *Term__VTerm__GlyphInfo;
typedef VTermLineInfo   *Term__VTerm__LineInfo;
typedef VTermPos        *Term__VTerm__Pos;
typedef VTermRect       *Term__VTerm__Rect;
typedef VTermScreenCell *Term__VTerm__Screen__Cell;

typedef struct Term__VTerm__State {
#ifdef tTHX
  tTHX myperl;
#endif
  VTermState *state;
  SV         *vterm;

  int has_selection_cb : 1;

  struct {
    CV *putglyph;
    CV *movecursor;
    CV *scrollrect;
    CV *moverect;
    CV *erase;
    CV *initpen;
    CV *setpenattr;
    CV *settermprop;
    CV *bell;
    CV *resize;
    CV *setlineinfo;

    CV *selection_set;
    CV *selection_query;
  } cb;
} *Term__VTerm__State;

typedef struct Term__VTerm__Screen {
#ifdef tTHX
  tTHX myperl;
#endif
  VTermScreen *screen;
  SV          *vterm;

  struct {
    CV *damage;
    CV *moverect;
    CV *movecursor;
    CV *settermprop;
    CV *bell;
    CV *resize;
  } cb;
} *Term__VTerm__Screen;

#ifdef HAVE_DMD_HELPER
static int dmd_helper_vterm(pTHX_ DMDContext *ctx, const SV *sv)
{
  Term__VTerm self = INT2PTR(Term__VTerm, SvIV((SV *)sv));
  int ret = 0;

  if(self->parser_cb.text)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.text, "the 'text' parser callback");
  if(self->parser_cb.control)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.control, "the 'control' parser callback");
  if(self->parser_cb.escape)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.escape, "the 'escape' parser callback");
  if(self->parser_cb.csi)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.csi, "the 'csi' parser callback");
  if(self->parser_cb.osc)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.osc, "the 'osc' parser callback");
  if(self->parser_cb.dcs)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.dcs, "the 'dcs' parser callback");
  if(self->parser_cb.resize)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->parser_cb.resize, "the 'resize' parser callback");

  if(self->strbuf)
    ret += DMD_ANNOTATE_SV(sv, self->strbuf, "the temporary string buffer");

  return ret;
}

static int dmd_helper_vterm_state(pTHX_ DMDContext *ctx, const SV *sv)
{
  Term__VTerm__State self = INT2PTR(Term__VTerm__State, SvIV((SV *)sv));
  int ret = 0;

  if(self->vterm)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->vterm, "the vterm SV");

  if(self->cb.putglyph)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.putglyph, "the 'putglyph' callback");
  if(self->cb.movecursor)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.movecursor, "the 'movecursor' callback");
  if(self->cb.scrollrect)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.scrollrect, "the 'scrollrect' callback");
  if(self->cb.moverect)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.moverect, "the 'moverect' callback");
  if(self->cb.erase)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.erase, "the 'erase' callback");
  if(self->cb.initpen)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.initpen, "the 'initpen' callback");
  if(self->cb.setpenattr)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.setpenattr, "the 'setpenattr' callback");
  if(self->cb.settermprop)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.settermprop, "the 'settermprop' callback");
  if(self->cb.bell)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.bell, "the 'bell' callback");
  if(self->cb.resize)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.resize, "the 'resize' callback");
  if(self->cb.setlineinfo)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.setlineinfo, "the 'setlineinfo' callback");

  return ret;
}

static int dmd_helper_vterm_screen(pTHX_ DMDContext *ctx, const SV *sv)
{
  Term__VTerm__Screen self = INT2PTR(Term__VTerm__Screen, SvIV((SV *)sv));
  int ret = 0;

  if(self->vterm)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->vterm, "the vterm SV");

  if(self->cb.damage)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.damage, "the 'damage' callback");
  if(self->cb.moverect)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.moverect, "the 'moverect' callback");
  if(self->cb.movecursor)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.movecursor, "the 'movecursor' callback");
  if(self->cb.settermprop)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.settermprop, "the 'settermprop' callback");
  if(self->cb.bell)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.bell, "the 'bell' callback");
  if(self->cb.resize)
    ret += DMD_ANNOTATE_SV(sv, (const SV *)self->cb.resize, "the 'resize' callback");

  return ret;
}
#endif

#define newSVcolor(col)  S_newSVcolor(aTHX_ col)
static SV *S_newSVcolor(pTHX_ VTermColor *col)
{
  VTermColor *self;
  SV *sv = newSV(0);

  Newx(self, 1, VTermColor);
  *self = *col;

  sv_setref_pv(sv, "Term::VTerm::Color", self);
  return sv;
}

#define newSVlineinfo(info)  S_newSVlineinfo(aTHX_ info)
static SV *S_newSVlineinfo(pTHX_ const VTermLineInfo *info)
{
  VTermLineInfo *self;
  SV *sv = newSV(0);

  Newx(self, 1, VTermLineInfo);
  *self = *info;

  sv_setref_pv(sv, "Term::VTerm::LineInfo", self);
  return sv;
}

#define newSVglyphinfo(info)  S_newSVglyphinfo(aTHX_ info)
static SV *S_newSVglyphinfo(pTHX_ VTermGlyphInfo *info)
{
  VTermGlyphInfo *self;
  SV *sv = newSV(0);
  int nchars, i;

  for(nchars = 0; info->chars[nchars]; nchars++)
    ;
  nchars++; // include the terminating NUL

  Newxc(self, sizeof(VTermGlyphInfo) + nchars * sizeof(uint32_t), char, VTermGlyphInfo);
  *self = *info;
  self->chars = (uint32_t *)(((char *)self) + sizeof(VTermGlyphInfo));

  for(i = 0; i < nchars; i++)
    // This is our own glyphinfo so we're allowed to write it. Honest gov
    ((uint32_t *)self->chars)[i] = info->chars[i];

  sv_setref_pv(sv, "Term::VTerm::GlyphInfo", self);
  return sv;
}

#define newSVpos(pos)  S_newSVpos(aTHX_ pos)
static SV *S_newSVpos(pTHX_ VTermPos pos)
{
  VTermPos *self;
  SV *sv = newSV(0);

  Newx(self, 1, VTermPos);
  *self = pos;

  sv_setref_pv(sv, "Term::VTerm::Pos", self);
  return sv;
}

#define newSVrect(rect)  S_newSVrect(aTHX_ rect)
static SV *S_newSVrect(pTHX_ VTermRect rect)
{
  VTermRect *self;
  SV *sv = newSV(0);

  Newx(self, 1, VTermRect);
  *self = rect;

  sv_setref_pv(sv, "Term::VTerm::Rect", self);
  return sv;
}

#define newSVscreencell(cell)  S_newSVscreencell(aTHX_ cell)
static SV *S_newSVscreencell(pTHX_ VTermScreenCell cell)
{
  VTermScreenCell *self;
  SV *sv = newSV(0);

  Newx(self, 1, VTermScreenCell);
  *self = cell;

  sv_setref_pv(sv, "Term::VTerm::Screen::Cell", self);
  return sv;
}

#define newSVvalue(val, type)  S_newSVvalue(aTHX_ val, type)
static SV *S_newSVvalue(pTHX_ VTermValue *val, VTermValueType type)
{
  switch(type) {
    case VTERM_VALUETYPE_BOOL:
      return val->boolean ? &PL_sv_yes : &PL_sv_no;
    case VTERM_VALUETYPE_INT:
      return newSViv(val->number);
    case VTERM_VALUETYPE_COLOR:
      return newSVcolor(&val->color);

    case VTERM_VALUETYPE_STRING:
      croak("ARGH should never invoke newSVvalue() on type=VTERM_VALUETYPE_STRING");
  }
}

static int parser_text(const char *bytes, size_t len, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.text;

  SV *str = newSVpv(bytes, len);
  if(vterm_get_utf8(self->vt))
    SvUTF8_on(str);

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  mPUSHs(str);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return len;
}

static int parser_control(unsigned char control, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.control;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  mPUSHi(control);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int parser_escape(const char *bytes, size_t len, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.escape;

  SV *str = newSVpv(bytes, len);

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  mPUSHs(str);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return len;
}

static int parser_csi(const char *leader, const long args[], int argcount, const char *intermed, char command, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  int i;
  CV *cb = self->parser_cb.csi;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);

  if(leader && *leader)
    mPUSHp(leader, 1);
  else
    PUSHs(&PL_sv_undef);

  mPUSHp(&command, 1);

  for(i = 0; i < argcount; i++) {
    AV *av = newAV();
    for( ; i < argcount; i++) {
      av_push(av, CSI_ARG_IS_MISSING(args[i]) ?
        &PL_sv_undef :
        newSViv(CSI_ARG(args[i])));

      if(!CSI_ARG_HAS_MORE(args[i]))
        break;
    }

    mXPUSHs(newRV((SV*)av));
  }

  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int parser_osc(int command, VTermStringFragment frag, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.osc;

  if(!cb)
    return 0;

  if(frag.initial)
    SvCUR_set(self->strbuf, 0);
  if(frag.len)
    sv_catpvn(self->strbuf, frag.str, frag.len);
  if(!frag.final)
    return 1;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(command);
  PUSHs(self->strbuf);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int parser_dcs(const char *command, size_t commandlen, VTermStringFragment frag, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.dcs;

  if(!cb)
    return 0;

  if(frag.initial)
    SvCUR_set(self->strbuf, 0);
  if(frag.len)
    sv_catpvn(self->strbuf, frag.str, frag.len);
  if(!frag.final)
    return 1;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHp(command, commandlen);
  PUSHs(self->strbuf);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int parser_resize(int rows, int cols, void *user)
{
  Term__VTerm self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->parser_cb.resize;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(rows);
  mPUSHi(cols);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;
}

static const VTermParserCallbacks parser_cbs = {
  .text    = parser_text,
  .control = parser_control,
  .escape  = parser_escape,
  .csi     = parser_csi,
  .osc     = parser_osc,
  .dcs     = parser_dcs,
  .resize  = parser_resize,
};

static int state_putglyph(VTermGlyphInfo *info, VTermPos pos, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.putglyph;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHs(newSVglyphinfo(info));
  mPUSHs(newSVpos(pos));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.movecursor;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 3);
  mPUSHs(newSVpos(pos));
  mPUSHs(newSVpos(oldpos));
  mPUSHi(visible);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_scrollrect(VTermRect rect, int downward, int rightward, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.scrollrect;
  int ret;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 3);
  mPUSHs(newSVrect(rect));
  mPUSHi(downward);
  mPUSHi(rightward);
  PUTBACK;

  call_sv((SV*)cb, G_SCALAR);

  SPAGAIN;

  ret = POPi;

  PUTBACK;
  FREETMPS;
  LEAVE;

  return ret;
}

static int state_moverect(VTermRect dest, VTermRect src, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.moverect;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHs(newSVrect(dest));
  mPUSHs(newSVrect(src));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_erase(VTermRect rect, int selective, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.erase;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHs(newSVrect(rect));
  mPUSHi(selective);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_initpen(void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.initpen;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_setpenattr(VTermAttr attr, VTermValue *val, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.setpenattr;

  if(!cb)
    return 0;

  /* pen attrs are never VTERM_VALUETYPE_STRING */

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(attr);
  mPUSHs(newSVvalue(val, vterm_get_attr_type(attr)));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_settermprop(VTermProp prop, VTermValue *val, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.settermprop;
  VTermValueType type = vterm_get_prop_type(prop);
  SV *strbuf;

  if(!cb)
    return 0;

  if(type == VTERM_VALUETYPE_STRING) {
    strbuf = (INT2PTR(Term__VTerm, SvIV(SvRV(self->vterm))))->strbuf;

    if(val->string.initial)
      SvCUR_set(strbuf, 0);
    if(val->string.len)
      sv_catpvn(strbuf, val->string.str, val->string.len);
    if(!val->string.final)
      return 1;
  }

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(prop);
  if(type == VTERM_VALUETYPE_STRING)
    PUSHs(strbuf);
  else
    mPUSHs(newSVvalue(val, type));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_bell(void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.bell;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int state_setlineinfo(int row, const VTermLineInfo *info, const VTermLineInfo *oldinfo, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.setlineinfo;
  int ret;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 3);
  mPUSHi(row);
  mPUSHs(newSVlineinfo(info));
  mPUSHs(newSVlineinfo(oldinfo));
  PUTBACK;

  call_sv((SV*)cb, G_SCALAR);

  SPAGAIN;

  ret = POPi;

  PUTBACK;
  FREETMPS;
  LEAVE;

  return ret;
}

static const VTermStateCallbacks state_cbs = {
  .putglyph    = state_putglyph,
  .movecursor  = state_movecursor,
  .scrollrect  = state_scrollrect,
  .moverect    = state_moverect,
  .erase       = state_erase,
  .initpen     = state_initpen,
  .setpenattr  = state_setpenattr,
  .settermprop = state_settermprop,
  .bell        = state_bell,
  .setlineinfo = state_setlineinfo,
};

static int state_selection_set(VTermSelectionMask mask, VTermStringFragment frag, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  SV *strbuf;
  dSP;
  CV *cb = self->cb.selection_set;
  int ret;

  if(!cb)
    return 0;

  strbuf = (INT2PTR(Term__VTerm, SvIV(SvRV(self->vterm))))->strbuf;

  if(frag.initial)
    SvCUR_set(strbuf, 0);
  if(frag.len)
    sv_catpvn(strbuf, frag.str, frag.len);
  if(!frag.final)
    return 1;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(mask);
  PUSHs(strbuf);
  PUTBACK;

  call_sv((SV*)cb, G_SCALAR);

  SPAGAIN;

  ret = POPi;

  PUTBACK;
  FREETMPS;
  LEAVE;

  return ret;
}

static int state_selection_query(VTermSelectionMask mask, void *user)
{
  Term__VTerm__State self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.selection_query;
  int ret;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  mPUSHi(mask);
  PUTBACK;

  call_sv((SV*)cb, G_SCALAR);

  SPAGAIN;

  ret = POPi;

  PUTBACK;
  FREETMPS;
  LEAVE;

  return ret;
}

static const VTermSelectionCallbacks state_selection_cbs = {
  .set   = state_selection_set,
  .query = state_selection_query,
};

static int screen_damage(VTermRect rect, void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.damage;
  SV *retsv;
  int ret;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  mPUSHs(newSVrect(rect));
  PUTBACK;

  call_sv((SV*)cb, G_SCALAR);

  SPAGAIN;

  // TODO: This can raise 'Use of uninitialised value in subroutine entry' warnings
  retsv = POPs;
  if(!SvOK(retsv))
    Perl_warn(aTHX_ "Term::VTerm::Screen on_damage callback returned undef");
  else
    ret = SvIV(retsv);

  PUTBACK;
  FREETMPS;
  LEAVE;

  return ret;
}

static int screen_moverect(VTermRect dest, VTermRect src, void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.moverect;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHs(newSVrect(dest));
  mPUSHs(newSVrect(src));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int screen_movecursor(VTermPos pos, VTermPos oldpos, int visible, void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.movecursor;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 3);
  mPUSHs(newSVpos(pos));
  mPUSHs(newSVpos(oldpos));
  mPUSHi(visible);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int screen_settermprop(VTermProp prop, VTermValue *val, void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.settermprop;
  VTermValueType type = vterm_get_prop_type(prop);
  SV *strbuf;

  if(!cb)
    return 0;

  if(type == VTERM_VALUETYPE_STRING) {
    strbuf = (INT2PTR(Term__VTerm, SvIV(SvRV(self->vterm))))->strbuf;

    if(val->string.initial)
      SvCUR_set(strbuf, 0);
    if(val->string.len)
      sv_catpvn(strbuf, val->string.str, val->string.len);
    if(!val->string.final)
      return 1;
  }

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(prop);
  if(type == VTERM_VALUETYPE_STRING)
    PUSHs(strbuf);
  else
    mPUSHs(newSVvalue(val, type));
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int screen_bell(void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.bell;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static int screen_resize(int rows, int cols, void *user)
{
  Term__VTerm__Screen self = user;
  dTHXa_FROM_SELF;
  dSP;
  CV *cb = self->cb.resize;

  if(!cb)
    return 0;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  mPUSHi(rows);
  mPUSHi(cols);
  PUTBACK;

  call_sv((SV*)cb, G_VOID);

  FREETMPS;
  LEAVE;
}

static const VTermScreenCallbacks screen_cbs = {
  .damage      = screen_damage,
  .moverect    = screen_moverect,
  .movecursor  = screen_movecursor,
  .settermprop = screen_settermprop,
  .bell        = screen_bell,
  .resize      = screen_resize,
};

static void S_setup_constants(pTHX)
{
  HV *stash  = gv_stashpvn("Term::VTerm", 11, TRUE);
  AV *export = get_av("Term::VTerm::EXPORT_OK", TRUE);

#define DO_CONSTANT(c) \
  newCONSTSUB(stash, #c+6, newSViv(c)); \
  av_push(export, newSVpv(#c+6, 0));

  DO_CONSTANT(VTERM_VALUETYPE_BOOL);
  DO_CONSTANT(VTERM_VALUETYPE_INT);
  DO_CONSTANT(VTERM_VALUETYPE_STRING);
  DO_CONSTANT(VTERM_VALUETYPE_COLOR);

  DO_CONSTANT(VTERM_ATTR_BOLD);
  DO_CONSTANT(VTERM_ATTR_UNDERLINE);
  DO_CONSTANT(VTERM_ATTR_ITALIC);
  DO_CONSTANT(VTERM_ATTR_BLINK);
  DO_CONSTANT(VTERM_ATTR_REVERSE);
  DO_CONSTANT(VTERM_ATTR_STRIKE);
  DO_CONSTANT(VTERM_ATTR_FONT);
  DO_CONSTANT(VTERM_ATTR_FOREGROUND);
  DO_CONSTANT(VTERM_ATTR_BACKGROUND);
  DO_CONSTANT(VTERM_ATTR_SMALL);
  DO_CONSTANT(VTERM_ATTR_BASELINE);

  DO_CONSTANT(VTERM_BASELINE_NORMAL);
  DO_CONSTANT(VTERM_BASELINE_RAISE);
  DO_CONSTANT(VTERM_BASELINE_LOWER);

  DO_CONSTANT(VTERM_PROP_CURSORVISIBLE);
  DO_CONSTANT(VTERM_PROP_CURSORBLINK);
  DO_CONSTANT(VTERM_PROP_ALTSCREEN);
  DO_CONSTANT(VTERM_PROP_TITLE);
  DO_CONSTANT(VTERM_PROP_ICONNAME);
  DO_CONSTANT(VTERM_PROP_REVERSE);
  DO_CONSTANT(VTERM_PROP_CURSORSHAPE);
  DO_CONSTANT(VTERM_PROP_MOUSE);

  DO_CONSTANT(VTERM_PROP_CURSORSHAPE_BLOCK);
  DO_CONSTANT(VTERM_PROP_CURSORSHAPE_UNDERLINE);
  DO_CONSTANT(VTERM_PROP_CURSORSHAPE_BAR_LEFT);

  DO_CONSTANT(VTERM_PROP_MOUSE_NONE);
  DO_CONSTANT(VTERM_PROP_MOUSE_CLICK);
  DO_CONSTANT(VTERM_PROP_MOUSE_DRAG);
  DO_CONSTANT(VTERM_PROP_MOUSE_MOVE);

  DO_CONSTANT(VTERM_MOD_SHIFT);
  DO_CONSTANT(VTERM_MOD_CTRL);
  DO_CONSTANT(VTERM_MOD_ALT);

  DO_CONSTANT(VTERM_DAMAGE_CELL);
  DO_CONSTANT(VTERM_DAMAGE_ROW);
  DO_CONSTANT(VTERM_DAMAGE_SCREEN);
  DO_CONSTANT(VTERM_DAMAGE_SCROLL);

  DO_CONSTANT(VTERM_KEY_ENTER);
  DO_CONSTANT(VTERM_KEY_TAB);
  DO_CONSTANT(VTERM_KEY_BACKSPACE);
  DO_CONSTANT(VTERM_KEY_ESCAPE);
  DO_CONSTANT(VTERM_KEY_UP);
  DO_CONSTANT(VTERM_KEY_DOWN);
  DO_CONSTANT(VTERM_KEY_LEFT);
  DO_CONSTANT(VTERM_KEY_RIGHT);
  DO_CONSTANT(VTERM_KEY_INS);
  DO_CONSTANT(VTERM_KEY_DEL);
  DO_CONSTANT(VTERM_KEY_HOME);
  DO_CONSTANT(VTERM_KEY_END);
  DO_CONSTANT(VTERM_KEY_PAGEUP);
  DO_CONSTANT(VTERM_KEY_PAGEDOWN);
  DO_CONSTANT(VTERM_KEY_FUNCTION_0);

  DO_CONSTANT(VTERM_SELECTION_CLIPBOARD);
  DO_CONSTANT(VTERM_SELECTION_PRIMARY);
  DO_CONSTANT(VTERM_SELECTION_SECONDARY);
  DO_CONSTANT(VTERM_SELECTION_SELECT);
  DO_CONSTANT(VTERM_SELECTION_CUT0);
}

MODULE = Term::VTerm        PACKAGE = Term::VTerm

Term::VTerm
_new(package,rows,cols)
  char *package
  int   rows
  int   cols
  INIT:
    VTerm *vt;
  CODE:
    vt = vterm_new(rows, cols);
    if(!vt)
      XSRETURN_UNDEF;

    Newxz(RETVAL, 1, struct Term__VTerm);
#ifdef tTHX
    RETVAL->myperl = aTHX;
#endif
    RETVAL->vt = vt;

    RETVAL->strbuf = newSV(256);
    SvPOK_on(RETVAL->strbuf);

  OUTPUT:
    RETVAL

void
DESTROY(self)
  Term::VTerm self
  INIT:
    struct ParserCallbackData *pcbdata;
  CODE:
    FREE_CB(parser_cb.text);
    FREE_CB(parser_cb.control);
    FREE_CB(parser_cb.escape);
    FREE_CB(parser_cb.csi);
    FREE_CB(parser_cb.osc);
    FREE_CB(parser_cb.dcs);
    FREE_CB(parser_cb.resize);

    SvREFCNT_dec(self->strbuf);

    vterm_free(self->vt);
    Safefree(self);

void
get_size(self)
  Term::VTerm self
  INIT:
    int rows, cols;
  PPCODE:
    vterm_get_size(self->vt, &rows, &cols);

    EXTEND(SP, 2);
    mPUSHi(rows);
    mPUSHi(cols);
    XSRETURN(2);

void
set_size(self,rows,cols)
  Term::VTerm self
  int         rows
  int         cols
  CODE:
    vterm_set_size(self->vt, rows, cols);

int
get_utf8(self)
  Term::VTerm self
  CODE:
    RETVAL = vterm_get_utf8(self->vt);
  OUTPUT:
    RETVAL

void
set_utf8(self,utf8)
  Term::VTerm self
  int         utf8
  CODE:
    vterm_set_utf8(self->vt, utf8);

size_t
input_write(self,str)
  Term::VTerm  self
  SV          *str
  CODE:
    if(SvUTF8(str))
      warn("Wide string in Term::VTerm::input_write()");
    RETVAL = vterm_input_write(self->vt, SvPV_nolen(str), SvCUR(str));
  OUTPUT:
    RETVAL

size_t
output_read(self,buffer,len)
  Term::VTerm  self
  SV          *buffer
  size_t       len
  CODE:
    sv_grow(buffer, len);
    RETVAL = vterm_output_read(self->vt, SvPVX(buffer), len);
    if(RETVAL > 0) {
      SvPOK_on(buffer);
      SvCUR_set(buffer, RETVAL);
    }
    else
      SvCUR_set(buffer, 0);
  OUTPUT:
    RETVAL

void
keyboard_unichar(self,c,mod=&PL_sv_undef)
  Term::VTerm  self
  int          c
  SV          *mod
  INIT:
    VTermModifier m = 0;
  CODE:
    if(SvOK(mod))
      m = SvIV(mod);
    m &= VTERM_MOD_SHIFT|VTERM_MOD_CTRL|VTERM_MOD_ALT;
    vterm_keyboard_unichar(self->vt, c, m);

void
keyboard_key(self,key,mod=&PL_sv_undef)
  Term::VTerm  self
  int          key
  SV          *mod
  INIT:
    VTermModifier m = 0;
  CODE:
    if(SvOK(mod))
      m = SvIV(mod);
    m &= VTERM_MOD_SHIFT|VTERM_MOD_CTRL|VTERM_MOD_ALT;
    vterm_keyboard_key(self->vt, key, m);

void
mouse_move(self,row,col,mod=&PL_sv_undef)
  Term::VTerm  self
  int          row
  int          col
  SV          *mod
  INIT:
    VTermModifier m = 0;
  CODE:
    if(SvOK(mod))
      m = SvIV(mod);
    m &= VTERM_MOD_SHIFT|VTERM_MOD_CTRL|VTERM_MOD_ALT;
    vterm_mouse_move(self->vt, row, col, m);

void
mouse_button(self,button,pressed,mod=&PL_sv_undef)
  Term::VTerm  self
  int          button
  bool         pressed
  SV          *mod
  INIT:
    VTermModifier m = 0;
  CODE:
    if(SvOK(mod))
      m = SvIV(mod);
    m &= VTERM_MOD_SHIFT|VTERM_MOD_CTRL|VTERM_MOD_ALT;
    vterm_mouse_button(self->vt, button, pressed, m);

void
parser_set_callbacks(self,...)
  Term::VTerm  self
  INIT:
    int i;
  CODE:
    vterm_parser_set_callbacks(self->vt, &parser_cbs, self);

    for(i = 1; i < items; i++) {
      char *name = SvPV_nolen(ST(i));
      SV *newcb;
      CV **cvp;
      i++;

      if     (streq(name, "on_text"   )) cvp = &self->parser_cb.text;
      else if(streq(name, "on_control")) cvp = &self->parser_cb.control;
      else if(streq(name, "on_escape"))  cvp = &self->parser_cb.escape;
      else if(streq(name, "on_csi"))     cvp = &self->parser_cb.csi;
      else if(streq(name, "on_osc"))     cvp = &self->parser_cb.osc;
      else if(streq(name, "on_dcs"))     cvp = &self->parser_cb.dcs;
      else if(streq(name, "on_resize"))  cvp = &self->parser_cb.resize;
      else
        croak("Unrecognised parser callback name '%s'", name);

      if(*cvp)
        SvREFCNT_dec(*cvp);

      if(i < items && (newcb = ST(i)) && SvOK(newcb))
        *cvp = (CV *)SvREFCNT_inc(newcb);
      else
        *cvp = NULL;
    }

Term::VTerm::State
obtain_state(self)
  Term::VTerm self
  INIT:
    VTermState *state;
  CODE:
    state = vterm_obtain_state(self->vt);
    if(!state)
      XSRETURN_UNDEF;

    Newxz(RETVAL, 1, struct Term__VTerm__State);
#ifdef tTHX
    RETVAL->myperl = aTHX;
#endif
    RETVAL->state = state;
    RETVAL->vterm = SvREFCNT_inc(ST(0));

    RETVAL->has_selection_cb = FALSE;

  OUTPUT:
    RETVAL

Term::VTerm::Screen
obtain_screen(self)
  Term::VTerm self
  INIT:
    VTermScreen *screen;
  CODE:
    screen = vterm_obtain_screen(self->vt);
    if(!screen)
      XSRETURN_UNDEF;

    Newxz(RETVAL, 1, struct Term__VTerm__Screen);
#ifdef tTHX
    RETVAL->myperl = aTHX;
#endif
    RETVAL->screen = screen;
    RETVAL->vterm  = SvREFCNT_inc(ST(0));

  OUTPUT:
    RETVAL

int
get_attr_type(attr)
  int attr
  CODE:
    RETVAL = vterm_get_attr_type(attr);
  OUTPUT:
    RETVAL

int
get_prop_type(prop)
  int prop
  CODE:
    RETVAL = vterm_get_prop_type(prop);
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::Color

SV *
_new_rgb(package,red,green,blue)
  char *package
  int   red
  int   green
  int   blue
  INIT:
    VTermColor color;
  CODE:
    vterm_color_rgb(&color, red, green, blue);
    RETVAL = newSVcolor(&color);
  OUTPUT:
    RETVAL

void
DESTROY(self)
  Term::VTerm::Color self
  CODE:
    Safefree(self);

bool
is_indexed(self)
  Term::VTerm::Color self
  ALIAS:
    is_indexed    = 0
    is_rgb        = 1
    is_default_fg = 2
    is_default_bg = 3
  CODE:
    switch(ix) {
      case 0: RETVAL = VTERM_COLOR_IS_INDEXED(self);    break;
      case 1: RETVAL = VTERM_COLOR_IS_RGB(self);        break;
      case 2: RETVAL = VTERM_COLOR_IS_DEFAULT_FG(self); break;
      case 3: RETVAL = VTERM_COLOR_IS_DEFAULT_BG(self); break;
    }
  OUTPUT:
    RETVAL

int
index(self)
  Term::VTerm::Color self
  CODE:
    if(!VTERM_COLOR_IS_INDEXED(self))
      XSRETURN_UNDEF;
    RETVAL = self->indexed.idx;
  OUTPUT:
    RETVAL

int
red(self)
  Term::VTerm::Color self
  ALIAS:
    red   = 0
    green = 1
    blue  = 2
  CODE:
    if(!VTERM_COLOR_IS_RGB(self))
      XSRETURN_UNDEF;
    switch(ix) {
      case 0: RETVAL = self->rgb.red;   break;
      case 1: RETVAL = self->rgb.green; break;
      case 2: RETVAL = self->rgb.blue;  break;
    }
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::GlyphInfo

void
DESTROY(self)
  Term::VTerm::GlyphInfo self
  CODE:
    Safefree(self);

void
chars(self)
  Term::VTerm::GlyphInfo self
  INIT:
    int i;
  PPCODE:
    for(i = 0; self->chars[i]; i++)
      mXPUSHi(self->chars[i]);
    XSRETURN(i);

SV *
str(self)
  Term::VTerm::GlyphInfo self
  CODE:
  {
    STRLEN len = 0;
    U8 *u8;
    int i;

    for(i = 0; self->chars[i]; i++)
      len += UNISKIP(self->chars[i]);

    RETVAL = newSV(len + 1);

    u8 = SvPVX(RETVAL);
    for(i = 0; self->chars[i]; i++)
      u8 = uvchr_to_utf8(u8, self->chars[i]);

    *u8 = 0;
    SvCUR_set(RETVAL, len);
    SvPOK_on(RETVAL);
    SvUTF8_on(RETVAL);
  }
  OUTPUT:
    RETVAL

int
width(self)
  Term::VTerm::GlyphInfo self
  ALIAS:
    width = 0
    dhl   = 1
  CODE:
    switch(ix) {
      case 0: RETVAL = self->width; break;
      case 1: RETVAL = self->dhl;   break;
    }
  OUTPUT:
    RETVAL

bool
protected_cell(self)
  Term::VTerm::GlyphInfo self
  ALIAS:
    protected_cell = 0
    dwl            = 1
  CODE:
    switch(ix) {
      case 0: RETVAL = self->protected_cell; break;
      case 1: RETVAL = self->dwl;            break;
    }
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::LineInfo

void
DESTROY(self)
  Term::VTerm::LineInfo self
  CODE:
    Safefree(self);

int
doublewidth(self)
  Term::VTerm::LineInfo self
  ALIAS:
    doublewidth  = 0
    doubleheight = 1
  CODE:
    switch(ix) {
      case 0: RETVAL = self->doublewidth;  break;
      case 1: RETVAL = self->doubleheight; break;
    }
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::Pos

SV *
_new(package,row,col)
  char *package
  int   row
  int   col
  INIT:
    VTermPos pos;
  CODE:
    pos.row = row;
    pos.col = col;
    RETVAL = newSVpos(pos);
  OUTPUT:
    RETVAL

void
DESTROY(self)
  Term::VTerm::Pos self
  CODE:
    Safefree(self);

int
row(self)
  Term::VTerm::Pos self
  CODE:
    RETVAL = self->row;
  OUTPUT:
    RETVAL

int
col(self)
  Term::VTerm::Pos self
  CODE:
    RETVAL = self->col;
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::Rect

SV *
_new(package,start_row,end_row,start_col,end_col)
  char *package
  int   start_row
  int   end_row
  int   start_col
  int   end_col
  INIT:
    VTermRect rect;
  CODE:
    rect.start_row = start_row;
    rect.end_row   = end_row;
    rect.start_col = start_col;
    rect.end_col   = end_col;
    RETVAL = newSVrect(rect);
  OUTPUT:
    RETVAL

void
DESTROY(self)
  Term::VTerm::Rect self
  CODE:
    Safefree(self);

int
start_row(self)
  Term::VTerm::Rect self
  ALIAS:
    start_row = 0
    end_row   = 1
    start_col = 2
    end_col   = 3
  CODE:
    switch(ix) {
      case 0: RETVAL = self->start_row; break;
      case 1: RETVAL = self->end_row;   break;
      case 2: RETVAL = self->start_col; break;
      case 3: RETVAL = self->end_col;   break;
    }
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::Screen

void
DESTROY(self)
  Term::VTerm::Screen self
  CODE:
    FREE_CB(cb.damage);
    FREE_CB(cb.moverect);
    FREE_CB(cb.movecursor);
    FREE_CB(cb.settermprop);
    FREE_CB(cb.bell);
    FREE_CB(cb.resize);

    SvREFCNT_dec(self->vterm);
    Safefree(self);

void
enable_altscreen(self,enabled)
  Term::VTerm::Screen self
  bool                enabled
  CODE:
    vterm_screen_enable_altscreen(self->screen, enabled);

void
enable_reflow(self,enabled)
  Term::VTerm::Screen self
  bool                enabled
  CODE:
    vterm_screen_enable_reflow(self->screen, enabled);

void
flush_damage(self)
  Term::VTerm::Screen self
  CODE:
    vterm_screen_flush_damage(self->screen);

void
set_damage_merge(self,size)
  Term::VTerm::Screen self
  int                 size
  CODE:
    vterm_screen_set_damage_merge(self->screen, size);

void
reset(self,hard=&PL_sv_undef)
  Term::VTerm::Screen  self
  SV                  *hard
  CODE:
    vterm_screen_reset(self->screen, SvOK(hard) ? SvIV(hard) : 0);

SV *
get_cell(self,pos)
  Term::VTerm::Screen self
  Term::VTerm::Pos    pos
  INIT:
    VTermScreenCell cell;
  CODE:
    if(!vterm_screen_get_cell(self->screen, *pos, &cell))
      XSRETURN_UNDEF;

    RETVAL = newSVscreencell(cell);
  OUTPUT:
    RETVAL

SV *
get_text(self,rect)
  Term::VTerm::Screen self
  Term::VTerm::Rect   rect
  INIT:
    size_t len;
  CODE:
    len = vterm_screen_get_text(self->screen, NULL, 0, *rect);

    RETVAL = newSV(len + 1);
    vterm_screen_get_text(self->screen, SvPVX(RETVAL), len, *rect);
    SvPVX(RETVAL)[len] = 0;

    SvCUR_set(RETVAL, len);
    SvPOK_on(RETVAL);
    SvUTF8_on(RETVAL);
  OUTPUT:
    RETVAL

void
set_callbacks(self,...)
  Term::VTerm::Screen self
  INIT:
    int i;
  CODE:
    vterm_screen_set_callbacks(self->screen, &screen_cbs, self);

    for(i = 1; i < items; i++) {
      char *name = SvPV_nolen(ST(i));
      SV *newcb;
      CV **cvp;
      i++;

      if     (streq(name, "on_damage"     )) cvp = &self->cb.damage;
      else if(streq(name, "on_moverect"   )) cvp = &self->cb.moverect;
      else if(streq(name, "on_movecursor" )) cvp = &self->cb.movecursor;
      else if(streq(name, "on_settermprop")) cvp = &self->cb.settermprop;
      else if(streq(name, "on_bell"       )) cvp = &self->cb.bell;
      else if(streq(name, "on_resize"     )) cvp = &self->cb.resize;
      else
        croak("Unrecognised screen callback name '%s'", name);

      if(*cvp)
        SvREFCNT_dec(*cvp);

      if(i < items && (newcb = ST(i)) && SvOK(newcb))
        *cvp = (CV *)SvREFCNT_inc(newcb);
      else
        *cvp = NULL;
    }

SV *
convert_color_to_rgb(self,col)
  Term::VTerm::Screen self
  Term::VTerm::Color  col
  CODE:
    vterm_screen_convert_color_to_rgb(self->screen, col);
    RETVAL = newSVcolor(col);
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::Screen::Cell

void
DESTROY(self)
  Term::VTerm::Screen::Cell self
  CODE:
    Safefree(self);

void
chars(self)
  Term::VTerm::Screen::Cell self
  INIT:
    int i;
  PPCODE:
    for(i = 0; self->chars[i]; i++)
      mXPUSHi(self->chars[i]);
    XSRETURN(i);

SV *
str(self)
  Term::VTerm::Screen::Cell self
  CODE:
  {
    STRLEN len = 0;
    U8 *u8;
    int i;

    for(i = 0; self->chars[i]; i++)
      len += UNISKIP(self->chars[i]);

    RETVAL = newSV(len + 1);

    u8 = SvPVX(RETVAL);
    for(i = 0; self->chars[i]; i++)
      u8 = uvchr_to_utf8(u8, self->chars[i]);

    *u8 = 0;
    SvCUR_set(RETVAL, len);
    SvPOK_on(RETVAL);
    SvUTF8_on(RETVAL);
  }
  OUTPUT:
    RETVAL

int
width(self)
  Term::VTerm::Screen::Cell self
  ALIAS:
    width     = 0
    underline = 1
    font      = 2
    baseline  = 3
  CODE:
    switch(ix) {
      case 0: RETVAL = self->width;           break;
      case 1: RETVAL = self->attrs.underline; break;
      case 2: RETVAL = self->attrs.font;      break;
      case 3: RETVAL = self->attrs.baseline;  break;
    }
  OUTPUT:
    RETVAL

bool
bold(self)
  Term::VTerm::Screen::Cell self
  ALIAS:
    bold    = 0
    italic  = 1
    blink   = 2
    reverse = 3
    strike  = 4
    small   = 5
  CODE:
    switch(ix) {
      case 0: RETVAL = self->attrs.bold;    break;
      case 1: RETVAL = self->attrs.italic;  break;
      case 2: RETVAL = self->attrs.blink;   break;
      case 3: RETVAL = self->attrs.reverse; break;
      case 4: RETVAL = self->attrs.strike;  break;
      case 5: RETVAL = self->attrs.small;   break;
    }
  OUTPUT:
    RETVAL

SV *
fg(self)
  Term::VTerm::Screen::Cell self
  ALIAS:
    fg = 0
    bg = 1
  CODE:
    switch(ix) {
      case 0: RETVAL = newSVcolor(&self->fg); break;
      case 1: RETVAL = newSVcolor(&self->bg); break;
    }
  OUTPUT:
    RETVAL


MODULE = Term::VTerm        PACKAGE = Term::VTerm::State

void
DESTROY(self)
  Term::VTerm::State self
  CODE:
    FREE_CB(cb.putglyph);
    FREE_CB(cb.movecursor);
    FREE_CB(cb.scrollrect);
    FREE_CB(cb.moverect);
    FREE_CB(cb.erase);
    FREE_CB(cb.initpen);
    FREE_CB(cb.setpenattr);
    FREE_CB(cb.settermprop);
    FREE_CB(cb.bell);
    FREE_CB(cb.resize);
    FREE_CB(cb.setlineinfo);

    SvREFCNT_dec(self->vterm);
    Safefree(self);

void
reset(self,hard=&PL_sv_undef)
  Term::VTerm::State  self
  SV                 *hard
  CODE:
    vterm_state_reset(self->state, SvOK(hard) ? SvIV(hard) : 0);

Term::VTerm::Pos
get_cursorpos(self)
  Term::VTerm::State self
  CODE:
    Newx(RETVAL, 1, VTermPos);
    vterm_state_get_cursorpos(self->state, RETVAL);
  OUTPUT:
    RETVAL

void
get_default_colors(self)
  Term::VTerm::State self
  INIT:
    VTermColor fg, bg;
  PPCODE:
    vterm_state_get_default_colors(self->state, &fg, &bg);
    EXTEND(SP, 2);
    mPUSHs(newSVcolor(&fg));
    mPUSHs(newSVcolor(&bg));
    XSRETURN(2);

void
set_default_colors(self,fg,bg)
  Term::VTerm::State self
  Term::VTerm::Color fg
  Term::VTerm::Color bg
  CODE:
    vterm_state_set_default_colors(self->state, fg, bg);

SV *
get_palette_color(self,index)
  Term::VTerm::State self
  int                index
  INIT:
    VTermColor col;
  CODE:
    vterm_state_get_palette_color(self->state, index, &col);
    RETVAL = newSVcolor(&col);
  OUTPUT:
    RETVAL

SV *
get_penattr(self,attr)
  Term::VTerm::State self
  int                attr
  INIT:
    VTermValue val;
  CODE:
    vterm_state_get_penattr(self->state, attr, &val);
    RETVAL = newSVvalue(&val, vterm_get_attr_type(attr));
  OUTPUT:
    RETVAL

void
set_callbacks(self,...)
  Term::VTerm::State self
  INIT:
    int i;
  CODE:
    vterm_state_set_callbacks(self->state, &state_cbs, self);

    for(i = 1; i < items; i++) {
      char *name = SvPV_nolen(ST(i));
      SV *newcb;
      CV **cvp;
      i++;

      if     (streq(name, "on_putglyph"   )) cvp = &self->cb.putglyph;
      else if(streq(name, "on_movecursor" )) cvp = &self->cb.movecursor;
      else if(streq(name, "on_scrollrect" )) cvp = &self->cb.scrollrect;
      else if(streq(name, "on_moverect"   )) cvp = &self->cb.moverect;
      else if(streq(name, "on_erase"      )) cvp = &self->cb.erase;
      else if(streq(name, "on_initpen"    )) cvp = &self->cb.initpen;
      else if(streq(name, "on_setpenattr" )) cvp = &self->cb.setpenattr;
      else if(streq(name, "on_settermprop")) cvp = &self->cb.settermprop;
      else if(streq(name, "on_bell"       )) cvp = &self->cb.bell;
      else if(streq(name, "on_resize"     )) cvp = &self->cb.resize;
      else if(streq(name, "on_setlineinfo")) cvp = &self->cb.setlineinfo;
      else
        croak("Unrecognised state callback name '%s'", name);

      if(*cvp)
        SvREFCNT_dec(*cvp);

      if(i < items && (newcb = ST(i)) && SvOK(newcb))
        *cvp = (CV *)SvREFCNT_inc(newcb);
      else
        *cvp = NULL;
    }

void
set_selection_callbacks(self,...)
  Term::VTerm::State self
  INIT:
    int i;
  CODE:
    /* TODO: argument to set buffer size? */
    if(!self->has_selection_cb) {
      vterm_state_set_selection_callbacks(self->state, &state_selection_cbs, self,
        NULL, 4096);
      self->has_selection_cb = TRUE;
    }

    for(i = 1; i < items; i++) {
      char *name = SvPV_nolen(ST(i));
      SV *newcb;
      CV **cvp;
      i++;

      if     (streq(name, "on_set"  )) cvp = &self->cb.selection_set;
      else if(streq(name, "on_query")) cvp = &self->cb.selection_query;
      else
        croak("Unrecognised state callback name '%s'", name);

      if(*cvp)
        SvREFCNT_dec(*cvp);

      if(i < items && (newcb = ST(i)) && SvOK(newcb))
        *cvp = (CV *)SvREFCNT_inc(newcb);
      else
        *cvp = NULL;
    }

void
send_selection(self,mask,str)
  Term::VTerm::State self
  int mask
  SV *str
  INIT:
    STRLEN len;
    VTermStringFragment frag;
  CODE:
    frag = (VTermStringFragment){
      .str     = SvPVbyte(str, len),
      .initial = TRUE,
      .final   = TRUE,
    };
    frag.len = len;

    vterm_state_send_selection(self->state, mask, frag);

SV *
convert_color_to_rgb(self,col)
  Term::VTerm::State self
  Term::VTerm::Color col
  CODE:
    vterm_state_convert_color_to_rgb(self->state, col);
    RETVAL = newSVcolor(col);
  OUTPUT:
    RETVAL


BOOT:
  S_setup_constants(aTHX);
#ifdef HAVE_DMD_HELPER
  DMD_SET_PACKAGE_HELPER("Term::VTerm",         dmd_helper_vterm);
  DMD_SET_PACKAGE_HELPER("Term::VTerm::State",  dmd_helper_vterm_state);
  DMD_SET_PACKAGE_HELPER("Term::VTerm::Screen", dmd_helper_vterm_screen);
#endif