/*--------------------------------------------------------------------------
 *
 * test_tidstore.c
 *		Test TidStore data structure.
 *
 * Note: all locking in this test module is useless since there is only
 * a single process to use the TidStore. It is meant to be an example of
 * usage.
 *
 * Copyright (c) 2024-2025, PostgreSQL Global Development Group
 *
 * IDENTIFICATION
 *		src/test/modules/test_tidstore/test_tidstore.c
 *
 * -------------------------------------------------------------------------
 */
#include "postgres.h"

#include "access/tidstore.h"
#include "fmgr.h"
#include "storage/block.h"
#include "storage/itemptr.h"
#include "storage/lwlock.h"
#include "utils/array.h"
#include "utils/memutils.h"

PG_MODULE_MAGIC;

PG_FUNCTION_INFO_V1(test_create);
PG_FUNCTION_INFO_V1(do_set_block_offsets);
PG_FUNCTION_INFO_V1(check_set_block_offsets);
PG_FUNCTION_INFO_V1(test_is_full);
PG_FUNCTION_INFO_V1(test_destroy);

static TidStore *tidstore = NULL;
static size_t tidstore_empty_size;

/* array for verification of some tests */
typedef struct ItemArray
{
	ItemPointerData *insert_tids;
	ItemPointerData *lookup_tids;
	ItemPointerData *iter_tids;
	int			max_tids;
	int			num_tids;
} ItemArray;

static ItemArray items;

/* comparator routine for ItemPointer */
static int
itemptr_cmp(const void *left, const void *right)
{
	BlockNumber lblk,
				rblk;
	OffsetNumber loff,
				roff;

	lblk = ItemPointerGetBlockNumber((ItemPointer) left);
	rblk = ItemPointerGetBlockNumber((ItemPointer) right);

	if (lblk < rblk)
		return -1;
	if (lblk > rblk)
		return 1;

	loff = ItemPointerGetOffsetNumber((ItemPointer) left);
	roff = ItemPointerGetOffsetNumber((ItemPointer) right);

	if (loff < roff)
		return -1;
	if (loff > roff)
		return 1;

	return 0;
}

/*
 * Create a TidStore. If shared is false, the tidstore is created
 * on TopMemoryContext, otherwise on DSA. Although the tidstore
 * is created on DSA, only the same process can subsequently use
 * the tidstore. The tidstore handle is not shared anywhere.
*/
Datum
test_create(PG_FUNCTION_ARGS)
{
	bool		shared = PG_GETARG_BOOL(0);
	MemoryContext old_ctx;

	/* doesn't really matter, since it's just a hint */
	size_t		tidstore_max_size = 2 * 1024 * 1024;
	size_t		array_init_size = 1024;

	Assert(tidstore == NULL);

	/*
	 * Create the TidStore on TopMemoryContext so that the same process use it
	 * for subsequent tests.
	 */
	old_ctx = MemoryContextSwitchTo(TopMemoryContext);

	if (shared)
	{
		int			tranche_id;

		tranche_id = LWLockNewTrancheId();
		LWLockRegisterTranche(tranche_id, "test_tidstore");

		tidstore = TidStoreCreateShared(tidstore_max_size, tranche_id);

		/*
		 * Remain attached until end of backend or explicitly detached so that
		 * the same process use the tidstore for subsequent tests.
		 */
		dsa_pin_mapping(TidStoreGetDSA(tidstore));
	}
	else
		/* VACUUM uses insert only, so we test the other option. */
		tidstore = TidStoreCreateLocal(tidstore_max_size, false);

	tidstore_empty_size = TidStoreMemoryUsage(tidstore);

	items.num_tids = 0;
	items.max_tids = array_init_size / sizeof(ItemPointerData);
	items.insert_tids = (ItemPointerData *) palloc0(array_init_size);
	items.lookup_tids = (ItemPointerData *) palloc0(array_init_size);
	items.iter_tids = (ItemPointerData *) palloc0(array_init_size);

	MemoryContextSwitchTo(old_ctx);

	PG_RETURN_VOID();
}

