/*  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, 2022-2023 -- leonerd@leonerd.org.uk
 */
#define PERL_NO_GET_CONTEXT

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

#include "XSParseKeyword.h"
#include "object_pad.h"

#ifndef newSVsv_nomg
static SV *S_newSVsv_nomg(pTHX_ SV *osv)
{
  SV *nsv = newSV(0);
  sv_setsv_nomg(nsv, osv);
  return nsv;
}

#  define newSVsv_nomg(osv)  S_newSVsv_nomg(aTHX_ (osv))
#endif

struct AccessorCtx {
  CV *getcv;
  CV *setcv;
};

static int accessor_magic_get(pTHX_ SV *sv, MAGIC *mg)
{
  struct AccessorCtx *ctx = (struct AccessorCtx *)mg->mg_ptr;
  SV *self = mg->mg_obj;

  dSP;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 1);
  PUSHs(self);
  PUTBACK;

  int count = call_sv((SV *)ctx->getcv, G_SCALAR);
  PERL_UNUSED_VAR(count);
  assert(count == 1);

  SPAGAIN;

  sv_setsv_nomg(sv, POPs);

  PUTBACK;
  FREETMPS;
  LEAVE;

  return 1;
}

static int accessor_magic_set(pTHX_ SV *sv, MAGIC *mg)
{
  struct AccessorCtx *ctx = (struct AccessorCtx *)mg->mg_ptr;
  SV *self = mg->mg_obj;

  dSP;

  ENTER;
  SAVETMPS;

  PUSHMARK(SP);
  EXTEND(SP, 2);
  PUSHs(self);
  mPUSHs(newSVsv_nomg(sv));
  PUTBACK;

  call_sv((SV *)ctx->setcv, G_VOID);

  FREETMPS;
  LEAVE;

  return 1;
}

static MGVTBL vtbl_accessor = {
  .svt_get = accessor_magic_get,
  .svt_set = accessor_magic_set,
};

XS_INTERNAL(make_accessor_lvalue)
{
  dXSARGS;

  if(items < 1 || items > 1)
    croak("Usage: $self->accessor");
  SP -= items;

  SV *self = ST(0);

  SV *retval = sv_newmortal();
  sv_magicext(retval, SvREFCNT_inc(self), PERL_MAGIC_ext, &vtbl_accessor, XSANY.any_ptr, 0);

  ST(0) = retval;

  XSRETURN(1);
}

enum {
  PART_GET = 1,
  PART_SET,
};

static int build_accessor(pTHX_ OP **out, XSParseKeywordPiece *args[], size_t nargs, void *hookdata)
{
  int argi = 0;

  SV *name = args[argi++]->sv;

  ClassMeta *classmeta = get_compclassmeta();

  struct AccessorCtx *ctx;
  Newxz(ctx, 1, struct AccessorCtx);

  int nparts = args[argi++]->i;
  for(int parti = 0; parti < nparts; parti++) {
    int parttype = args[argi++]->i;
    switch(parttype) {
      case PART_GET:
        if(ctx->getcv)
          croak("Cannot provide two 'get' blocks for %" SVf " accessor", SVfARG(name));
        ctx->getcv = cv_clone((CV *)args[argi++]->sv);
        assert(SvTYPE(ctx->getcv) == SVt_PVCV);
        break;

      case PART_SET:
        if(ctx->setcv)
          croak("Cannot provide two 'set' blocks for %" SVf " accessor", SVfARG(name));
        ctx->setcv = cv_clone((CV *)args[argi++]->sv);
        assert(SvTYPE(ctx->setcv) == SVt_PVCV);
        break;

      default:
        croak("TODO: Handle part type %d", parttype);
    }
  }

  /* Sanity checking */
  if(!ctx->getcv)
    croak("accessor needs a 'get' stage");
  if(!ctx->setcv)
    croak("accessor needs a 'set' stage");

  CV *cv = newXS(NULL, make_accessor_lvalue, __FILE__);
  CvMETHOD_on(cv);
  CvLVALUE_on(cv);
  CvXSUBANY(cv).any_ptr = ctx;

  mop_class_add_method_cv(classmeta, name, cv);

  return KEYWORD_PLUGIN_STMT;
}

