/* This file is part of the "version" CPAN distribution.  Please avoid
   editing it in the perl core. */

#ifdef PERL_CORE
#  define VXS_CLASS "version"
#  define VXSp(name) XS_##name
/* VXSXSDP = XSUB Details Proto */
#  define VXSXSDP(x) x, 0
#else
#  define VXS_CLASS "version::vxs"
#  define VXSp(name) VXS_##name
/* proto member is unused in version, it is used in CORE by non version xsubs */
#  define VXSXSDP(x)
#endif

#ifndef XS_INTERNAL
#  define XS_INTERNAL(name) static XSPROTO(name)
#endif

#define VXS(name) XS_INTERNAL(VXSp(name)); XS_INTERNAL(VXSp(name))

/* uses PUSHs, so SP must be at start, PUSHs sv on Perl stack, then returns from
   xsub; this is a little more machine code/tailcall friendly than mPUSHs(foo);
   PUTBACK; return; */

#define VXS_RETURN_M_SV(sv) \
    STMT_START {							\
	SV * sv_vtc = sv;						\
	PUSHs(sv_vtc);							\
	PUTBACK;							\
	sv_2mortal(sv_vtc);						\
	return;								\
    } STMT_END


#ifdef VXS_XSUB_DETAILS
#  ifdef PERL_CORE
    {"UNIVERSAL::VERSION", VXSp(universal_version), VXSXSDP(NULL)},
