/*  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, 2021 -- leonerd@leonerd.org.uk
 */
#include "EXTERN.h"
#include "perl.h"
#include "XSUB.h"

#include "XSParseSublike.h"

#include "perl-backcompat.c.inc"

#include "newOP_CUSTOM.c.inc"

#if HAVE_PERL_VERSION(5, 43, 3)
#  define HAVE_OP_MULTIPARAM
#endif

struct MultiSubOption {
  int args_min, args_max;
  CV *cv;
};

#define get_optionsav(cv, padix)  S_get_optionsav(aTHX_ cv, padix)
static AV *S_get_optionsav(pTHX_ CV *cv, PADOFFSET padix)
{
  PADLIST *pl = CvPADLIST(cv);
  AV *optionsav = (AV *)PadARRAY(PadlistARRAY(pl)[1])[padix];
  return optionsav;
}

static OP *pp_dispatch_multisub(pTHX)
{
  IV nargs = av_count(GvAV(PL_defgv));
  CV *runcv = find_runcv(0);
  AV *optionsav = get_optionsav(runcv, PL_op->op_targ);

  CV *jumpcv = NULL;

  IV noptions = av_count(optionsav);
  IV optioni;
  for(optioni = 0; optioni < noptions; optioni++) {
    struct MultiSubOption *option = (struct MultiSubOption *)AvARRAY(optionsav)[optioni];

    if(nargs < option->args_min)
      continue;
    if(option->args_max > -1 && nargs > option->args_max)
      continue;

    jumpcv = option->cv;
    break;
  }

  if(!jumpcv)
    croak("Unable to find a function body for a call to &%s::%s having %d arguments",
      HvNAME(CvSTASH(runcv)), GvNAME(CvGV(runcv)), nargs);

  /* Now pretend to be  goto &$cv
   * Reuse the same PL_op structure and just call that ppfunc */
  assert(PL_op->op_flags & OPf_STACKED);
  dSP;
  mPUSHs(newRV_inc((SV *)jumpcv));
  PUTBACK;
  assert(SvROK(TOPs) && SvTYPE(SvRV(TOPs)) == SVt_PVCV);
  return (PL_ppaddr[OP_GOTO])(aTHX);
}

/* XSParseSublikeContext moddata keys */
#define MODDATA_KEY_NAME        "Syntax::Keyword::MultiSub/name"
#define MODDATA_KEY_COMPMULTICV "Syntax::Keyword::MultiSub/compmulticv"

static void parse_pre_subparse(pTHX_ struct XSParseSublikeContext *ctx, void *hookdata)
{
  SV *name = ctx->name;

  CV *multicv = get_cvn_flags(SvPVX(name), SvCUR(name), SvUTF8(name) ? SVf_UTF8 : 0);
  if(!multicv) {
    ENTER;

    I32 floor_ix = start_subparse(FALSE, 0);
    SAVEFREESV(PL_compcv);

    I32 save_ix = block_start(TRUE);

    PADOFFSET padix = pad_add_name_pvs("@(Syntax::Keyword::MultiSub/options)", 0, NULL, NULL);
    intro_my();

    OP *dispatchop = newOP_CUSTOM(&pp_dispatch_multisub, OPf_STACKED);
    dispatchop->op_targ = padix;

    OP *body = block_end(save_ix, dispatchop);

    SvREFCNT_inc(PL_compcv);

    multicv = newATTRSUB(floor_ix, newSVOP(OP_CONST, 0, SvREFCNT_inc(name)), NULL, NULL, body);

    LEAVE;
  }

  hv_stores(ctx->moddata, MODDATA_KEY_NAME,        SvREFCNT_inc(name));
  hv_stores(ctx->moddata, MODDATA_KEY_COMPMULTICV, SvREFCNT_inc(multicv));

  /* Do not let this sub be installed as a named symbol */
  ctx->actions &= ~XS_PARSE_SUBLIKE_ACTION_INSTALL_SYMBOL;
}

