The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#define NEED_newSV_type
#include "xshelper.h"
#include "mgx.h"
#define NEED_mro_get_linear_isa
#include "mro_compat.h"

#if PERL_BCDVERSION < 0x5010000
#define HF_USE_TIE TRUE
#endif

#define PACKAGE "Hash::FieldHash"

#ifdef HF_USE_TIE
#include "compat58.h"
#endif

#define OBJECT_REGISTRY_KEY PACKAGE "::" "::META"
#define NAME_REGISTRY_KEY   OBJECT_REGISTRY_KEY

#define INVALID_OBJECT "Invalid object \"%"SVf"\" as a fieldhash key"

#define MY_CXT_KEY PACKAGE "::_guts" XS_VERSION
typedef struct {
	AV* object_registry; /* the global object registry */
	I32 last_id;         /* the last allocated id */
	SV* free_id;         /* the top of the linked list */

	HV*  name_registry;
	bool name_registry_is_stale;
} my_cxt_t;
START_MY_CXT
#define ObjectRegistry  (MY_CXT.object_registry)
#define LastId          (MY_CXT.last_id)
#define FreeId          (MY_CXT.free_id)
#define NameRegistry    (MY_CXT.name_registry)

#define NameRegistryIsStale (MY_CXT.name_registry_is_stale)

static int fieldhash_key_free(pTHX_ SV* const sv, MAGIC* const mg);
static MGVTBL fieldhash_key_vtbl = {
	NULL, /* get */
	NULL, /* set */
	NULL, /* len */
	NULL, /* clear */
	fieldhash_key_free,
	NULL, /* copy */
	NULL, /* dup */
#ifdef MGf_LOCAL
	NULL, /* local */
#endif
};

#define fieldhash_key_mg(sv) MgFind(sv, &fieldhash_key_vtbl)

#ifndef HF_USE_TIE
static I32 fieldhash_watch(pTHX_ IV const action, SV* const fieldhash);
static struct ufuncs fieldhash_ufuncs = {
	fieldhash_watch, /* uf_val */
	NULL,            /* uf_set */
	0,               /* uf_index */
};

#define fieldhash_mg(sv) hf_fieldhash_mg(aTHX_ sv)
static MAGIC*
hf_fieldhash_mg(pTHX_ SV* const sv){
	MAGIC* mg;

	assert(sv != NULL);
	for(mg = SvMAGIC(sv); mg; mg = mg->mg_moremagic){
		if(((struct ufuncs*)mg->mg_ptr) == &fieldhash_ufuncs){
			break;
		}
	}
	return mg;
}

static SV*
fieldhash_fetch(pTHX_ HV* const fieldhash, SV* const key){
	HE* const he = hv_fetch_ent(fieldhash, key, FALSE, 0U);

	return he ? HeVAL(he) : &PL_sv_undef;
}

static void
fieldhash_store(pTHX_ HV* const fieldhash, SV* const key, SV* const val){
	(void)hv_store_ent(fieldhash, key, val, 0U);
}

#endif /* !HF_USE_TIE */

static SV*
hf_new_id(pTHX_ pMY_CXT){
	SV* obj_id;
	if(!FreeId){
		obj_id = newSV_type(SVt_PVIV);
		sv_setiv(obj_id, ++LastId);
	}
	else{
		obj_id = FreeId;
		FreeId = INT2PTR(SV*, SvIVX(obj_id)); /* next node */

		(void)sv_2iv(obj_id);
	}
	return obj_id;
}

static void
hf_free_id(pTHX_ pMY_CXT_ SV* const obj_id){
	assert(SvTYPE(obj_id) >= SVt_PVIV);

	SvIV_set(obj_id, PTR2IV(FreeId));
	SvIOK_off(obj_id);
	FreeId = obj_id;
}

static SV*
hf_av_find(pTHX_ AV* const av, SV* const sv){
	SV** const ary = AvARRAY(av);
	I32  const len = AvFILLp(av)+1;
	I32 i;

	for(i = 0; i < len; i++){
		if(ary[i] == sv){
			return sv;
		}
	}
	return NULL;
}

/*
    defined actions (in 5.10.0) are:
       HV_FETCH_ISSTORE  = 0x04
       HV_FETCH_ISEXISTS = 0x08
       HV_FETCH_LVALUE   = 0x10
       HV_FETCH_JUST_SV  = 0x20
       HV_DELETE         = 0x40
 */
#define HF_CREATE_KEY(a) (a & (HV_FETCH_ISSTORE | HV_FETCH_LVALUE))

