#include "postgres.h"

#include <math.h>

#include "fmgr.h"
#include "plperl.h"
#include "plperl_helpers.h"
#include "utils/fmgrprotos.h"
#include "utils/jsonb.h"

PG_MODULE_MAGIC;

static SV  *Jsonb_to_SV(JsonbContainer *jsonb);
static JsonbValue *SV_to_JsonbValue(SV *obj, JsonbParseState **ps, bool is_elem);


static SV  *
JsonbValue_to_SV(JsonbValue *jbv)
{
	dTHX;

	switch (jbv->type)
	{
		case jbvBinary:
			return Jsonb_to_SV(jbv->val.binary.data);

		case jbvNumeric:
			{
				char	   *str = DatumGetCString(DirectFunctionCall1(numeric_out,
																	  NumericGetDatum(jbv->val.numeric)));
				SV		   *result = newSVnv(SvNV(cstr2sv(str)));

				pfree(str);
				return result;
			}

		case jbvString:
			{
				char	   *str = pnstrdup(jbv->val.string.val,
										   jbv->val.string.len);
				SV		   *result = cstr2sv(str);

				pfree(str);
				return result;
			}

		case jbvBool:
			return newSVnv(SvNV(jbv->val.boolean ? &PL_sv_yes : &PL_sv_no));

		case jbvNull:
			return newSV(0);

		default:
			elog(ERROR, "unexpected jsonb value type: %d", jbv->type);
			return NULL;
	}
}

static SV  *
Jsonb_to_SV(JsonbContainer *jsonb)
{
	dTHX;
	JsonbValue	v;
	JsonbIterator *it;
	JsonbIteratorToken r;

	it = JsonbIteratorInit(jsonb);
	r = JsonbIteratorNext(&it, &v, true);

	switch (r)
	{
		case WJB_BEGIN_ARRAY:
			if (v.val.array.rawScalar)
			{
				JsonbValue	tmp;

				if ((r = JsonbIteratorNext(&it, &v, true)) != WJB_ELEM ||
					(r = JsonbIteratorNext(&it, &tmp, true)) != WJB_END_ARRAY ||
					(r = JsonbIteratorNext(&it, &tmp, true)) != WJB_DONE)
					elog(ERROR, "unexpected jsonb token: %d", r);

				return JsonbValue_to_SV(&v);
			}
			else
			{
				AV		   *av = newAV();

				while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
				{
					if (r == WJB_ELEM)
						av_push(av, JsonbValue_to_SV(&v));
				}

				return newRV((SV *) av);
			}

		case WJB_BEGIN_OBJECT:
			{
				HV		   *hv = newHV();

				while ((r = JsonbIteratorNext(&it, &v, true)) != WJB_DONE)
				{
					if (r == WJB_KEY)
					{
						/* json key in v, json value in val */
						JsonbValue	val;

						if (JsonbIteratorNext(&it, &val, true) == WJB_VALUE)
						{
							SV		   *value = JsonbValue_to_SV(&val);

							(void) hv_store(hv,
											v.val.string.val, v.val.string.len,
											value, 0);
						}
					}
				}

				return newRV((SV *) hv);
			}

		default:
			elog(ERROR, "unexpected jsonb token: %d", r);
			return NULL;
	}
}

static JsonbValue *
AV_to_JsonbValue(AV *in, JsonbParseState **jsonb_state)
{
	dTHX;
	SSize_t		pcount = av_len(in) + 1;
	SSize_t		i;

	pushJsonbValue(jsonb_state, WJB_BEGIN_ARRAY, NULL);

	for (i = 0; i < pcount; i++)
	{
		SV		  **value = av_fetch(in, i, FALSE);

		if (value)
			(void) SV_to_JsonbValue(*value, jsonb_state, true);
	}

	return pushJsonbValue(jsonb_state, WJB_END_ARRAY, NULL);
}

static JsonbValue *
HV_to_JsonbValue(HV *obj, JsonbParseState **jsonb_state)
{
	dTHX;
	JsonbValue	key;
	SV		   *val;
	char	   *kstr;
	I32			klen;

	key.type = jbvString;

	pushJsonbValue(jsonb_state, WJB_BEGIN_OBJECT, NULL);

	(void) hv_iterinit(obj);

	while ((val = hv_iternextsv(obj, &kstr, &klen)))
	{
		key.val.string.val = pnstrdup(kstr, klen);
		key.val.string.len = klen;
		pushJsonbValue(jsonb_state, WJB_KEY, &key);
		(void) SV_to_JsonbValue(val, jsonb_state, false);
	}

	return pushJsonbValue(jsonb_state, WJB_END_OBJECT, NULL);
}

