/*  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, 2011 -- leonerd@leonerd.org.uk
 */

#define PERL_NO_GET_CONTEXT

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

#ifndef G_METHOD_NAMED
#define G_METHOD_NAMED G_METHOD
#endif

#include <string.h>
#define streq(a,b) (strcmp((a),(b)) == 0)

enum {
  CTX_GET_CB,
  CTX_SET_CB,
  CTX_OBJ,
};

typedef SV *sentinel_ctx;

static int magic_get(pTHX_ SV *sv, MAGIC *mg)
{
  dSP;
  sentinel_ctx *ctx = (sentinel_ctx*)AvARRAY(mg->mg_obj);

  if(ctx[CTX_GET_CB]) {
    int count;

    ENTER;
    SAVETMPS;

    PUSHMARK(SP);
    if(ctx[CTX_OBJ]) {
      EXTEND(SP, 1);
      PUSHs(ctx[CTX_OBJ]);
    }
    PUTBACK;

    if(ctx[CTX_OBJ] && SvPOK(ctx[CTX_GET_CB]))
      // Calling method by name
      count = call_sv(ctx[CTX_GET_CB], G_SCALAR | G_METHOD_NAMED);
    else
      count = call_sv(ctx[CTX_GET_CB], G_SCALAR);
    assert(count == 1);

    SPAGAIN;
    sv_setsv_nomg(sv, POPs);

    PUTBACK;
    FREETMPS;
    LEAVE;
  }

  return 1;
}

static int magic_set(pTHX_ SV *sv, MAGIC *mg)
{
  dSP;
  sentinel_ctx *ctx = (sentinel_ctx*)AvARRAY(mg->mg_obj);

  if(ctx[CTX_SET_CB]) {
    ENTER;
    SAVETMPS;

    PUSHMARK(SP);
    if(ctx[CTX_OBJ]) {
      EXTEND(SP, 2);
      PUSHs(ctx[CTX_OBJ]);
    }
    else {
      EXTEND(SP, 1);
    }
    PUSHs(sv);
    PUTBACK;

    if(ctx[CTX_OBJ] && SvPOK(ctx[CTX_SET_CB]))
      // Calling method by name
      call_sv(ctx[CTX_SET_CB], G_VOID | G_METHOD_NAMED);
    else
      call_sv(ctx[CTX_SET_CB], G_VOID);

    FREETMPS;
    LEAVE;
  }

  return 1;
}

static MGVTBL vtbl = {
  &magic_get,
  &magic_set,
};

MODULE = Sentinel    PACKAGE = Sentinel

SV *
sentinel(...)
  PREINIT:
    int i;
    SV *value = NULL;
    SV *get_cb = NULL;
    SV *set_cb = NULL;
    SV *obj = NULL;
    SV *retval;

  PPCODE:
    /* Parse name => value argument pairs */
    for(i = 0; i < items; i += 2) {
      char *argname  = SvPV_nolen(ST(i));
      SV   *argvalue = ST(i+1);

      if(streq(argname, "value")) {
        value = argvalue;
      }
      else if(streq(argname, "get")) {
        get_cb = argvalue;
      }
      else if(streq(argname, "set")) {
        set_cb = argvalue;
      }
      else if(streq(argname, "obj")) {
        obj = argvalue;
      }
      else {
        fprintf(stderr, "Argument %s at %p\n", argname, argvalue);
      }
    }

    retval = sv_newmortal();
/**
 * Perl 5.14 allows any TEMP scalar to be returned in LVALUE context provided
 * it is magical. Perl versions before this only accept magic for being a tied
 * array or hash element. Rather than try to hack this magic type, we'll just
 * pretend the SV isn't a TEMP
 * The following workaround is known to work on Perl 5.12.4.
 */
#if (PERL_REVISION == 5) && (PERL_VERSION < 14)
    SvFLAGS(retval) &= ~SVs_TEMP;
#endif

    if(value)
      sv_setsv(retval, value);

    if(get_cb || set_cb) {
      sentinel_ctx *ctx;
      AV* payload = newAV();
      av_extend(payload, 2);
      AvFILLp(payload) = 2;

      ctx = (sentinel_ctx*)AvARRAY(payload);

      ctx[CTX_GET_CB] = get_cb ? newSVsv(get_cb) : NULL;
      ctx[CTX_SET_CB] = set_cb ? newSVsv(set_cb) : NULL;
      ctx[CTX_OBJ] = obj ? newSVsv(obj) : NULL;

      sv_magicext(retval, (SV*)payload, PERL_MAGIC_ext, &vtbl, NULL, 0);
      SvREFCNT_dec(payload);
    }

    if (!items)
      EXTEND(SP, 1);
    PUSHs(retval);
    XSRETURN(1);

BOOT:
  CvLVALUE_on(get_cv("Sentinel::sentinel", 0));