static I32
fieldhash_watch(pTHX_ IV const action, SV* const fieldhash){
	MAGIC* const mg = fieldhash_mg(fieldhash);
	SV* obj_ref;
	SV* obj;
	const MAGIC* key_mg;
	AV* reg;         /* field registry */

	assert(mg != NULL);

	obj_ref = mg->mg_obj; /* the given hash key */

	if(!SvROK(obj_ref)){ /* it can be an object ID */
		if(!looks_like_number(obj_ref)){ /* looks like an ID? */
			Perl_croak(aTHX_ INVALID_OBJECT, obj_ref);
		}

		if(!HF_CREATE_KEY(action)){ /* fetch, exists, delete */
			return 0;
		}
		else{ /* store, lvalue fetch */
			dMY_CXT;
			SV** const svp = av_fetch(ObjectRegistry, (I32)SvIV(obj_ref), FALSE);

			if(!svp){
				Perl_croak(aTHX_ INVALID_OBJECT, obj_ref);
			}

			/* retrieve object from ID */
			assert(SvIOK(*svp));
			obj = INT2PTR(SV*, SvIVX(*svp));
			obj_ref = NULL;
		}
	}
	else{
		obj = SvRV(obj_ref);
	}

	assert(!SvIS_FREED(obj));

	key_mg = fieldhash_key_mg(obj);
	if(!key_mg){ /* first access */
		if(!HF_CREATE_KEY(action)){ /* fetch, exists, delete */
			/* replace the key with a sv that is not a registered ID */
			mg->mg_obj = &PL_sv_no;
			return 0;
		}
		else{ /* store, lvalue fetch */
			dMY_CXT;
			SV* const obj_id      = hf_new_id(aTHX_ aMY_CXT);
			SV* const obj_weakref = newSViv(PTR2IV(obj));

			av_store(ObjectRegistry, (I32)SvIVX(obj_id), obj_weakref);

			mg->mg_obj = obj_id; /* key replacement */

			reg = newAV(); /* field registry for obj */

			key_mg = sv_magicext(
				obj,
				(SV*)reg,
				PERL_MAGIC_ext,
				&fieldhash_key_vtbl,
				(char*)obj_id,
				HEf_SVKEY
			);

			SvREFCNT_dec(reg);    /* refcnt++ in sv_magicext() */
		}
	}
	else{
		/* key_mg->mg_ptr is obj_id */
		mg->mg_obj = (SV*)key_mg->mg_ptr; /* key replacement */

		if(!HF_CREATE_KEY(action)){
			return 0;
		}

		reg = (AV*)key_mg->mg_obj;
		assert(SvTYPE(reg) == SVt_PVAV);
	}

	/* add a new fieldhash to the field registry if needed */
	if(!hf_av_find(aTHX_ reg, (SV*)fieldhash)){
		av_push(reg, (SV*)SvREFCNT_inc_simple_NN(fieldhash));
	}

	return 0;
}

static int
fieldhash_key_free(pTHX_ SV* const sv, MAGIC* const mg){
	PERL_UNUSED_ARG(sv);

	//warn("key_free(sv=0x%p, mg=0x%p, id=%"SVf")", sv, mg, (SV*)mg->mg_ptr);

	/*
		Does nothing during global destruction, because
		some data may have been released.
	*/
	if(!PL_dirty){
		dMY_CXT;
		AV* const reg    = (AV*)mg->mg_obj; /* field registry */
		SV* const obj_id = (SV*)mg->mg_ptr;
		I32 const len    = AvFILLp(reg)+1;
		I32 i;

		assert(SvTYPE(reg) == SVt_PVAV);


		/* delete $fieldhash{$obj} for each fieldhash */
		for(i = 0; i < len; i++){
			HV* const fieldhash = (HV*)AvARRAY(reg)[i];
			assert(SvTYPE(fieldhash) == SVt_PVHV);

			/* NOTE: Don't use G_DISCARD, because it may cause
			         a double-free problem (t/11_panic_malloc.t).
			*/
			(void)hv_delete_ent(fieldhash, obj_id, 0, 0U);
		}

		av_delete(ObjectRegistry, (I32)SvIVX(obj_id), G_DISCARD);
		hf_free_id(aTHX_ aMY_CXT_ obj_id);
	}

	return 0;
}

MGVTBL hf_accessor_vtbl;

XS(XS_Hash__FieldHash_accessor);
XS(XS_Hash__FieldHash_accessor){
	dVAR; dXSARGS;
	SV* const obj_ref   = ST(0);
	MAGIC* const mg     = mg_find_by_vtbl((SV*)cv, &hf_accessor_vtbl);
	HV* const fieldhash = (HV*)mg->mg_obj;

	if(items < 1 || !SvROK(obj_ref)){
		Perl_croak(aTHX_ "The %s() method must be called as an instance method", GvNAME(CvGV(cv)));
	}
	if(items > 2){
		Perl_croak(aTHX_ "Cannot set a list of values to \"%s\"", GvNAME(CvGV(cv)));
	}

	if(items == 1){ /* get */
		ST(0) = fieldhash_fetch(aTHX_ fieldhash, obj_ref);
	}
	else{ /* set */
		fieldhash_store(aTHX_ fieldhash, obj_ref, newSVsv(ST(1)));
		/* returns self */
	}
	XSRETURN(1);
}