/* stolen from perl-additions.c.inc */
#define lex_consume_unichar(c)  MY_lex_consume_unichar(aTHX_ c)
static bool MY_lex_consume_unichar(pTHX_ U32 c)
{
  if(lex_peek_unichar(0) != c)
    return FALSE;

  lex_read_unichar(0);
  return TRUE;
}

#define HINTKEY_PADIX  "Object::Pad::Keyword::Accessor/var-padix"

static void anonmethod_set_start(pTHX_ void *hookdata)
{
  if(!lex_consume_unichar('('))
    return;
  lex_read_space(0);

  char *name = PL_parser->bufptr;

  if(lex_read_unichar(0) != '$')
    croak("Expected a scalar lexical name");

  U32 c;
  if(!(c = lex_read_unichar(0)) || !isIDFIRST_uni(c))
    croak("Expected a scalar lexical name");
  while((c = lex_peek_unichar(0)) && isIDCONT_uni(c))
    lex_read_unichar(0);

  STRLEN namelen = PL_parser->bufptr - name;

  if(namelen == 2 && name[1] == '_')
    croak("Can't use global $_ in \"my\"");

  PADOFFSET padix = pad_add_name_pvn(name, namelen, 0, NULL, NULL);
  hv_stores(GvHV(PL_hintgv), HINTKEY_PADIX, newSVuv(padix));

  if(!lex_consume_unichar(')'))
    croak("Expected ')'");

  intro_my();
}

static OP *anonmethod_set_end(pTHX_ OP *o, void *hookdata)
{
  SV **svp = hv_fetchs(GvHV(PL_hintgv), HINTKEY_PADIX, 0);
  if(!svp)
    return o;

  /* $var = $_[0]; */
  OP *padsvop;
  OP *setupop = newBINOP(OP_SASSIGN, 0,
    newGVOP(OP_AELEMFAST, 0 << 8, PL_defgv),
    padsvop = newOP(OP_PADSV, 0));
  padsvop->op_targ = SvUV(*svp);

  o = op_append_elem(OP_LINESEQ, setupop, o);

  return o;
}

static const struct XSParseKeywordHooks kwhooks_accessor = {
  .permit_hintkey = "Object::Pad::Keyword::Accessor",

  .pieces = (const struct XSParseKeywordPieceType []) {
    XPK_IDENT,
    XPK_BRACES(
      XPK_REPEATED(
        XPK_TAGGEDCHOICE(
          /* A `get` block is just a regular anon method */
          XPK_SEQUENCE(XPK_KEYWORD("get"), OPXPK_ANONMETHOD),
            XPK_TAG(PART_GET),
          /* A `set` block requires special parsing of the "($var)" syntax */
          XPK_SEQUENCE(XPK_KEYWORD("set"), XPK_STAGED_ANONSUB(
              OPXPK_ANONMETHOD_PREPARE,
              OPXPK_ANONMETHOD_START,
              /* TODO: This is rather hacky; using a code block to do some
               * parsing. Ideally we'd like to use
               *   XPK_PARENS(XPK_LEXVAR_MY(XPK_LEXVAR_SCALAR))
               * for it, but that leaves us not knowing the padix for the new
               * variable when we come to END+WRAP the method into a CV. We'd
               * need some way to interrupt and put more code in there.
               * Somehow.
               */
              XPK_ANONSUB_START(&anonmethod_set_start),
              XPK_ANONSUB_END(&anonmethod_set_end),
              OPXPK_ANONMETHOD_WRAP)),
            XPK_TAG(PART_SET)
        )
      )
    ),
    {0}
  },
  .build = &build_accessor,
};

MODULE = Object::Pad::Keyword::Accessor    PACKAGE = Object::Pad::Keyword::Accessor

BOOT:
  boot_xs_parse_keyword(0.35);

  /* TODO: Consider if this needs to be done via O:P directly */
  register_xs_parse_keyword("accessor", &kwhooks_accessor, NULL);