static JsonbValue *
SV_to_JsonbValue(SV *in, JsonbParseState **jsonb_state, bool is_elem)
{
	dTHX;
	JsonbValue	out;			/* result */

	/* Dereference references recursively. */
	while (SvROK(in))
		in = SvRV(in);

	switch (SvTYPE(in))
	{
		case SVt_PVAV:
			return AV_to_JsonbValue((AV *) in, jsonb_state);

		case SVt_PVHV:
			return HV_to_JsonbValue((HV *) in, jsonb_state);

		default:
			if (!SvOK(in))
			{
				out.type = jbvNull;
			}
			else if (SvUOK(in))
			{
				/*
				 * If UV is >=64 bits, we have no better way to make this
				 * happen than converting to text and back.  Given the low
				 * usage of UV in Perl code, it's not clear it's worth working
				 * hard to provide alternate code paths.
				 */
				const char *strval = SvPV_nolen(in);

				out.type = jbvNumeric;
				out.val.numeric =
					DatumGetNumeric(DirectFunctionCall3(numeric_in,
														CStringGetDatum(strval),
														ObjectIdGetDatum(InvalidOid),
														Int32GetDatum(-1)));
			}
			else if (SvIOK(in))
			{
				IV			ival = SvIV(in);

				out.type = jbvNumeric;
				out.val.numeric =
					DatumGetNumeric(DirectFunctionCall1(int8_numeric,
														Int64GetDatum((int64) ival)));
			}
			else if (SvNOK(in))
			{
				double		nval = SvNV(in);

				/*
				 * jsonb doesn't allow infinity or NaN (per JSON
				 * specification), but the numeric type that is used for the
				 * storage accepts NaN, so we have to prevent it here
				 * explicitly.  We don't really have to check for isinf()
				 * here, as numeric doesn't allow it and it would be caught
				 * later, but it makes for a nicer error message.
				 */
				if (isinf(nval))
					ereport(ERROR,
							(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
							 errmsg("cannot convert infinity to jsonb")));
				if (isnan(nval))
					ereport(ERROR,
							(errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE),
							 errmsg("cannot convert NaN to jsonb")));

				out.type = jbvNumeric;
				out.val.numeric =
					DatumGetNumeric(DirectFunctionCall1(float8_numeric,
														Float8GetDatum(nval)));
			}
			else if (SvPOK(in))
			{
				out.type = jbvString;
				out.val.string.val = sv2cstr(in);
				out.val.string.len = strlen(out.val.string.val);
			}
			else
			{
				/*
				 * XXX It might be nice if we could include the Perl type in
				 * the error message.
				 */
				ereport(ERROR,
						(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
						 errmsg("cannot transform this Perl type to jsonb")));
				return NULL;
			}
	}

	/* Push result into 'jsonb_state' unless it is a raw scalar. */
	return *jsonb_state
		? pushJsonbValue(jsonb_state, is_elem ? WJB_ELEM : WJB_VALUE, &out)
		: memcpy(palloc(sizeof(JsonbValue)), &out, sizeof(JsonbValue));
}


PG_FUNCTION_INFO_V1(jsonb_to_plperl);

Datum
jsonb_to_plperl(PG_FUNCTION_ARGS)
{
	dTHX;
	Jsonb	   *in = PG_GETARG_JSONB_P(0);
	SV		   *sv = Jsonb_to_SV(&in->root);

	return PointerGetDatum(sv);
}


PG_FUNCTION_INFO_V1(plperl_to_jsonb);

Datum
plperl_to_jsonb(PG_FUNCTION_ARGS)
{
	dTHX;
	JsonbParseState *jsonb_state = NULL;
	SV		   *in = (SV *) PG_GETARG_POINTER(0);
	JsonbValue *out = SV_to_JsonbValue(in, &jsonb_state, true);
	Jsonb	   *result = JsonbValueToJsonb(out);

	PG_RETURN_JSONB_P(result);
}
