/*-------------------------------------------------------------------------
 *
 * fextra_cache
 *
 *			cache with function extra informations - mostly AST based
 *
 * by Pavel Stehule 2013-2026
 *
 *-------------------------------------------------------------------------
 */
#include "postgres.h"
#include "plpgsql.h"
#include "plpgsql_check.h"

#if PG_VERSION_NUM >= 190000

#include "utils/hsearch.h"

#endif

#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"

static HTAB *fextra_ht = NULL;
static MemoryContext fextra_mcxt = NULL;

typedef struct
{
	plch_fextra *fextra;
	int			parentid;
	int		   *naturalid;
	int			current_deep;	/* starts from zero */
} fextra_init_context;

static void
init_fextra_stmt_walker(PLpgSQL_stmt *stmt, fextra_init_context *context)
{
	fextra_init_context loccontext;
	plch_fextra *fextra = context->fextra;
	int			stmtid = stmt->stmtid;

	/*
	 * statement ids are starts by one. For simplicity don't change base to
	 * zero.
	 */
	fextra->parentids[stmtid] = context->parentid;
	fextra->naturalids[stmtid] = ++(*context->naturalid);
	fextra->levels[stmtid] = context->current_deep + 1;

	if (context->current_deep > fextra->max_deep)
		fextra->max_deep = context->current_deep;

	loccontext.fextra = fextra;
	loccontext.parentid = stmtid;
	loccontext.naturalid = context->naturalid;
	loccontext.current_deep = context->current_deep + 1;

	plch_statement_tree_walker(stmt, init_fextra_stmt_walker, NULL, &loccontext);
}

void
plch_init_fidentity_hk(plch_fidentity_hk *hk, PLpgSQL_function *func)
{
	memset(hk, 0, sizeof(plch_fidentity_hk));

	hk->fn_oid = func->fn_oid;
	hk->db_oid = MyDatabaseId;

	hk->fn_xmin = plch_func_xmin(func);
	hk->fn_tid = plch_func_tid(func);
}

static void
pin_func(PLpgSQL_function *func)
{
	/*
	 * We cannot to pin inline block due two reasons:
	 *
	 * 1. After error, the Assert(func->cfunc.use_count == 0); is executed
	 * before exec memory is released and our abort callback is raised.
	 *
	 * 2. There is not any reason, why to do it. inline block uses short life
	 * contexts for everything - including fextra, and then fextra cannot be
	 * corrupted. Unfortunately inside plpgsql_free_function_memory is
	 * free_stmt called before MemoryContextDelete(func->fn_cxt)
	 */
	Assert(OidIsValid(func->fn_oid));

	plch_use_count(func)++;
}

static void
unpin_func(PLpgSQL_function *func)
{
	Assert(OidIsValid(func->fn_oid));

	plch_use_count(func)--;
}


static void
fextra_CacheObjectCallback(Datum arg, int cacheid, uint32 hashValue)
{
	HASH_SEQ_STATUS status;
	plch_fextra *fextra;

	if (!fextra_ht)
		return;

	/* Currently we just flush all entries; hard to be smarter ... */
	hash_seq_init(&status, fextra_ht);

	while ((fextra = (plch_fextra *) hash_seq_search(&status)) != NULL)
	{
		if (hashValue == 0 || fextra->hashValue == hashValue)
			fextra->is_valid = false;

		if (!fextra->is_valid && fextra->use_count == 0)
		{
			MemoryContextDelete(fextra->mcxt);
			if (hash_search(fextra_ht,
							&fextra->hk,
							HASH_REMOVE, NULL) == NULL)
				elog(ERROR, "hash table corrupted");
		}
	}
}

