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

#include "ppport.h"

#include "const-c.inc"

#define PERL_VERSION_DECIMAL(r,v,s) (r*1000000 + v*1000 + s)
#define PERL_DECIMAL_VERSION PERL_VERSION_DECIMAL(PERL_REVISION,PERL_VERSION,PERL_SUBVERSION)
#define PERL_VERSION_GE(r,v,s) (PERL_DECIMAL_VERSION >= PERL_VERSION_DECIMAL(r,v,s))

#ifndef G_WANT
#define G_WANT (G_VOID|G_SCALAR|G_ARRAY)
#endif

struct block_symbol_t {
    CV * cv;
    SV * symbol_SV;
};

static Perl_ppaddr_t return_ppaddr;
static struct block_symbol_t * block_symbols;
static int block_symbols_capacity, block_symbols_n;

static SV * regex_match_sv;

static OP * my_pp_deep_ret(pTHX){
    dSP; POPs;

    IV depth = SvIV(PL_stack_base[TOPMARK+1]);

    for(SV ** p = PL_stack_base+TOPMARK+1; p<SP; ++p)
        *p = *(p+1);
    POPs;

    if( depth <= 0 )
        RETURN;

    OP * next_op;
    while( depth-- )
        next_op = return_ppaddr(aTHX);
    RETURNOP(next_op);
}

static OP * my_pp_sym_ret(pTHX){
    dSP; POPs;

    SV * symbol_SV = PL_stack_base[TOPMARK+1];

    for(SV ** p = PL_stack_base+TOPMARK+1; p<SP; ++p)
        *p = *(p+1);
    POPs;

    while(TRUE){
        for(PERL_CONTEXT * cx = &cxstack[cxstack_ix]; cx>=cxstack; --cx){
            switch( CxTYPE(cx) ){
                default:
                    continue;
                case CXt_SUB:
#if PERL_VERSION_GE(5,18,0)
                    if( cx->cx_type & CXp_SUB_RE_FAKE )
                        continue;
#endif
                    for(struct block_symbol_t *p = block_symbols+block_symbols_n-1; p>=block_symbols; --p)
                        if( p->cv == cx->blk_sub.cv ){
                            if( !SvOK(p->symbol_SV) )
                                RETURNOP(return_ppaddr(aTHX));
#if PERL_VERSION_GE(5,10,0)
                            if( SvRXOK(p->symbol_SV) ){
                                PUSHMARK(SP);
                                EXTEND(SP, 2);
                                PUSHs(p->symbol_SV);
                                PUSHs(symbol_SV);
                                PUTBACK;
                                call_sv(regex_match_sv, G_SCALAR);
                                SPAGAIN;
                                IV match_res = POPi;
                                PUTBACK;

                                if( match_res )
                                    RETURNOP(return_ppaddr(aTHX));
                            }
                            else
#endif
                                if( sv_cmp(p->symbol_SV, symbol_SV)==0 )
                                    RETURNOP(return_ppaddr(aTHX));
                        }
                case CXt_EVAL:
                case CXt_FORMAT:
                    goto DO_RETURN;
            }
        }
        DO_RETURN:
        return_ppaddr(aTHX);
    }
}

static OP * deep_ret_check(pTHX_ OP * o, GV * namegv, SV * ckobj){
    o->op_ppaddr = my_pp_deep_ret;
    return o;
}

static OP * sym_ret_check(pTHX_ OP * o, GV * namegv, SV * ckobj){
    o->op_ppaddr = my_pp_sym_ret;
    return o;
}

static int guard_free(pTHX_ SV * guard_SV, MAGIC * mg){
    for(struct block_symbol_t * p=block_symbols+block_symbols_n-1; p>=block_symbols; --p)
        if( (IV)p->cv == (IV)mg->mg_ptr ){
            --block_symbols_n;
            *p = block_symbols[block_symbols_n];
            break;
        }
    return 0;
}

static MGVTBL guard_vtbl = {
    0, 0, 0, 0,
    guard_free
};

#if !PERL_VERSION_GE(5,14,0)
static CV* my_deep_ret_cv;
static CV* my_sym_ret_cv;
static OP* (*orig_entersub_check)(pTHX_ OP*);
static OP* my_entersub_check(pTHX_ OP* o){
    CV *cv = NULL;
    OP *cvop = OpSIBLING(((OpSIBLING(cUNOPo->op_first)) ? cUNOPo : ((UNOP*)cUNOPo->op_first))->op_first);
    while( OpSIBLING(cvop) )
        cvop = OpSIBLING(cvop);
    if( cvop->op_type == OP_RV2CV && !(o->op_private & OPpENTERSUB_AMPER) ){
        SVOP *tmpop = (SVOP*)((UNOP*)cvop)->op_first;
        switch (tmpop->op_type) {
            case OP_GV: {
                GV *gv = cGVOPx_gv(tmpop);
                cv = GvCVu(gv);
                if (!cv)
                    tmpop->op_private |= OPpEARLY_CV;
            } break;
            case OP_CONST: {
               SV *sv = cSVOPx_sv(tmpop);
               if (SvROK(sv) && SvTYPE(SvRV(sv)) == SVt_PVCV)
                   cv = (CV*)SvRV(sv);
           } break;
        }
        if( cv==my_deep_ret_cv )
            o->op_ppaddr = my_pp_deep_ret;
        if( cv==my_sym_ret_cv )
            o->op_ppaddr = my_pp_sym_ret;
    }
    return orig_entersub_check(aTHX_ o);
}
#endif