static void parse_post_newcv(pTHX_ struct XSParseSublikeContext *ctx, void *hookdata)
{
  CV *cv = ctx->cv;
  if(!cv)
    return;

  SV *name    =       *hv_fetchs(ctx->moddata, MODDATA_KEY_NAME, 0);
  CV *multicv = (CV *)*hv_fetchs(ctx->moddata, MODDATA_KEY_COMPMULTICV, 0);

  PADNAMELIST *pln = PadlistNAMES(CvPADLIST(multicv));
  /* We can't use pad_findmy_pvn() because it gets upset about seqnums */
  PADOFFSET padix;
  for(padix = 1; padix <= PadnamelistMAX(pln); padix++)
    if(strEQ(PadnamePV(PadnamelistARRAY(pln)[padix]), "@(Syntax::Keyword::MultiSub/options)"))
      break;
  assert(padix <= PadnamelistMAX(pln));

  AV *optionsav = get_optionsav(multicv, padix);
  bool final_is_slurpy = av_count(optionsav) &&
    (((struct MultiSubOption *)AvARRAY(optionsav)[AvFILL(optionsav)])->args_max == -1);

  int args_min, args_max;

  OP *o = CvSTART(cv);
  while(o) {
redo:
    switch(o->op_type) {
      case OP_NEXTSTATE:
      case OP_DBSTATE:
        o = o->op_next;
        goto redo;

      case OP_ARGCHECK: {
#if HAVE_PERL_VERSION(5, 31, 5)
        struct op_argcheck_aux *aux = (struct op_argcheck_aux *)cUNOP_AUXo->op_aux;
        char slurpy = aux->slurpy;
        args_max = aux->params;
        args_min = args_max - aux->opt_params;
#else
        UNOP_AUX_item *aux = cUNOP_AUXo->op_aux;
        char slurpy = aux[2].iv;

        args_max = aux[0].iv;
        args_min = args_max - aux[1].iv;
#endif
        if(slurpy)
          args_max = -1;
        goto done;
      }

#ifdef HAVE_OP_MULTIPARAM
      case OP_MULTIPARAM: {
        struct op_multiparam_aux *aux = (struct op_multiparam_aux *)cUNOP_AUXo->op_aux;
        args_min = aux->min_args;
        args_max = aux->n_positional;
        if(aux->slurpy)
          args_max = -1;
        goto done;
      }
#endif

      default:
        croak("TODO: Unsure how to find argcheck op within %s", PL_op_name[o->op_type]);
    }
  }
done: ;

  if(final_is_slurpy && args_max == -1)
    croak("Already have a slurpy function body for multi sub %" SVf, name);

  IV noptions = av_count(optionsav);
  IV optioni;
  for(optioni = 0; optioni < noptions; optioni++) {
    struct MultiSubOption *option = (struct MultiSubOption *)AvARRAY(optionsav)[optioni];

    if(option->args_max == -1 || args_min > option->args_max)
      continue;
    if(args_max < option->args_min)
      continue;

    croak("Ambiguous argument count for multi sub %" SVf, name);
  }

  struct MultiSubOption *option;
  Newx(option, 1, struct MultiSubOption);

  option->args_min = args_min;
  option->args_max = args_max;
  option->cv       = cv_clone(cv); /* Because it is currently a protosub */

  av_push(optionsav, (SV *)option);
}

static struct XSParseSublikeHooks hooks_multi = {
  .permit_hintkey = "Syntax::Keyword::MultiSub/multi",
  .flags          = XS_PARSE_SUBLIKE_FLAG_PREFIX|XS_PARSE_SUBLIKE_COMPAT_FLAG_DYNAMIC_ACTIONS,
  .require_parts  = XS_PARSE_SUBLIKE_PART_NAME,
  .pre_subparse   = parse_pre_subparse,
  .post_newcv     = parse_post_newcv,
};

MODULE = Syntax::Keyword::MultiSub    PACKAGE = Syntax::Keyword::MultiSub

BOOT:
  boot_xs_parse_sublike(0.15);

  register_xs_parse_sublike("multi", &hooks_multi, NULL);