static HV*
hf_get_named_fields(pTHX_ HV* const stash, const char** const pkg_ptr, I32* const pkglen_ptr){
	dMY_CXT;
	const char* const pkg  = HvNAME_get(stash);
	I32 const pkglen       = HvNAMELEN_get(stash);
	SV** const svp         = hv_fetch(NameRegistry, pkg, pkglen, FALSE);
	HV* fields;

	if(!svp){
		fields = newHV();

		(void)hv_store(NameRegistry, pkg, pkglen, newRV_noinc((SV*)fields), 0U);
		NameRegistryIsStale = TRUE;
	}
	else{
		assert(SvROK(*svp));
		fields = (HV*)SvRV(*svp);
		assert(SvTYPE(fields) == SVt_PVHV);
	}

	if(NameRegistryIsStale){
		AV* const isa = mro_get_linear_isa(stash);
		I32 const len = AvFILLp(isa)+1;
		I32 i;
		for(i = 1 /* skip this class */; i < len; i++){
			HE* const he          = hv_fetch_ent(NameRegistry, AvARRAY(isa)[i], FALSE, 0U);
			HV* const base_fields = he && SvROK(HeVAL(he)) ? (HV*)SvRV(HeVAL(he)) : NULL;

			if(base_fields){
				char* key;
				I32   keylen;
				SV*   val;
				hv_iterinit(base_fields);
				while((val = hv_iternextsv(base_fields, &key, &keylen))){
					(void)hv_store(fields, key, keylen, newSVsv(val), 0U);
				}
			}
		}
	}

	if(pkg_ptr)    *pkg_ptr    = pkg;
	if(pkglen_ptr) *pkglen_ptr = pkglen;

	return fields;
}

static void
hf_add_field(pTHX_ HV* const fieldhash, SV* const name, SV* const package){
	if(name){
		dMY_CXT;
		HV* const stash = package ? gv_stashsv(package, TRUE) : CopSTASH(PL_curcop);
		I32         pkglen;
		const char* pkg;
		HV* const fields = hf_get_named_fields(aTHX_ stash, &pkg, &pkglen);
		STRLEN namelen;
		const char* namepv = SvPV_const(name, namelen);
		CV* xsub;

		if(hv_exists_ent(fields, name, 0U) && ckWARN(WARN_REDEFINE)){
			Perl_warner(aTHX_ packWARN(WARN_REDEFINE), "field \"%"SVf"\" redefined or overridden", name);
		}

		(void)hv_store_ent(fields, name, newRV_inc((SV*)fieldhash), 0U);

		namepv   = Perl_form(aTHX_ "%s::%s", pkg, namepv); /* fully qualified name */
		namelen += sizeof("::")-1 + pkglen;
		(void)hv_store(fields, namepv, namelen, newRV_inc((SV*)fieldhash), 0U);

		if(ckWARN(WARN_REDEFINE) && get_cv(namepv, 0x00)){
			Perl_warner(aTHX_ packWARN(WARN_REDEFINE),
				"Subroutine %s redefined", namepv);
		}

		xsub = newXS( (char*)namepv, XS_Hash__FieldHash_accessor, __FILE__);
		sv_magicext(
			(SV*)xsub,
			(SV*)fieldhash,
			PERL_MAGIC_ext,
			&hf_accessor_vtbl,
			NULL,
			0
		);
		CvMETHOD_on(xsub);

		NameRegistryIsStale = TRUE;
	}
}

MODULE = Hash::FieldHash	PACKAGE = Hash::FieldHash

PROTOTYPES: DISABLE

BOOT:
{
	MY_CXT_INIT;
	ObjectRegistry = get_av(OBJECT_REGISTRY_KEY, GV_ADDMULTI);
	NameRegistry   = get_hv(  NAME_REGISTRY_KEY, GV_ADDMULTI);
	LastId         = -1;
}

#ifdef USE_ITHREADS

void
CLONE(...)
CODE:
	MY_CXT_CLONE;

	ObjectRegistry = get_av(OBJECT_REGISTRY_KEY, GV_ADDMULTI);
	NameRegistry   = get_hv(  NAME_REGISTRY_KEY, GV_ADDMULTI);
	FreeId         = NULL;
	PERL_UNUSED_VAR(items);

#endif /* !USE_ITHREADS */