MODULE = Return::Deep		PACKAGE = Return::Deep		

INCLUDE: const-xs.inc

void add_bound(SV * act_SV, SV * symbol_SV)
    PPCODE:
        if( !(SvOK(act_SV) && SvROK(act_SV) && SvTYPE(SvRV(act_SV))==SVt_PVCV) )
            croak("there should be a code block");

        CV * act_CV = (CV*) SvRV(act_SV);
        SV * guard_SV = newSV(0);

        sv_magicext(guard_SV, NULL, PERL_MAGIC_ext, &guard_vtbl, (char*) act_CV, 0);

        if( block_symbols_n >= block_symbols_capacity ){
            block_symbols_capacity *= 2;
            Renew(block_symbols, block_symbols_capacity, struct block_symbol_t);
        }
        block_symbols[block_symbols_n].cv = act_CV;
        block_symbols[block_symbols_n].symbol_SV = symbol_SV;
        ++block_symbols_n;

        PUSHs(sv_2mortal(newRV_noinc(guard_SV)));

void deep_wantarray(IV depth)
    PPCODE:
        if( depth<1 )
            croak("deep_wantarray with non-positive depth");

        PERL_CONTEXT * cx = &cxstack[cxstack_ix];
        for(; cx>=cxstack; --cx)
            switch( CxTYPE(cx) ){
                default:
                    continue;
                case CXt_SUB:
#if PERL_VERSION_GE(5,18,0)
                    if( cx->cx_type & CXp_SUB_RE_FAKE )
                        continue;
#endif
                case CXt_EVAL:
                case CXt_FORMAT:
                    if( --depth <= 0 )
                        goto FOUND;
            }
        FOUND:

        if( cx<cxstack )
            PUSHs(&PL_sv_undef);
        else
            switch(cx->blk_gimme & G_WANT){
                case G_VOID:
                    PUSHs(&PL_sv_undef);
                    break;
                case G_SCALAR:
                    PUSHs(&PL_sv_no);
                    break;
                case G_ARRAY:
                    PUSHs(&PL_sv_yes);
                    break;
                default:
                    croak("Unknown wantarray");
            }

void sym_wantarray(SV * symbol_SV)
    PPCODE:
        PERL_CONTEXT * cx = &cxstack[cxstack_ix];
        for(; cx>=cxstack; --cx){
            switch( CxTYPE(cx) ){
                default:
                    continue;
                case CXt_SUB:
#if PERL_VERSION_GE(5,18,0)
                    if( cx->cx_type & CXp_SUB_RE_FAKE )
                        continue;
#endif
                    for(struct block_symbol_t *p = block_symbols+block_symbols_n-1; p>=block_symbols; --p)
                        if( p->cv == cx->blk_sub.cv ){
                            if( !SvOK(p->symbol_SV) )
                                break;
#if PERL_VERSION_GE(5,10,0)
                            if( SvRXOK(p->symbol_SV) ){
                                PUSHMARK(SP);
                                EXTEND(SP, 2);
                                PUSHs(p->symbol_SV);
                                PUSHs(symbol_SV);
                                PUTBACK;
                                call_sv(regex_match_sv, G_SCALAR);
                                SPAGAIN;
                                IV match_res = POPi;
                                PUTBACK;

                                if( match_res )
                                    goto FOUND;
                            }
                            else
#endif
                                if( sv_cmp(p->symbol_SV, symbol_SV)==0 )
                                    goto FOUND;
                        }
                case CXt_EVAL:
                case CXt_FORMAT:
                    break;
            }
        }
        FOUND:
        if( cx<cxstack )
            PUSHs(&PL_sv_undef);
        else
            switch(cx->blk_gimme & G_WANT){
                case G_VOID:
                    PUSHs(&PL_sv_undef);
                    break;
                case G_SCALAR:
                    PUSHs(&PL_sv_no);
                    break;
                case G_ARRAY:
                    PUSHs(&PL_sv_yes);
                    break;
                default:
                    croak("Unknown wantarray");
            }

BOOT:
    block_symbols_capacity = 8;
    block_symbols_n = 0;
    Newx(block_symbols, block_symbols_capacity, struct block_symbol_t);

    regex_match_sv = newRV_inc((SV*)get_cv("Return::Deep::regex_match", FALSE));

    return_ppaddr = PL_ppaddr[OP_RETURN];
#if PERL_VERSION_GE(5,14,0)
    cv_set_call_checker(get_cv("Return::Deep::deep_ret", TRUE), deep_ret_check, &PL_sv_undef);
    cv_set_call_checker(get_cv("Return::Deep::sym_ret", TRUE), sym_ret_check, &PL_sv_undef);
#else
    my_deep_ret_cv = get_cv("Return::Deep::deep_ret", TRUE);
    my_sym_ret_cv = get_cv("Return::Deep::sym_ret", TRUE);
    orig_entersub_check = PL_check[OP_ENTERSUB];
    PL_check[OP_ENTERSUB] = my_entersub_check;
#endif