#  endif
    {VXS_CLASS "::_VERSION", VXSp(universal_version), VXSXSDP(NULL)},
    {VXS_CLASS "::()", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::new", VXSp(version_new), VXSXSDP(NULL)},
    {VXS_CLASS "::parse", VXSp(version_new), VXSXSDP(NULL)},
    {VXS_CLASS "::(\"\"", VXSp(version_stringify), VXSXSDP(NULL)},
    {VXS_CLASS "::stringify", VXSp(version_stringify), VXSXSDP(NULL)},
    {VXS_CLASS "::(0+", VXSp(version_numify), VXSXSDP(NULL)},
    {VXS_CLASS "::numify", VXSp(version_numify), VXSXSDP(NULL)},
    {VXS_CLASS "::normal", VXSp(version_normal), VXSXSDP(NULL)},
    {VXS_CLASS "::to_decimal", VXSp(version_to_decimal), VXSXSDP(NULL)},
    {VXS_CLASS "::to_dotted_decimal", VXSp(version_to_dotted_decimal), VXSXSDP(NULL)},
    {VXS_CLASS "::(cmp", VXSp(version_vcmp), VXSXSDP(NULL)},
    {VXS_CLASS "::(<=>", VXSp(version_vcmp), VXSXSDP(NULL)},
#  ifdef PERL_CORE
    {VXS_CLASS "::vcmp", XS_version_vcmp, VXSXSDP(NULL)},
#  else
    {VXS_CLASS "::VCMP", VXS_version_vcmp, VXSXSDP(NULL)},
#  endif
    {VXS_CLASS "::(bool", VXSp(version_boolean), VXSXSDP(NULL)},
    {VXS_CLASS "::boolean", VXSp(version_boolean), VXSXSDP(NULL)},
    {VXS_CLASS "::(+", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(-", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(*", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(/", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(+=", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(-=", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(*=", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(/=", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(abs", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::(nomethod", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::noop", VXSp(version_noop), VXSXSDP(NULL)},
    {VXS_CLASS "::is_alpha", VXSp(version_is_alpha), VXSXSDP(NULL)},
    {VXS_CLASS "::qv", VXSp(version_qv), VXSXSDP(NULL)},
    {VXS_CLASS "::declare", VXSp(version_qv), VXSXSDP(NULL)},
    {VXS_CLASS "::is_qv", VXSp(version_is_qv), VXSXSDP(NULL)},
    {VXS_CLASS "::tuple", VXSp(version_tuple), VXSXSDP(NULL)},
    {VXS_CLASS "::from_tuple", VXSp(version_from_tuple), VXSXSDP(NULL)},
#else

#ifndef dVAR
#  define dVAR
#endif

#ifdef HvNAME_HEK
typedef HEK HVNAME;
#  ifndef HEKf
#    define HEKfARG(arg)	((void*)(sv_2mortal(newSVhek(arg))))
#    define HEKf		SVf
#  endif
#else
typedef char HVNAME;
#  define HvNAME_HEK	HvNAME_get
#  define HEKfARG(arg)	arg
#  define HEKf		"s"
#endif

VXS(universal_version)
{
    dXSARGS;
    HV *pkg;
    GV **gvp;
    GV *gv;
    SV *sv;
    const char *undef;
    PERL_UNUSED_ARG(cv);

    if (items < 1)
       Perl_croak(aTHX_ "Usage: UNIVERSAL::VERSION(sv, ...)");

    sv = ST(0);

    if (SvROK(sv)) {
        sv = (SV*)SvRV(sv);
        if (!SvOBJECT(sv))
            Perl_croak(aTHX_ "Cannot find version of an unblessed reference");
        pkg = SvSTASH(sv);
    }
    else {
        pkg = gv_stashsv(sv, FALSE);
    }

    gvp = pkg ? (GV**)hv_fetchs(pkg,"VERSION",FALSE) : (GV**)NULL;

    if (gvp && isGV(gv = *gvp) && (sv = GvSV(gv)) && SvOK(sv)) {
        sv = sv_mortalcopy(sv);
	if ( ! ISA_VERSION_OBJ(sv) )
	    UPG_VERSION(sv, FALSE);
        undef = NULL;
    }
    else {
        sv = &PL_sv_undef;
        undef = "(undef)";
    }

    if (items > 1) {
	SV *req = ST(1);

	if (undef) {
	    if (pkg) {
		const HVNAME* const name = HvNAME_HEK(pkg);
		Perl_croak(aTHX_
			   "%" HEKf " does not define $%" HEKf
			   "::VERSION--version check failed",
			   HEKfARG(name), HEKfARG(name));
	    }
	    else {
#if PERL_VERSION_GE(5,8,0)
		Perl_croak(aTHX_
			     "%" SVf " defines neither package nor VERSION--"
                             "version check failed",
			     (void*)(ST(0)) );
#else
		Perl_croak(aTHX_ "%s does not define $%s::VERSION--version check failed",
			   SvPVx_nolen_const(ST(0)),
			   SvPVx_nolen_const(ST(0)) );
#endif
	    }
	}

	if ( ! ISA_VERSION_OBJ(req) ) {
	    /* req may very well be R/O, so create a new object */
	    req = sv_2mortal( NEW_VERSION(req) );
	}

	if ( VCMP( req, sv ) > 0 ) {
	    if ( hv_exists(MUTABLE_HV(SvRV(req)), "qv", 2 ) ) {
		req = VNORMAL(req);
		sv  = VNORMAL(sv);
	    }
	    else {
		req = VSTRINGIFY(req);
		sv  = VSTRINGIFY(sv);
	    }
	    Perl_croak(aTHX_ "%" HEKf " version %" SVf " required--"
		"this is only version %" SVf, HEKfARG(HvNAME_HEK(pkg)),
		SVfARG(sv_2mortal(req)),
		SVfARG(sv_2mortal(sv)));
	}
    }

    /* if the package's $VERSION is not undef, it is upgraded to be a version object */
    if (ISA_VERSION_OBJ(sv)) {
	ST(0) = sv_2mortal(VSTRINGIFY(sv));
    } else {
	ST(0) = sv;
    }

    XSRETURN(1);
}

VXS(version_new)
{
    dXSARGS;
    SV *vs;
    SV *rv;
    const char * classname = "";
    STRLEN len;
    U32 flags = 0;
    SV * svarg0 = NULL;
    PERL_UNUSED_VAR(cv);

    SP -= items;

    switch((U32)items) {
    case 3: {
        SV * svarg2;
        vs = sv_newmortal();
        svarg2 = ST(2);
        Perl_sv_setpvf(aTHX_ vs,"v%s",SvPV_nolen_const(svarg2));
        break;
    }
    case 2:
        vs = ST(1);
    /* Just in case this is something like a tied hash */
        SvGETMAGIC(vs);
        if(SvOK(vs))
            break;
        /* fall through */
    case 1:
        /* no param or explicit undef */
        /* create empty object */
        vs = sv_newmortal();
        sv_setpvs(vs,"undef");
        break;
    default:
    case 0:
        Perl_croak_nocontext("Usage: version::new(class, version)");
    }

    svarg0 = ST(0);
    if ( sv_isobject(svarg0) ) {
	/* get the class if called as an object method */
	const HV * stash = SvSTASH(SvRV(svarg0));
	classname = HvNAME_get(stash);
	len	  = HvNAMELEN_get(stash);
#ifdef HvNAMEUTF8
	flags	  = HvNAMEUTF8(stash) ? SVf_UTF8 : 0;
#endif
    }
    else {
	classname = SvPV_nomg(svarg0, len);
	flags     = SvUTF8(svarg0);
    }

    rv = NEW_VERSION(vs);
    if ( len != sizeof(VXS_CLASS)-1
      || strcmp(classname,VXS_CLASS) != 0 ) /* inherited new() */
        sv_bless(rv, gv_stashpvn(classname, len, GV_ADD | flags));

    VXS_RETURN_M_SV(rv);
}

#define VTYPECHECK(var, val, varname) \
    STMT_START {							\
	SV * sv_vtc = val;						\
	if (ISA_VERSION_OBJ(sv_vtc)) {				\
	    (var) = SvRV(sv_vtc);						\
	}								\
	else								\
	    Perl_croak_nocontext(varname " is not of type version");	\
    } STMT_END

VXS(version_stringify)
{
     dXSARGS;
     if (items < 1)
	 croak_xs_usage(cv, "lobj, ...");
     SP -= items;
     {
	  SV *	lobj;
	  VTYPECHECK(lobj, ST(0), "lobj");

	  VXS_RETURN_M_SV(VSTRINGIFY(lobj));
     }
}

VXS(version_numify)
{
     dXSARGS;
     if (items < 1)
	 croak_xs_usage(cv, "lobj, ...");
     SP -= items;
     {
	  SV *	lobj;
	  VTYPECHECK(lobj, ST(0), "lobj");
	  VXS_RETURN_M_SV(VNUMIFY(lobj));
     }
}

VXS(version_normal)
{
     dXSARGS;
     if (items != 1)
	 croak_xs_usage(cv, "ver");
     SP -= items;
     {
	  SV *	ver;
	  VTYPECHECK(ver, ST(0), "ver");

	  VXS_RETURN_M_SV(VNORMAL(ver));
     }
}

VXS(version_to_decimal)
{
     dXSARGS;
     SV* self = ST(0);
     if (items < 1)
	 croak_xs_usage(cv, "lobj, ...");
     SP -= items;
     {
	  SV *lobj, *rv;
	  VTYPECHECK(lobj, self, "lobj");
          rv = NEW_VERSION(VNUMIFY(lobj));
	  VXS_RETURN_M_SV(sv_bless(rv, SvSTASH(SvRV(self))));
     }
}

VXS(version_to_dotted_decimal)
{
     dXSARGS;
     SV* self = ST(0);
     if (items != 1)
	 croak_xs_usage(cv, "ver");
     SP -= items;
     {
	  SV *lobj, *rv;
	  VTYPECHECK(lobj, self, "lobj");
          rv = NEW_VERSION(VNORMAL(lobj));
          sv_bless(rv, SvSTASH(SvRV(self)));
	  VXS_RETURN_M_SV(sv_bless(rv, SvSTASH(SvRV(self))));
     }
}

VXS(version_vcmp)
{
     dXSARGS;
     if (items < 2)
	 croak_xs_usage(cv, "lobj, robj, ...");
     SP -= items;
     {
	  SV *	lobj;
	  VTYPECHECK(lobj, ST(0), "lobj");
	  {
	       SV	*rs;
	       SV	*rvs;
	       SV * robj = ST(1);
	       const int swap = items > 2 ? SvTRUE(ST(2)) : 0;

	       if ( !ISA_VERSION_OBJ(robj) )
	       {
		    robj = sv_2mortal(NEW_VERSION(SvOK(robj) ? robj : newSVpvs_flags("0", SVs_TEMP)));
	       }
	       rvs = SvRV(robj);

	       if ( swap )
	       {
		    rs = newSViv(VCMP(rvs,lobj));
	       }
	       else
	       {
		    rs = newSViv(VCMP(lobj,rvs));
	       }

	       VXS_RETURN_M_SV(rs);
	  }
     }
}

VXS(version_boolean)
{
    dXSARGS;
    SV *lobj;
    if (items < 1)
	croak_xs_usage(cv, "lobj, ...");
    SP -= items;
    VTYPECHECK(lobj, ST(0), "lobj");
    {
	SV * const rs =
	    newSViv( VCMP(lobj,
			  sv_2mortal(NEW_VERSION(
					sv_2mortal(newSVpvs("0"))
				    ))
			 )
		   );
	VXS_RETURN_M_SV(rs);
    }
}

VXS(version_noop)
{
    dXSARGS;
    if (items < 1)
	croak_xs_usage(cv, "lobj, ...");
    if (ISA_VERSION_OBJ(ST(0)))
	Perl_croak(aTHX_ "operation not supported with version object");
    else
	Perl_croak(aTHX_ "lobj is not of type version");
    XSRETURN_EMPTY;
}

static
void
S_version_check_key(pTHX_ CV * cv, const char * key, int keylen)
{
    dXSARGS;
    if (items != 1)
	croak_xs_usage(cv, "lobj");
    {
	SV *lobj = POPs;
	SV *ret;
	VTYPECHECK(lobj, lobj, "lobj");
	if ( hv_exists(MUTABLE_HV(lobj), key, keylen ) )
	    ret = &PL_sv_yes;
	else
	    ret = &PL_sv_no;
	PUSHs(ret);
	PUTBACK;
	return;
    }
}

VXS(version_is_alpha)
{
    S_version_check_key(aTHX_ cv, "alpha", 5);
}

VXS(version_qv)
{
    dXSARGS;
    PERL_UNUSED_ARG(cv);
    SP -= items;
    {
	SV * ver = ST(0);
	SV * sv0 = ver;
	SV * rv;
        STRLEN len = 0;
        const char * classname = "";
        U32 flags = 0;
        if ( items == 2 ) {
	    SV * sv1 = ST(1);
	    SvGETMAGIC(sv1);
	    if (SvOK(sv1)) {
		ver = sv1;
	    }
	    else {
		Perl_croak(aTHX_ "Invalid version format (version required)");
	    }
            if ( sv_isobject(sv0) ) { /* class called as an object method */
                const HV * stash = SvSTASH(SvRV(sv0));
                classname = HvNAME_get(stash);
                len       = HvNAMELEN_get(stash);
#ifdef HvNAMEUTF8
                flags     = HvNAMEUTF8(stash) ? SVf_UTF8 : 0;
#endif
            }
            else {
	       classname = SvPV(sv0, len);
                flags     = SvUTF8(sv0);
            }
	}
	if ( !SvVOK(ver) ) { /* not already a v-string */
	    rv = sv_newmortal();
	    SvSetSV_nosteal(rv,ver); /* make a duplicate */
	    UPG_VERSION(rv, TRUE);
	} else {
	    rv = sv_2mortal(NEW_VERSION(ver));
	}
	if ( items == 2 && (len != 7
                || strcmp(classname,"version")) ) { /* inherited new() */
	    sv_bless(rv, gv_stashpvn(classname, len, GV_ADD | flags));
        }
	PUSHs(rv);
    }
    PUTBACK;
    return;
}


VXS(version_is_qv)
{
    S_version_check_key(aTHX_ cv, "qv", 2);
}

VXS(version_tuple)
{
    dXSARGS;
    if (items != 1)
	croak_xs_usage(cv, "lobj");
    SP -= items;
    {
	SV * lobj;
	size_t i;
	VTYPECHECK(lobj, ST(0), "lobj");

	SV** avptr = hv_fetchs(MUTABLE_HV(lobj), "version", 0);
	if (!avptr || !SvROK(*avptr) || SvTYPE(SvRV(*avptr)) != SVt_PVAV) {
	    PUTBACK;
	    return;
	}
	AV* version = MUTABLE_AV(SvRV(*avptr));
	for (i = 0; i < av_count(version); ++i) {
	    SV** svptr = av_fetch(version, i, 0);
	    if (!svptr || !*svptr) {
		PUTBACK;
		return;
	    }
	    XPUSHs(*svptr);
	}
	PUTBACK;
     }
}

VXS(version_from_tuple)
{
    dXSARGS;
    SV *lobj;
    int i;
    if (items < 2)
	croak_xs_usage(cv, "lobj, ...");
    lobj = ST(0);
    SP -= items;

    AV* versions = newAV();
    SV* original = newSVpvs("v");

    for (i = 1; i < items; ++i) {
	if (SvIV(ST(i)) < 0)
	    Perl_croak(aTHX_ "Value %" IVdf " in version is negative", SvIV(ST(i)));
	UV value = SvUV(ST(i));
	av_push(versions, newSVuv(value));
	if (i != 1)
	    sv_catpvs(original, ".");
	sv_catpvf(original, "%" UVuf, value);
    }

    HV* hash = newHV();
    (void)hv_stores(hash, "version", newRV_noinc(MUTABLE_SV(versions)));
    (void)hv_stores(hash, "original", original);
    (void)hv_stores(hash, "qv", newSVsv(&PL_sv_yes));

    HV* stash = SvROK(lobj) ? SvSTASH(lobj) : gv_stashsv(lobj, GV_ADD);
    SV* result = sv_bless(newRV_noinc(MUTABLE_SV(hash)), stash);
    XPUSHs(result);
    PUTBACK;
}

#endif