static void
sanity_check_array(ArrayType *ta)
{
	if (ARR_HASNULL(ta) && array_contains_nulls(ta))
		ereport(ERROR,
				(errcode(ERRCODE_NULL_VALUE_NOT_ALLOWED),
				 errmsg("array must not contain nulls")));

	if (ARR_NDIM(ta) > 1)
		ereport(ERROR,
				(errcode(ERRCODE_DATA_EXCEPTION),
				 errmsg("argument must be empty or one-dimensional array")));
}

static void
check_tidstore_available(void)
{
	if (tidstore == NULL)
		elog(ERROR, "tidstore is not created");
}

static void
purge_from_verification_array(BlockNumber blkno)
{
	int			dst = 0;

	for (int src = 0; src < items.num_tids; src++)
		if (ItemPointerGetBlockNumber(&items.insert_tids[src]) != blkno)
			items.insert_tids[dst++] = items.insert_tids[src];
	items.num_tids = dst;
}


/* Set the given block and offsets pairs */
Datum
do_set_block_offsets(PG_FUNCTION_ARGS)
{
	BlockNumber blkno = PG_GETARG_INT64(0);
	ArrayType  *ta = PG_GETARG_ARRAYTYPE_P_COPY(1);
	OffsetNumber *offs;
	int			noffs;

	check_tidstore_available();
	sanity_check_array(ta);

	noffs = ArrayGetNItems(ARR_NDIM(ta), ARR_DIMS(ta));
	offs = ((OffsetNumber *) ARR_DATA_PTR(ta));

	/* Set TIDs in the store */
	TidStoreLockExclusive(tidstore);
	TidStoreSetBlockOffsets(tidstore, blkno, offs, noffs);
	TidStoreUnlock(tidstore);

	/* Remove the existing items of blkno from the verification array */
	purge_from_verification_array(blkno);

	/* Set TIDs in verification array */
	for (int i = 0; i < noffs; i++)
	{
		ItemPointer tid;
		int			idx = items.num_tids + i;

		/* Enlarge the TID arrays if necessary */
		if (idx >= items.max_tids)
		{
			items.max_tids *= 2;
			items.insert_tids = repalloc(items.insert_tids, sizeof(ItemPointerData) * items.max_tids);
			items.lookup_tids = repalloc(items.lookup_tids, sizeof(ItemPointerData) * items.max_tids);
			items.iter_tids = repalloc(items.iter_tids, sizeof(ItemPointerData) * items.max_tids);
		}

		tid = &(items.insert_tids[idx]);
		ItemPointerSet(tid, blkno, offs[i]);
	}

	/* Update statistics */
	items.num_tids += noffs;

	PG_RETURN_INT64(blkno);
}

/*
 * Verify TIDs in store against the array.
 */
