/*  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"

#define HAVE_PERL_VERSION(R, V, S) \
    (PERL_REVISION > (R) || (PERL_REVISION == (R) && (PERL_VERSION > (V) || (PERL_VERSION == (V) && (PERL_SUBVERSION >= (S))))))

#include "perl-backcompat.c.inc"

#include "newOP_CUSTOM.c.inc"
#include "BINOP_ANY.c.inc"

#include "XSParseInfix.h"

enum Inop_Operator {
  INOP_CUSTOM,
  INOP_NUMBER,
  INOP_STRING,
};

static OP *pp_in(pTHX)
{
  dSP;
  dMARK;
  SV **svp;
  enum Inop_Operator type = PL_op->op_private;

  OP cmpop;
  switch(type) {
    case INOP_CUSTOM:
    {
      ANY *op_any = cBINOP_ANY->op_any;
      cmpop.op_type = OP_CUSTOM;
      cmpop.op_flags = 0;
      cmpop.op_ppaddr = op_any[0].any_ptr;
      break;
    }

    case INOP_NUMBER:
      cmpop.op_type = OP_EQ;
      cmpop.op_flags = 0;
      cmpop.op_ppaddr = PL_ppaddr[OP_EQ];
      break;

    case INOP_STRING:
      cmpop.op_type = OP_SEQ;
      cmpop.op_flags = 0;
      cmpop.op_ppaddr = PL_ppaddr[OP_SEQ];
      break;
  }

  SV *lhs = *MARK;
  SV **listend = SP;

  SP = MARK - 1;
  PUTBACK;

  ENTER;
  SAVEVPTR(PL_op);
  PL_op = &cmpop;
  EXTEND(SP, 2);

  for(svp = MARK + 1; svp <= listend; svp++) {
    SV *rhs = *svp;

    PUSHs(lhs);
    PUSHs(rhs);
    PUTBACK;

    (*cmpop.op_ppaddr)(aTHX);

    SPAGAIN;

    SV *ret = POPs;

    if(SvTRUE(ret)) {
      LEAVE;

      PUSHs(&PL_sv_yes);
      RETURN;
    }
  }

  LEAVE;

  PUSHs(&PL_sv_no);
  RETURN;
}

#ifndef isIDCONT_utf8_safe
/* It doesn't really matter that this is not "safe", because the function is
 * only ever called on perls new enough to have PL_infix_plugin, and in that
 * case they'll have the _safe version anyway
 */
#  define isIDCONT_utf8_safe(s, e)  isIDCONT_utf8(s)
#endif

static void parse_in(pTHX_ U32 flags, SV **parsedata, void *hookdata)
{
  bool using_circumfix = false;
  if(lex_peek_unichar(0) == '<')
    using_circumfix = true;
  else if(lex_peek_unichar(0) != ':')
    croak("Expected ':' or '<'");
  lex_read_unichar(0);

  lex_read_space(0);

  struct XSParseInfixInfo *info;
  if(!parse_infix(XPI_SELECT_ANY, &info))
    croak("Expected an equality test operator");
  if(info->cls != XPI_CLS_EQUALITY)
    croak("The %s operator is not permitted for the in: meta-operator (cls=%d)", info->opname, info->cls);

  /* parsedata will be an AV containing
   *  [0] IV = enum Inop_Operator
   *  [1] UV = PTR to pp_addr if CUSTOM
   */
  AV *parsedata_av = newAV();
  *parsedata = newRV_noinc((SV *)parsedata_av);

  /* See if we got one of the core ones */
  if(info->opcode == OP_EQ) {
    av_push(parsedata_av, newSViv(INOP_NUMBER));
  }
  else if(info->opcode == OP_SEQ) {
    av_push(parsedata_av, newSViv(INOP_STRING));
  }
  else if(info->opcode == OP_CUSTOM) {
    if(info->hooks->new_op)
      croak("TODO: handle custom op using the new_op function for '%s'", info->opname);

    av_push(parsedata_av, newSViv(INOP_CUSTOM));
    av_push(parsedata_av, newSVuv(PTR2UV(info->hooks->ppaddr)));
  }
  else
    croak("Expected an equality test operator name but found '%s'", info->opname);

  if(using_circumfix) {
    if(lex_peek_unichar(0) != '>')
      croak("Expected '>'");
    lex_read_unichar(0);
  }
}

static OP *newop_in(pTHX_ U32 flags, OP *lhs, OP *rhs, SV **parsedata, void *hookdata)
{
  AV *parsedata_av = AV_FROM_REF(*parsedata);

  enum Inop_Operator operator = SvIV(AvARRAY(parsedata_av)[0]);

  OP *ret;
  switch(operator) {
    case INOP_CUSTOM:
      ret = newBINOP_ANY_CUSTOM(&pp_in, 0, lhs, rhs, 1);
      cBINOP_ANYx(ret)->op_any[0].any_ptr = INT2PTR(void *, SvUV(AvARRAY(parsedata_av)[1]));
      ret->op_private = INOP_CUSTOM;
      break;

    case INOP_NUMBER:
    case INOP_STRING:
      ret = newBINOP_CUSTOM(&pp_in, 0, lhs, rhs);
      ret->op_private = operator;
      break;
  }

  return ret;
}

static OP *newop_in_str(pTHX_ U32 flags, OP *lhs, OP *rhs, SV **parsedata, void *hookdata)
{
  OP *ret = newBINOP_CUSTOM(&pp_in, 0, lhs, rhs);
  ret->op_private = INOP_STRING;

  return ret;
}

static OP *newop_in_num(pTHX_ U32 flags, OP *lhs, OP *rhs, SV **parsedata, void *hookdata)
{
  OP *ret = newBINOP_CUSTOM(&pp_in, 0, lhs, rhs);
  ret->op_private = INOP_NUMBER;

  return ret;
}

struct XSParseInfixHooks infix_in = {
  .cls       = XPI_CLS_MATCH_MISC,
  .rhs_flags = XPI_OPERAND_LIST,

  .parse = &parse_in,
  .new_op = &newop_in,
};

struct XSParseInfixHooks infix_elem_str = {
  .cls            = XPI_CLS_MATCH_MISC,
  .rhs_flags      = XPI_OPERAND_LIST,
  .permit_hintkey = "Syntax::Operator::Elem/elem",

  .wrapper_func_name = "Syntax::Operator::Elem::elem_str",

  .new_op = &newop_in_str,
};

struct XSParseInfixHooks infix_elem_num = {
  .cls            = XPI_CLS_MATCH_MISC,
  .rhs_flags      = XPI_OPERAND_LIST,
  .permit_hintkey = "Syntax::Operator::Elem/elem",

  .wrapper_func_name = "Syntax::Operator::Elem::elem_num",

  .new_op = &newop_in_num,
};

MODULE = Syntax::Operator::In    PACKAGE = Syntax::Operator::In

BOOT:
  boot_xs_parse_infix(0.44);

  register_xs_parse_infix("Syntax::Operator::In::in", &infix_in, NULL);

  register_xs_parse_infix("Syntax::Operator::Elem::elem", &infix_elem_str, NULL);
  register_xs_parse_infix("Syntax::Operator::Elem::∈",    &infix_elem_num, NULL);