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

#include "AsyncAwait.h"

#include "XSParseKeyword.h"

#include "perl-backcompat.c.inc"
#include "forbid_outofblock_ops.c.inc"
#include "newOP_CUSTOM.c.inc"

enum {
  HOOK_SUSPEND = 1,
  HOOK_RESUME,
};

static void S_call_block_noargs(pTHX_ SV *blocksv)
{
  OP *start = NUM2PTR(OP *, SvUV(blocksv));
  I32 was_cxstack_ix = cxstack_ix;

  cx_pushblock(CXt_BLOCK, G_VOID, PL_stack_sp, PL_savestack_ix);
  ENTER;
  SAVETMPS;

  SAVEOP();
  PL_op = start;
  CALLRUNOPS(aTHX);

  FREETMPS;
  LEAVE;

  if(cxstack_ix != was_cxstack_ix + 1) {
    croak("panic: A non-local control flow operation exited a suspend/resume block");
  }

  PERL_CONTEXT *cx = CX_CUR();
  PL_stack_sp = PL_stack_base + cx->blk_oldsp;

  dounwind(was_cxstack_ix);
}
#define call_block_noargs(blocksv)  S_call_block_noargs(aTHX_ blocksv)

static void hook_pre_suspend(pTHX_ CV *cv, HV *modhookdata, void *hookdata)
{
  SV **svp = hv_fetchs(modhookdata, "Future::AsyncAwait::Hooks/hooklist", 0);
  if(!svp)
    return;

  AV *hooklist = (AV *)*svp;
  svp = AvARRAY(hooklist);

  I32 i;
  for(i = 0; i <= AvFILL(hooklist); i += 2) {
    int type = SvIV(svp[i]);
    if(type == HOOK_SUSPEND)
      call_block_noargs(svp[i+1]);
  }
}

static void hook_post_resume(pTHX_ CV *cv, HV *modhookdata, void *hookdata)
{
  SV **svp = hv_fetchs(modhookdata, "Future::AsyncAwait::Hooks/hooklist", 0);
  if(!svp)
    return;

  AV *hooklist = (AV *)*svp;
  svp = AvARRAY(hooklist);

  I32 i;
  for(i = AvFILL(hooklist)-1; i >= 0; i -= 2) {
    int type = SvIV(svp[i]);
    if(type == HOOK_RESUME)
      call_block_noargs(svp[i+1]);
  }
}

static const struct AsyncAwaitHookFuncs faa_hooks = {
  .pre_suspend = &hook_pre_suspend,
  .post_resume = &hook_post_resume,
};

#define SAVEAVLEN(av)  S_save_avlen(aTHX_ av)
/* This would be a lot neater if perl had a SAVEFUNCANY2() */
struct AvWithLength {
  AV *av;
  U32 len;
};
void restore_av_len(pTHX_ void *_avl)
{
  struct AvWithLength *avl = _avl;
  AV *av = avl->av;

  while(av_count(av) > avl->len)
    SvREFCNT_dec(av_pop(av));

  Safefree(avl);
}
static void S_save_avlen(pTHX_ AV *av)
{
  struct AvWithLength *avl;
  Newx(avl, 1, struct AvWithLength);

  avl->av = av;
  avl->len = av_count(av);

  SAVEDESTRUCTOR_X(restore_av_len, avl);
}

static OP *pp_pushhook(pTHX)
{
  OP *blockstart = cLOGOP->op_other;
  int type = PL_op->op_private;

  HV *modhookdata = future_asyncawait_get_modhookdata(find_runcv(0), FAA_MODHOOK_CREATE, PL_op->op_targ);
  if(!modhookdata)
    croak("panic: expected modhookdata");

  AV *hooklist;
  SV **svp = hv_fetchs(modhookdata, "Future::AsyncAwait::Hooks/hooklist", 0);
  if(svp)
    hooklist = (AV *)*svp;
  else
    hv_stores(modhookdata, "Future::AsyncAwait::Hooks/hooklist", (SV *)(hooklist = newAV()));

  SAVEAVLEN(hooklist);

  av_push(hooklist, newSViv(type));
  /* We can't push an OP * to the AV, but we can wrap it */
  av_push(hooklist, newSVuv(PTR2UV(blockstart)));

  return PL_op->op_next;
}

static OP *build_pushhook_op(pTHX_ OP *block, int phase, const char *name)
{
  forbid_outofblock_ops(block, name);

  OP *o = newLOGOP_CUSTOM(&pp_pushhook, 0,
    newOP(OP_NULL, 0), block);

  /* The actual pp_pushhook LOGOP is the op_first of o */
  LOGOP *pushhooko = (LOGOP *)cUNOPo->op_first;
  pushhooko->op_targ = future_asyncawait_make_precreate_padix();
  pushhooko->op_private = phase;

  /* ensure the block will terminate properly */
  block->op_next = NULL;

  return o;
}

static int build_suspend(pTHX_ OP **out, XSParseKeywordPiece *arg0, void *hookdata)
{
  OP *block = arg0->op;

  *out = build_pushhook_op(aTHX_ block, HOOK_SUSPEND, "a suspend block");

  return KEYWORD_PLUGIN_STMT;
}

static int build_resume(pTHX_ OP **out, XSParseKeywordPiece *arg0, void *hookdata)
{
  OP *block = arg0->op;

  *out = build_pushhook_op(aTHX_ block, HOOK_RESUME, "a resume block");

  return KEYWORD_PLUGIN_STMT;
}

static const struct XSParseKeywordHooks hooks_suspend = {
  .permit_hintkey = "Future::AsyncAwait::Hooks/suspend",
  .piece1 = XPK_BLOCK,
  .build1 = &build_suspend,
};

static const struct XSParseKeywordHooks hooks_resume = {
  .permit_hintkey = "Future::AsyncAwait::Hooks/resume",
  .piece1 = XPK_BLOCK,
  .build1 = &build_resume,
};

MODULE = Future::AsyncAwait::Hooks    PACKAGE = Future::AsyncAwait::Hooks

BOOT:
  boot_future_asyncawait(0.64);
  boot_xs_parse_keyword(0.13);

  register_future_asyncawait_hook(&faa_hooks, NULL);

  register_xs_parse_keyword("suspend", &hooks_suspend, NULL);
  register_xs_parse_keyword("resume",  &hooks_resume,  NULL);