Datum
check_set_block_offsets(PG_FUNCTION_ARGS)
{
	TidStoreIter *iter;
	TidStoreIterResult *iter_result;
	int			num_iter_tids = 0;
	int			num_lookup_tids = 0;
	BlockNumber prevblkno = 0;

	check_tidstore_available();

	/* lookup each member in the verification array */
	for (int i = 0; i < items.num_tids; i++)
		if (!TidStoreIsMember(tidstore, &items.insert_tids[i]))
			elog(ERROR, "missing TID with block %u, offset %u",
				 ItemPointerGetBlockNumber(&items.insert_tids[i]),
				 ItemPointerGetOffsetNumber(&items.insert_tids[i]));

	/*
	 * Lookup all possible TIDs for each distinct block in the verification
	 * array and save successful lookups in the lookup array.
	 */

	for (int i = 0; i < items.num_tids; i++)
	{
		BlockNumber blkno = ItemPointerGetBlockNumber(&items.insert_tids[i]);

		if (i > 0 && blkno == prevblkno)
			continue;

		for (OffsetNumber offset = FirstOffsetNumber; offset < MaxOffsetNumber; offset++)
		{
			ItemPointerData tid;

			ItemPointerSet(&tid, blkno, offset);

			TidStoreLockShare(tidstore);
			if (TidStoreIsMember(tidstore, &tid))
				ItemPointerSet(&items.lookup_tids[num_lookup_tids++], blkno, offset);
			TidStoreUnlock(tidstore);
		}

		prevblkno = blkno;
	}

	/* Collect TIDs stored in the tidstore, in order */

	TidStoreLockShare(tidstore);
	iter = TidStoreBeginIterate(tidstore);
	while ((iter_result = TidStoreIterateNext(iter)) != NULL)
	{
		OffsetNumber offsets[MaxOffsetNumber];
		int			num_offsets;

		num_offsets = TidStoreGetBlockOffsets(iter_result, offsets, lengthof(offsets));
		Assert(num_offsets <= lengthof(offsets));
		for (int i = 0; i < num_offsets; i++)
			ItemPointerSet(&(items.iter_tids[num_iter_tids++]), iter_result->blkno,
						   offsets[i]);
	}
	TidStoreEndIterate(iter);
	TidStoreUnlock(tidstore);

	/*
	 * Sort verification and lookup arrays and test that all arrays are the
	 * same.
	 */

	if (num_lookup_tids != items.num_tids)
		elog(ERROR, "should have %d TIDs, have %d", items.num_tids, num_lookup_tids);
	if (num_iter_tids != items.num_tids)
		elog(ERROR, "should have %d TIDs, have %d", items.num_tids, num_iter_tids);

	qsort(items.insert_tids, items.num_tids, sizeof(ItemPointerData), itemptr_cmp);
	qsort(items.lookup_tids, items.num_tids, sizeof(ItemPointerData), itemptr_cmp);
	for (int i = 0; i < items.num_tids; i++)
	{
		if (itemptr_cmp(&items.insert_tids[i], &items.iter_tids[i]) != 0)
			elog(ERROR, "TID iter array doesn't match verification array, got (%u,%u) expected (%u,%u)",
				 ItemPointerGetBlockNumber(&items.iter_tids[i]),
				 ItemPointerGetOffsetNumber(&items.iter_tids[i]),
				 ItemPointerGetBlockNumber(&items.insert_tids[i]),
				 ItemPointerGetOffsetNumber(&items.insert_tids[i]));
		if (itemptr_cmp(&items.insert_tids[i], &items.lookup_tids[i]) != 0)
			elog(ERROR, "TID lookup array doesn't match verification array, got (%u,%u) expected (%u,%u)",
				 ItemPointerGetBlockNumber(&items.lookup_tids[i]),
				 ItemPointerGetOffsetNumber(&items.lookup_tids[i]),
				 ItemPointerGetBlockNumber(&items.insert_tids[i]),
				 ItemPointerGetOffsetNumber(&items.insert_tids[i]));
	}

	PG_RETURN_VOID();
}

/*
 * In real world use, we care if the memory usage is greater than
 * some configured limit. Here we just want to verify that
 * TidStoreMemoryUsage is not broken.
 */
Datum
test_is_full(PG_FUNCTION_ARGS)
{
	bool		is_full;

	check_tidstore_available();

	is_full = (TidStoreMemoryUsage(tidstore) > tidstore_empty_size);

	PG_RETURN_BOOL(is_full);
}

/* Free the tidstore */
Datum
test_destroy(PG_FUNCTION_ARGS)
{
	check_tidstore_available();

	TidStoreDestroy(tidstore);
	tidstore = NULL;
	items.num_tids = 0;
	pfree(items.insert_tids);
	pfree(items.lookup_tids);
	pfree(items.iter_tids);

	PG_RETURN_VOID();
}
