#define PERL_NO_GET_CONTEXT
#include <EXTERN.h>
#include <perl.h>
#include <XSUB.h>

#include "ppport.h"

#include "xslate.h"

#define TXBM_DECL(name) SV* name \
    (pTHX_ tx_state_t* const st PERL_UNUSED_DECL, SV* const method PERL_UNUSED_DECL, SV** MARK)

#define TXBM(moniker) static TXBM_DECL(CAT2(tx_builtin_method_, moniker))

#define TXBM_SETUP(name, nargs, trait) \
    { STRINGIFY(name), CAT2(tx_builtin_method_, name), nargs, trait }

enum tx_trait_t {
    TX_TRAIT_ANY,
    TX_TRAIT_ENUMERABLE,
    TX_TRAIT_KV,
};

#define TX_PAIR_CLASS "Text::Xslate::Type::Pair"

typedef struct {
    const char* const name;

    TXBM_DECL( (*body) );

    I16 nargs;
    U16 trait;
} tx_builtin_method_t;

static SV*
tx_make_pair(pTHX_ HV* const stash, SV* const key, SV* const val) {
    AV* av;
    SV* pair[2];
    pair[0] = key;
    pair[1] = val;

    av = av_make(2, pair);
    return sv_bless( sv_2mortal( newRV_noinc((SV*)av) ), stash );
}

static I32
tx_pair_cmp(pTHX_ SV* const a, SV* const b) {
    assert(SvROK(a));
    assert(SvTYPE(SvRV(a)) == SVt_PVAV);
    assert(SvROK(b));
    assert(SvTYPE(SvRV(b)) == SVt_PVAV);

    return sv_cmp(
        *av_fetch((AV*)SvRV(a), 0, TRUE),
        *av_fetch((AV*)SvRV(b), 0, TRUE)
    );
}

static SV*
tx_kv(pTHX_ SV* const hvref) {
    HV* const stash = gv_stashpvs(TX_PAIR_CLASS, GV_ADDMULTI);
    HV* const hv    = (HV*)SvRV(hvref);
    AV* const av    = newAV();
    SV* const avref = sv_2mortal(newRV_noinc((SV*)av));
    HE* he;

    assert(SvROK(hvref));
    assert(SvTYPE(hv) == SVt_PVHV);

    av_extend(av, HvKEYS(hv) - 1); /* if possible */

    hv_iterinit(hv);
    while((he = hv_iternext(hv))) {
        SV* const pair = tx_make_pair(aTHX_ stash,
            hv_iterkeysv(he),
            hv_iterval(hv, he));

        av_push(av, pair);
        SvREFCNT_inc_simple_void_NN(pair);
    }
    sortsv(AvARRAY(av), AvFILLp(av)+1, tx_pair_cmp);

    return avref;
}

/* Enumerable containers */

TXBM(size) {
    return sv_2mortal( newSViv(av_len((AV*)SvRV(*MARK)) + 1) );
}

TXBM(join) {
    dSP;
    SV* const result = sv_newmortal();
    AV* const av     = (AV*)SvRV(*MARK);
    I32 const len    = av_len(av) + 1;
    I32 i;

    EXTEND(SP, len);
    for(i = 0; i < len; i++) {
        SV** const svp = av_fetch(av, i, FALSE);
        PUSHs(svp ? *svp : &PL_sv_undef);
    }
    /* don't do PUTBACK */

    MARK++;
    do_join(result, *MARK, MARK, SP);

    return result;
}

TXBM(reverse) {
    AV* const av        = (AV*)SvRV(*MARK);
    I32 const len       = av_len(av) + 1;
    AV* const result    = newAV();
    SV* const resultref = sv_2mortal(newRV_noinc((SV*)result));
    I32 i;

    av_fill(result, len - 1);
    for(i = 0; i < len; i++) {
        SV** const svp = av_fetch(av, i, FALSE);
        av_store(result, -(i+1), newSVsv(svp ? *svp : &PL_sv_undef));
    }

    return resultref;
}

/* Key-Value containers */

TXBM(keys) {
    HV* hv;
    HE* he;
    AV* av;
    SV* avref;

    if(!(SvROK(*MARK) && SvTYPE(SvRV(*MARK)) == SVt_PVHV)) {
        tx_warn(aTHX_ st, "keys() requires a key-value container, not %s",
            tx_neat(aTHX_ *MARK));
        return &PL_sv_undef;
    }

    hv    = (HV*)SvRV(*MARK);
    av    = newAV();
    avref = sv_2mortal(newRV_noinc((SV*)av));

    av_extend(av, HvKEYS(hv) - 1); /* if possible */

    hv_iterinit(hv);
    while((he = hv_iternext(hv))) {
        SV* const key = hv_iterkeysv(he);
        AvARRAY(av)[++AvFILLp(av)] = key;
        SvREFCNT_inc_simple_void_NN(key);
    }
    sortsv(AvARRAY(av), AvFILLp(av)+1, Perl_sv_cmp);

    return avref;
}

TXBM(values) {
    SV* const avref = tx_builtin_method_keys(aTHX_ st, method, MARK);
    HV* const hv    = (HV*)SvRV(*MARK);
    AV* const av    = (AV*)SvRV(avref);
    I32 const len   = AvFILLp(av) + 1;
    I32 i;

    /* map { $hv->{$_} } @{$keys} */
    for(i = 0; i < len; i++) {
        SV* const key = AvARRAY(av)[i];
        HE* const he  = hv_fetch_ent(hv, key, TRUE, 0U);
        SV* const val = hv_iterval(hv, he);
        AvARRAY(av)[i] = val;
        SvREFCNT_inc_simple_void_NN(val);
        SvREFCNT_dec(key);
    }

    return avref;
}