plch_fextra *
plch_get_fextra(PLpgSQL_function *func)
{
	plch_fextra *fextra;

	if (OidIsValid(func->fn_oid))
	{
		HASHCTL		ctl;
		plch_fidentity_hk hk;
		bool		found;

		/*
		 * Prepare persistent cache - don't do this for anonymous block
		 */
		if (!fextra_mcxt)
		{
			fextra_mcxt = AllocSetContextCreate(TopMemoryContext,
												"plpgsql_check - fextra cache context",
												ALLOCSET_DEFAULT_MINSIZE,
												ALLOCSET_DEFAULT_INITSIZE,
												ALLOCSET_DEFAULT_MAXSIZE);

			Assert(fextra_ht == NULL);

			memset(&ctl, 0, sizeof(ctl));
			ctl.keysize = sizeof(plch_fidentity_hk);
			ctl.entrysize = sizeof(plch_fextra);
			ctl.hcxt = fextra_mcxt;

			fextra_ht = hash_create("plpgsql_check function fextra cache",
									FUNCS_PER_USER,
									&ctl,
									HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);

			CacheRegisterSyscacheCallback(PROCOID, fextra_CacheObjectCallback, (Datum) 0);
		}

		plch_init_fidentity_hk(&hk, func);
		fextra = (plch_fextra *) hash_search(fextra_ht, (void *) &hk, HASH_ENTER, &found);

		if (found && !fextra->is_valid && fextra->use_count == 0)
		{
			MemoryContextReset(fextra->mcxt);
		}

		if (!found)
		{
			fextra->mcxt = AllocSetContextCreate(fextra_mcxt,
												 "PLpgSQL fextra entry context",
												 ALLOCSET_DEFAULT_SIZES);

			fextra->hashValue = GetSysCacheHashValue1(PROCOID,
													  ObjectIdGetDatum(func->fn_oid));

			fextra->use_count = 0;
			fextra->is_valid = false;
		}
	}
	else
	{
		/* one shot fextra for anonymous blocks */
		fextra = palloc0(sizeof(plch_fextra));
		fextra->mcxt = CurrentMemoryContext;
		fextra->use_count = 0;
		fextra->is_valid = false;
	}

	if (!fextra->is_valid && fextra->use_count == 0)
	{
		MemoryContext oldcxt;
		char	   *fn_name = NULL;
		char	   *fn_namespacename = NULL;
		int			naturalid = 0;
		fextra_init_context context;

		if (func->fn_oid)
		{
			fn_name = get_func_name(func->fn_oid);
			if (!fn_name)
				fn_name = func->fn_signature;

			fn_namespacename = get_namespace_name_or_temp(get_func_namespace(func->fn_oid));
		}

		oldcxt = MemoryContextSwitchTo(fextra->mcxt);

		fextra->fn_oid = func->fn_oid;
		fextra->fn_name = fn_name ? pstrdup(fn_name) : NULL;
		fextra->fn_namespacename = fn_namespacename ? pstrdup(fn_namespacename) : NULL;
		fextra->fn_signature = func->fn_signature ? pstrdup(func->fn_signature) : NULL;
		fextra->nstatements = func->nstatements;

		fextra->parentids = palloc(sizeof(int) * (func->nstatements + 1));
		fextra->naturalids = palloc(sizeof(int) * (func->nstatements + 1));
		fextra->levels = palloc(sizeof(int) * (func->nstatements + 1));

		MemoryContextSwitchTo(oldcxt);

		fextra->max_deep = 0;

		context.fextra = fextra;
		context.parentid = 0;
		context.current_deep = 0;
		context.naturalid = &naturalid;

		init_fextra_stmt_walker((PLpgSQL_stmt *) func->action, &context);

		fextra->func = func;

		fextra->is_valid = true;
	}

	if (OidIsValid(func->fn_oid))
		pin_func(fextra->func);

	fextra->use_count++;

	return fextra;
}

void
plch_release_fextra(plch_fextra *fextra)
{
	Assert(fextra->use_count > 0);

	/* until now, referenced PLpgSQL_function should be still valid */
	Assert(fextra->hk.fn_oid == fextra->func->fn_oid);

	if (OidIsValid(fextra->fn_oid))
	{
		/*
		 * until now, referenced PLpgSQL_function should be still valid.
		 * Attention - it is not true, for inline block. Pointer fextra->func
		 * when func is inline block is already invalid.
		 */
		Assert(fextra->hk.fn_oid == fextra->func->fn_oid);

		unpin_func(fextra->func);
	}

	fextra->use_count--;
}


#if PG_VERSION_NUM < 150000

void
plch_fextra_deinit()
{
	if (fextra_mcxt)
	{
		MemoryContextDelete(fextra_mcxt);

		fextra_mcxt = NULL;
		fextra_ht = NULL;
	}
}

#endif