#ifndef HF_USE_TIE

void
fieldhash(HV* hash, SV* name = NULL, SV* package = NULL)
PROTOTYPE: \%;$$
CODE:
	assert(SvTYPE(hash) >= SVt_PVMG);
	if(!fieldhash_mg((SV*)hash)){
		hv_clear(hash);
		sv_magic((SV*)hash,
			NULL,                     /* mg_obj */
			PERL_MAGIC_uvar,          /* mg_type */
			(char*)&fieldhash_ufuncs, /* mg_ptr as the ufuncs table */
			0                         /* mg_len (0 indicates static data) */
		);

		hf_add_field(aTHX_ hash, name, package);
	}

#else /* HF_USE_TIE */

INCLUDE: compat58.xsi

#endif


#ifdef FIELDHASH_DEBUG

void
_dump_internals()
PREINIT:
	dMY_CXT;
	SV* obj_id;
CODE:
	for(obj_id = FreeId; obj_id; obj_id = INT2PTR(SV*, SvIVX(obj_id))){
		sv_dump(obj_id);
	}

HV*
_name_registry()
PREINIT:
	dMY_CXT;
CODE:
	RETVAL = NameRegistry;
OUTPUT:
	RETVAL

#endif /* !FIELDHASH_DEBUG */


void
from_hash(SV* object, ...)
PREINIT:
	const char* stashname;
	HV*   stash;
	HV*   fields;
INIT:
	if(!sv_isobject(object)){
		Perl_croak(aTHX_ "The %s() method must be called as an instance method", GvNAME(CvGV(cv)));
	}
CODE:
	stash  = SvSTASH(SvRV(object));
	fields = hf_get_named_fields(aTHX_ stash, &stashname, NULL);

	if(items == 2){
		SV* const arg = ST(1);
		HV* hv;
		char* key;
		I32   keylen;
		SV*   val;

		if(!(SvROK(arg) && SvTYPE(SvRV(arg)) == SVt_PVHV)){
			Perl_croak(aTHX_ "Single parameters to %s() must be a HASH reference", GvNAME(CvGV(cv)));
		}

		hv = (HV*)SvRV(arg);
		hv_iterinit(hv);
		while((val = hv_iternextsv(hv, &key, &keylen))){
			SV** const svp = hv_fetch(fields, key, keylen, FALSE);

			if(!(svp && SvROK(*svp))){
				Perl_croak(aTHX_ "No such field \"%s\" for %s", key, stashname);
			}

			fieldhash_store(aTHX_ (HV*)SvRV(*svp), object, newSVsv(val));
		}
	}
	else{
		I32 i;

		if( ((items-1) % 2) != 0 ){
			Perl_croak(aTHX_ "Odd number of parameters for %s()", GvNAME(CvGV(cv)));
		}

		for(i = 1; i < items; i += 2){
			HE* const he = hv_fetch_ent(fields, ST(i), FALSE, 0U);

			if(!(he && SvROK(HeVAL(he)))){
				Perl_croak(aTHX_ "No such field \"%s\" for %s", SvPV_nolen_const(ST(i)), stashname);
			}

			fieldhash_store(aTHX_ (HV*)SvRV(HeVAL(he)), object, newSVsv(ST(i+1)));
		}
	}
	XSRETURN(1); /* returns the first argument */

HV*
to_hash(SV* object, ...)
PREINIT:
	HV*   stash;
	HV*   fields;
	char* key;
	I32   keylen;
	SV*   val;
	bool  fully_qualify = FALSE;
INIT:
	if(!sv_isobject(object)){
		Perl_croak(aTHX_ "The %s() method must be called as an instance method", GvNAME(CvGV(cv)));
	}
	while(items > 1){
		SV* const option = ST(--items);

		if(SvOK(option)){
			if(strEQ(SvPV_nolen_const(option), "-fully_qualify")){
				fully_qualify = TRUE;
			}
			else{
				Perl_croak(aTHX_ "Unknown option \"%"SVf"\"", option);
			}
		}
	}
CODE:
	stash = SvSTASH(SvRV(object));
	fields = hf_get_named_fields(aTHX_ stash, NULL, NULL);
	RETVAL = newHV();

	hv_iterinit(fields);
	while((val = hv_iternextsv(fields, &key, &keylen))){
		bool const need_to_store = strchr(key, ':') ? fully_qualify : !fully_qualify;
		if( need_to_store && SvROK(val) ){
			HV* const fieldhash = (HV*)SvRV(val);
			SV* const value     = fieldhash_fetch(aTHX_ fieldhash, object);
			(void)hv_store(RETVAL, key, keylen, newSVsv(value), 0U);
		}
	}
OUTPUT:
	RETVAL