TXBM(kv) {
    return tx_kv(aTHX_ *MARK);
}

static const tx_builtin_method_t tx_builtin_method[] = {
    TXBM_SETUP(size,    0, TX_TRAIT_ENUMERABLE),
    TXBM_SETUP(join,    1, TX_TRAIT_ENUMERABLE),
    TXBM_SETUP(reverse, 0, TX_TRAIT_ENUMERABLE),

    TXBM_SETUP(keys,    0, TX_TRAIT_KV),
    TXBM_SETUP(values,  0, TX_TRAIT_KV),
    TXBM_SETUP(kv,      0, TX_TRAIT_KV),

};

static const size_t tx_num_buildin_method
    = sizeof(tx_builtin_method) / sizeof(tx_builtin_method[0]);


static I32
tx_as_enumerable(pTHX_ tx_state_t* const st, SV** const svp) {
    SV* const sv = *svp;

    if(sv_isobject(sv)) {
        dSP;
        SV* retval;
        PUSHMARK(SP);
        XPUSHs(sv);
        PUTBACK;

        call_method("(@{}", G_SCALAR | G_EVAL);

        if(sv_true(ERRSV)) {
            tx_error(aTHX_ st, "Use of %s as %s objects",
                tx_neat(aTHX_ sv), "enumerable");
            return FALSE;
        }

        retval = TX_pop();

        SvGETMAGIC(retval);
        if(SvROK(retval) && SvTYPE(SvRV(retval)) == SVt_PVAV) {
            *svp = retval;
            return TRUE;
        }
    }
    else if(SvROK(sv)){
        if(SvTYPE(SvRV(sv)) == SVt_PVAV) {
            return TRUE;
        }
        else if(SvTYPE(SvRV(sv)) == SVt_PVHV) {
            *svp = tx_kv(aTHX_ sv);
            return TRUE;
        }
    }

    return FALSE;
}

static I32
tx_as_kv(pTHX_ tx_state_t* const st, SV** const svp) {
    SV* const sv = *svp;

    if(sv_isobject(sv)) {
        dSP;
        SV* retval;
        PUSHMARK(SP);
        XPUSHs(sv);
        PUTBACK;

        call_method("(%{}", G_SCALAR | G_EVAL);

        if(sv_true(ERRSV)) {
            tx_error(aTHX_ st, "Use of %s as %s objects",
                tx_neat(aTHX_ sv), "kv");
            return FALSE;
        }

        retval = TX_pop();

        SvGETMAGIC(retval);
        if(SvROK(retval) && SvTYPE(SvRV(retval)) == SVt_PVHV) {
            *svp = retval;
            return TRUE;
        }
    }
    else if(SvROK(sv)){
        if(SvTYPE(SvRV(sv)) == SVt_PVHV) {
            return TRUE;
        }
    }

    return FALSE;
}

SV*
tx_methodcall(pTHX_ tx_state_t* const st, SV* const method) {
    /* ENTER & LEAVE & PUSHMARK & PUSH must be done */
    dSP;
    SV** MARK = PL_stack_base + TOPMARK;
    dORIGMARK;
    SV* const invocant = *(++MARK);
    STRLEN methodlen;
    const char* const methodpv = SvPV_const(method, methodlen);
    U32 i;
    SV* retval = NULL;

    if(sv_isobject(invocant)) {
        HV* const stash = SvSTASH(SvRV(invocant));
        GV* const mgv   = gv_fetchmeth_autoload(stash, methodpv, methodlen, 0);

        if(mgv) {
            call_sv((SV*)GvCV(mgv), G_SCALAR | G_EVAL);
            if(sv_true(ERRSV)) {
                tx_error(aTHX_ st, "%"SVf"\t...", ERRSV);
            }
            retval = st->targ;
            sv_setsv_nomg(retval, TX_pop());
            goto finish;
        }
        /* fallback */
    }

    (void)POPMARK;

    if(!SvOK(invocant)) {
        tx_warn(aTHX_ st, "Use of nil to invoke method %"SVf, method);
        goto finish;
    }

    /* linear search */
    for(i = 0; i < tx_num_buildin_method; i++) {
        tx_builtin_method_t const bm = tx_builtin_method[i];
        if(strEQ(methodpv, bm.name)) {
            dSP;
            I32 const items = SP - MARK;

            if(bm.nargs != -1
                && bm.nargs != items) {
                tx_error(aTHX_ st,
                    "Builtin method %"SVf" requres exactly %d argument(s), "
                    "but supplied %d",
                    method, (int)bm.nargs, (int)items);
                goto finish;
            }

            if(bm.trait == TX_TRAIT_ENUMERABLE) {
                if(!tx_as_enumerable(aTHX_ st, MARK /* invocant ptr */)) {
                    goto finish;
                }
                assert(SvROK(*MARK) && SvTYPE(SvRV(*MARK)) == SVt_PVAV);
            }
            else if(bm.trait == TX_TRAIT_KV) {
                if(!tx_as_kv(aTHX_ st, MARK /* invocant ptr */)) {
                    goto finish;
                }
                assert(SvROK(*MARK) && SvTYPE(SvRV(*MARK)) == SVt_PVHV);
            }

            retval = st->targ;
            sv_setsv(retval, bm.body(aTHX_ st, method, MARK));
            goto finish;
        }
    }

    finish:
    SP = ORIGMARK;
    PUTBACK;

    FREETMPS;
    LEAVE;
    return retval ? retval : &PL_sv_undef;
}

MODULE = Text::Xslate::Methods    PACKAGE = Text::Xslate::Type::Pair

void
key(AV* pair)
ALIAS:
    key   = 0
    value = 1
CODE:
{
    ST(0) = *av_fetch(pair, ix, TRUE);
    XSRETURN(1);
}