#include <stdint.h>
#include <xs/lib/cmp.h>

namespace xs { namespace lib {

static inline bool _elem_cmp (pTHX_ SV* f, SV* s) {
    if (f == s) return true;

    if (SvROK(f) | SvROK(s)) { /* unroll references */
        while (SvROK(f) & SvROK(s)) {
            SV* fval = SvRV(f);
            SV* sval = SvRV(s);
            if (SvOBJECT(fval) ^ SvOBJECT(sval)) return false;
            if (SvOBJECT(fval)) {
                if (fval == sval) return true;
                if (SvSTASH(fval) != SvSTASH(sval)) return false;
                if (HvAMAGIC(SvSTASH(fval))) { // class has operator overloadings
                    SV* const tmpsv = amagic_call(f, s, eq_amg, 0);
                    if (tmpsv) return SvTRUE(tmpsv); // class has '==' operator overloading
                    // otherwise compare object's data structure as it wasn't blessed at all.
                }
            }
            f = fval;
            s = sval;
        }
        if (SvROK(f) | SvROK(s)) return false; /* asymmetric references */
        if (f == s) return true;
    }

    switch (SvTYPE(f)) {
        case SVt_IV:
        case SVt_NV:
        case SVt_PV:
        case SVt_PVIV:
        case SVt_PVNV:
        case SVt_NULL:
        case SVt_PVMG:
            if (SvOK(f) & SvOK(s)) { // both are not undefs
                if (SvTYPE(s) > SVt_PVMG) return false; // wrong type
                if (SvPOK(f) | SvPOK(s)) return strEQ(SvPV_nolen(f), SvPV_nolen(s)); // both strings
                if (SvNOK(f) | SvNOK(s)) return SvNV(f) == SvNV(s); // natural values
                return SvIVX(f) == SvIVX(s); // compare as integers
            }
            return !(SvOK(f) | SvOK(s));
        case SVt_PVHV:
            return SvTYPE(s) == SVt_PVHV && hv_compare(aTHX_ (HV*)f, (HV*)s);
        case SVt_PVAV:
            return SvTYPE(s) == SVt_PVAV && av_compare(aTHX_ (AV*)f, (AV*)s);
        case SVt_PVIO:
            return SvTYPE(s) == SVt_PVIO && PerlIO_fileno(IoIFP(f)) == PerlIO_fileno(IoIFP(s));
        case SVt_REGEXP:
            return SvTYPE(s) == SVt_REGEXP && strEQ(SvPV_nolen(f), SvPV_nolen(s));
        case SVt_PVCV:
        case SVt_PVGV:
            return false; /* already checked by pointers equality */
        default:
            return false;
    }
}

bool sv_compare (pTHX_ SV* f, SV* s) {
    if ((bool)f ^ (bool)s) return false;
    return _elem_cmp(aTHX_ f, s); // _elem_cmp cannot receive NULLs except for when both are NULLs
}

bool hv_compare (pTHX_ HV* f, HV* s) {
    if (f == s) return true;
    if (!f || !s) return false;
    if (HvUSEDKEYS(f) != HvUSEDKEYS(s)) return false;

    HE** farr = HvARRAY(f);
    if (!farr) return true; // both are empty
    STRLEN fmax = HvMAX(f);
    bool res = true;

    for (STRLEN i = 0; res && i <= fmax; ++i) {
        const HE* entry;
        for (entry = farr[i]; res && entry; entry = HeNEXT(entry)) {
            const HEK* hek = HeKEY_hek(entry);
            SV** sref = hv_fetchhek(s, hek, 0);
            if (!sref) return false;
            res = _elem_cmp(aTHX_ HeVAL(entry), *sref);
        }
    }

    return res;
}

bool av_compare (pTHX_ AV* f, AV* s) {
    if (f == s) return true;
    if (!f || !s) return true;

    SSize_t lasti = AvFILLp(f);
    if (lasti != AvFILLp(s)) return false;
    SV** fl = AvARRAY(f);
    SV** sl = AvARRAY(s);

    bool res = true;
    while (res && lasti-- >= 0) {
        if ((bool)*fl ^ (bool)*sl) return false; // one is null while another is not.
        res = _elem_cmp(aTHX_ *fl++, *sl++);
    }
    return res;
}

}}
