/*
 * This file and its contents are licensed under the Apache License 2.0.
 * Please see the included NOTICE for copyright information and
 * LICENSE-APACHE for a copy of the license.
 */
#include <postgres.h>
#include <catalog/namespace.h>
#include <utils/builtins.h>
#include <utils/catcache.h>
#include <utils/lsyscache.h>

#include "cache.h"
#include "dimension.h"
#include "errors.h"
#include "hypertable.h"
#include "hypertable_cache.h"
#include "scanner.h"
#include "ts_catalog/catalog.h"
#include "ts_catalog/tablespace.h"

static void *hypertable_cache_create_entry(Cache *cache, CacheQuery *query);
static void hypertable_cache_missing_error(const Cache *cache, const CacheQuery *query);

typedef struct HypertableCacheQuery
{
	CacheQuery q;
	Oid relid;
	const char *schema;
	const char *table;
} HypertableCacheQuery;

static void *
hypertable_cache_get_key(CacheQuery *query)
{
	return &((HypertableCacheQuery *) query)->relid;
}

typedef struct
{
	Oid relid;
	Hypertable *hypertable;
} HypertableCacheEntry;

static bool
hypertable_cache_valid_result(const void *result)
{
	if (result == NULL)
		return false;
	return ((HypertableCacheEntry *) result)->hypertable != NULL;
}

static Cache *
hypertable_cache_create()
{
	MemoryContext ctx =
		AllocSetContextCreate(CacheMemoryContext, "Hypertable cache", ALLOCSET_DEFAULT_SIZES);

	Cache *cache = MemoryContextAlloc(ctx, sizeof(Cache));
	Cache		template =
	{
		.hctl =
		{
			.keysize = sizeof(Oid),
			.entrysize = sizeof(HypertableCacheEntry),
			.hcxt = ctx,
		},
		.name = "hypertable_cache",
		.numelements = 16,
		.flags = HASH_ELEM | HASH_CONTEXT | HASH_BLOBS,
		.get_key = hypertable_cache_get_key,
		.create_entry = hypertable_cache_create_entry,
		.missing_error = hypertable_cache_missing_error,
		.valid_result = hypertable_cache_valid_result,
	};

	*cache = template;

	ts_cache_init(cache);

	return cache;
}

static Cache *hypertable_cache_current = NULL;

static ScanTupleResult
hypertable_tuple_found(TupleInfo *ti, void *data)
{
	HypertableCacheEntry *entry = data;

	entry->hypertable = ts_hypertable_from_tupleinfo(ti);
	return SCAN_DONE;
}

static void *
hypertable_cache_create_entry(Cache *cache, CacheQuery *query)
{
	HypertableCacheQuery *hq = (HypertableCacheQuery *) query;
	HypertableCacheEntry *cache_entry = query->result;
	int number_found;

	if (NULL == hq->schema)
		hq->schema = get_namespace_name(get_rel_namespace(hq->relid));

	if (NULL == hq->table)
		hq->table = get_rel_name(hq->relid);

	number_found = ts_hypertable_scan_with_memory_context(hq->schema,
														  hq->table,
														  hypertable_tuple_found,
														  query->result,
														  AccessShareLock,
														  ts_cache_memory_ctx(cache));

	switch (number_found)
	{
		case 0:
			/* Negative cache entry: table is not a hypertable */
			cache_entry->hypertable = NULL;
			break;
		case 1:
			Assert(strncmp(NameStr(cache_entry->hypertable->fd.schema_name),
						   hq->schema,
						   NAMEDATALEN) == 0);
			Assert(strncmp(NameStr(cache_entry->hypertable->fd.table_name),
						   hq->table,
						   NAMEDATALEN) == 0);
			break;
		default:
			elog(ERROR, "got an unexpected number of records: %d", number_found);
			break;
	}

	return cache_entry->hypertable == NULL ? NULL : cache_entry;
}

static void
hypertable_cache_missing_error(const Cache *cache, const CacheQuery *query)
{
	HypertableCacheQuery *hq = (HypertableCacheQuery *) query;
	const char *const rel_name = get_rel_name(hq->relid);

	if (rel_name == NULL)
		ereport(ERROR,
				(errcode(ERRCODE_UNDEFINED_TABLE),
				 errmsg("OID %u does not refer to a table", hq->relid)));
	else
		ereport(ERROR,
				(errcode(ERRCODE_TS_HYPERTABLE_NOT_EXIST),
				 errmsg("table \"%s\" is not a hypertable", rel_name)));
}

void
ts_hypertable_cache_invalidate_callback(void)
{
	ts_cache_invalidate(&hypertable_cache_current);
	hypertable_cache_current = hypertable_cache_create();
}

#ifdef TS_DEBUG
TS_FUNCTION_INFO_V1(ts_hypertable_cache_clear);

/*
 * Force a cache clearing. This function is used for debugging purposes.
 */
Datum
ts_hypertable_cache_clear(PG_FUNCTION_ARGS)
{
	ts_hypertable_cache_invalidate_callback();
	PG_RETURN_VOID();
}
#endif

/* Get hypertable cache entry. If the entry is not in the cache, add it. */
Hypertable *
ts_hypertable_cache_get_entry(Cache *const cache, const Oid relid, const unsigned int flags)
{
	if (!OidIsValid(relid))
	{
		if (flags & CACHE_FLAG_MISSING_OK)
			return NULL;
		else
			ereport(ERROR, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("invalid Oid")));
	}

	return ts_hypertable_cache_get_entry_with_table(cache, relid, NULL, NULL, flags);
}

/*
 * Returns cache into the argument and hypertable as the function result.
 * If hypertable is not found, fails with an error.
 */
Hypertable *
ts_hypertable_cache_get_cache_and_entry(const Oid relid, const unsigned int flags,
										Cache **const cache)
{
	*cache = ts_hypertable_cache_pin();
	return ts_hypertable_cache_get_entry(*cache, relid, flags);
}

Hypertable *
ts_hypertable_cache_get_entry_rv(Cache *cache, const RangeVar *rv)
{
	return ts_hypertable_cache_get_entry(cache,
										 RangeVarGetRelid(rv, NoLock, true),
										 CACHE_FLAG_MISSING_OK);
}

TSDLLEXPORT Hypertable *
ts_hypertable_cache_get_entry_by_id(Cache *cache, const int32 hypertable_id)
{
	return ts_hypertable_cache_get_entry(cache,
										 ts_hypertable_id_to_relid(hypertable_id, true),
										 CACHE_FLAG_MISSING_OK);
}

Hypertable *
ts_hypertable_cache_get_entry_with_table(Cache *cache, const Oid relid, const char *schema,
										 const char *table, const unsigned int flags)
{
	HypertableCacheQuery query = {
		.q.flags = flags,
		.relid = relid,
		.schema = schema,
		.table = table,
	};
	HypertableCacheEntry *entry = ts_cache_fetch(cache, &query.q);
	Assert((flags & CACHE_FLAG_MISSING_OK) ? true : (entry != NULL && entry->hypertable != NULL));
	return entry == NULL ? NULL : entry->hypertable;
}

extern TSDLLEXPORT Cache *
ts_hypertable_cache_pin()
{
	return ts_cache_pin(hypertable_cache_current);
}

void
_hypertable_cache_init(void)
{
	CreateCacheMemoryContext();
	hypertable_cache_current = hypertable_cache_create();
}

void
_hypertable_cache_fini(void)
{
	ts_cache_invalidate(&hypertable_cache_current);
}
