/*
 * timetravel.c --	function to get time travel feature
 *		using general triggers.
 */

#include "executor/spi.h"		/* this is what you need to work with SPI */
#include "commands/trigger.h"	/* -"- and triggers */
#include <ctype.h>				/* tolower () */

#define ABSTIMEOID	702			/* it should be in pg_type.h */

AbsoluteTime	currabstime(void);
HeapTuple		timetravel(void);
int32			set_timetravel(Name relname, int32 on);

typedef struct
{
	char   *ident;
	void   *splan;
} EPlan;

static EPlan *Plans = NULL;		/* for UPDATE/DELETE */
static int	nPlans = 0;

static char	  **TTOff = NULL;
static int		nTTOff = 0;

static EPlan *find_plan(char *ident, EPlan ** eplan, int *nplans);

/*
 * timetravel () -- 
 *		1.	IF an update affects tuple with stop_date eq INFINITY 
 * 			then form (and return) new tuple with stop_date eq current date 
 * 			and all other column values as in old tuple, and insert tuple 
 * 			with new data and start_date eq current date and 
 * 			stop_date eq INFINITY
 * 			ELSE - skip updation of tuple.
 * 		2.	IF an delete affects tuple with stop_date eq INFINITY
 * 			then insert the same tuple with stop_date eq current date
 * 			ELSE - skip deletion of tuple.
 * 		3.	On INSERT, if start_date is NULL then current date will be
 * 			inserted, if stop_date is NULL then INFINITY will be inserted.
 * 
 * In CREATE TRIGGER you are to specify start_date and stop_date column
 * names:
 * EXECUTE PROCEDURE
 * timetravel ('date_on', 'date_off').
 */

HeapTuple						/* have to return HeapTuple to Executor */
timetravel()
{
	Trigger    *trigger;		/* to get trigger name */
	char	  **args;			/* arguments */
	int			attnum[2];		/* fnumbers of start/stop columns */
	Datum		oldon, oldoff;
	Datum		newon, newoff;
	Datum	   *cvals;			/* column values */
	char	   *cnulls;			/* column nulls */
	char	   *relname;		/* triggered relation name */
	Relation	rel;			/* triggered relation */
	HeapTuple	trigtuple;
	HeapTuple	newtuple = NULL;
	HeapTuple	rettuple;
	TupleDesc	tupdesc;		/* tuple description */
	int			natts;			/* # of attributes */
	EPlan	   *plan;			/* prepared plan */
	char		ident[2 * NAMEDATALEN];
	bool		isnull;			/* to know is some column NULL or not */
	bool		isinsert = false;
	int			ret;
	int			i;

	/*
	 * Some checks first...
	 */

	/* Called by trigger manager ? */
	if (!CurrentTriggerData)
		elog(WARN, "timetravel: triggers are not initialized");
	
	/* Should be called for ROW trigger */
	if (TRIGGER_FIRED_FOR_STATEMENT(CurrentTriggerData->tg_event))
		elog(WARN, "timetravel: can't process STATEMENT events");
	
	/* Should be called BEFORE */
	if (TRIGGER_FIRED_AFTER(CurrentTriggerData->tg_event))
		elog(WARN, "timetravel: must be fired before event");

	/* INSERT ? */
	if (TRIGGER_FIRED_BY_INSERT(CurrentTriggerData->tg_event))
		isinsert = true;
	
	if (TRIGGER_FIRED_BY_UPDATE(CurrentTriggerData->tg_event))
		newtuple = CurrentTriggerData->tg_newtuple;
	
	trigtuple = CurrentTriggerData->tg_trigtuple;
	
	rel = CurrentTriggerData->tg_relation;
	relname = SPI_getrelname(rel);
	
	/* check if TT is OFF for this relation */
	for (i = 0; i < nTTOff; i++)
		if (strcasecmp (TTOff[i], relname) == 0)
			break;
	if (i < nTTOff)				/* OFF - nothing to do */
	{
		pfree (relname);
		return ((newtuple != NULL) ? newtuple : trigtuple);
	}
	
	trigger = CurrentTriggerData->tg_trigger;

	if (trigger->tgnargs != 2)
		elog(WARN, "timetravel (%s): invalid (!= 2) number of arguments %d", 
				relname, trigger->tgnargs);
	
	args = trigger->tgargs;
	tupdesc = rel->rd_att;
	natts = tupdesc->natts;
	
	/*
	 * Setting CurrentTriggerData to NULL prevents direct calls to trigger
	 * functions in queries. Normally, trigger functions have to be called
	 * by trigger manager code only.
	 */
	CurrentTriggerData = NULL;
	
	for (i = 0; i < 2; i++ )
	{
		attnum[i] = SPI_fnumber (tupdesc, args[i]);
		if ( attnum[i] < 0 )
			elog(WARN, "timetravel (%s): there is no attribute %s", relname, args[i]);
		if (SPI_gettypeid (tupdesc, attnum[i]) != ABSTIMEOID)
			elog(WARN, "timetravel (%s): attributes %s and %s must be of abstime type", 
					relname, args[0], args[1]);
	}
	
	if (isinsert)								/* INSERT */
	{
		int		chnattrs = 0;
		int		chattrs[2];
		Datum	newvals[2];
		
		oldon = SPI_getbinval (trigtuple, tupdesc, attnum[0], &isnull);
		if (isnull)
		{
			newvals[chnattrs] = GetCurrentAbsoluteTime ();
			chattrs[chnattrs] = attnum[0];
			chnattrs++;
		}
		
		oldoff = SPI_getbinval (trigtuple, tupdesc, attnum[1], &isnull);
		if (isnull)
		{
			if ((chnattrs == 0 && DatumGetInt32 (oldon) >= NOEND_ABSTIME) || 
				(chnattrs > 0 && DatumGetInt32 (newvals[0]) >= NOEND_ABSTIME))
				elog (WARN, "timetravel (%s): %s ge %s", 
						relname, args[0], args[1]);
			newvals[chnattrs] = NOEND_ABSTIME;
			chattrs[chnattrs] = attnum[1];
			chnattrs++;
		}
		else
		{
			if ((chnattrs == 0 && DatumGetInt32 (oldon) >= 
										DatumGetInt32 (oldoff)) || 
				(chnattrs > 0 && DatumGetInt32 (newvals[0]) >= 
										DatumGetInt32 (oldoff)))
				elog (WARN, "timetravel (%s): %s ge %s", 
						relname, args[0], args[1]);
		}
		
		pfree (relname);
		if ( chnattrs <= 0 )
			return (trigtuple);
		
		rettuple = SPI_modifytuple (rel, trigtuple, chnattrs, 
									chattrs, newvals, NULL);
		return (rettuple);
	}
	
	oldon = SPI_getbinval (trigtuple, tupdesc, attnum[0], &isnull);
	if (isnull)
		elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[0]);
	
	oldoff = SPI_getbinval (trigtuple, tupdesc, attnum[1], &isnull);
	if (isnull)
		elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[1]);
	/*
	 * If DELETE/UPDATE of tuple with stop_date neq INFINITY
	 * then say upper Executor to skip operation for this tuple
	 */
	if (newtuple != NULL)						/* UPDATE */
	{
		newon = SPI_getbinval (newtuple, tupdesc, attnum[0], &isnull);
		if (isnull)
			elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[0]);
		newoff = SPI_getbinval (newtuple, tupdesc, attnum[1], &isnull);
		if (isnull)
			elog(WARN, "timetravel (%s): %s must be NOT NULL", relname, args[1]);
		
		if ( oldon != newon || oldoff != newoff )
			elog (WARN, "timetravel (%s): you can't change %s and/or %s columns (use set_timetravel)",
					relname, args[0], args[1]);
		
		if ( newoff != NOEND_ABSTIME )
		{
			pfree (relname);	/* allocated in upper executor context */
			return (NULL);
		}
	}
	else if (oldoff != NOEND_ABSTIME)		/* DELETE */
	{
		pfree (relname);
		return (NULL);
	}
	
	newoff = GetCurrentAbsoluteTime ();
	
	/* Connect to SPI manager */
	if ((ret = SPI_connect()) < 0)
		elog(WARN, "timetravel (%s): SPI_connect returned %d", relname, ret);
	
	/* Fetch tuple values and nulls */
	cvals = (Datum *) palloc (natts * sizeof (Datum));
	cnulls = (char *) palloc (natts * sizeof (char));
	for (i = 0; i < natts; i++)
	{
		cvals[i] = SPI_getbinval ((newtuple != NULL) ? newtuple : trigtuple, 
								  tupdesc, i + 1, &isnull);
		cnulls[i] = (isnull) ? 'n' : ' ';
	}
	
	/* change date column(s) */
	if (newtuple)					/* UPDATE */
	{
		cvals[attnum[0] - 1] = newoff;			/* start_date eq current date */
		cnulls[attnum[0] - 1] = ' ';
		cvals[attnum[1] - 1] = NOEND_ABSTIME;	/* stop_date eq INFINITY */
		cnulls[attnum[1] - 1] = ' ';
	}
	else							/* DELETE */
	{
		cvals[attnum[1] - 1] = newoff;			/* stop_date eq current date */
		cnulls[attnum[1] - 1] = ' ';
	}
	
	/*
	 * Construct ident string as TriggerName $ TriggeredRelationId 
	 * and try to find prepared execution plan.
	 */
	sprintf(ident, "%s$%u", trigger->tgname, rel->rd_id);
	plan = find_plan(ident, &Plans, &nPlans);
	
	/* if there is no plan ... */
	if (plan->splan == NULL)
	{
		void	   *pplan;
		Oid		   *ctypes;
		char		sql[8192];
		
		/* allocate ctypes for preparation */
		ctypes = (Oid *) palloc(natts * sizeof(Oid));
		
		/*
		 * Construct query: 
		 * 	INSERT INTO _relation_ VALUES ($1, ...)
		 */
		sprintf(sql, "INSERT INTO %s VALUES (", relname);
		for (i = 1; i <= natts; i++)
		{
			sprintf(sql + strlen(sql), "$%d%s",
				i, (i < natts) ? ", " : ")");
			ctypes[i - 1] = SPI_gettypeid(tupdesc, i);
		}
		
		/* Prepare plan for query */
		pplan = SPI_prepare(sql, natts, ctypes);
		if (pplan == NULL)
			elog(WARN, "timetravel (%s): SPI_prepare returned %d", relname, SPI_result);
		
		/*
		 * Remember that SPI_prepare places plan in current memory context
		 * - so, we have to save plan in Top memory context for latter
		 * use.
		 */
		pplan = SPI_saveplan(pplan);
		if (pplan == NULL)
			elog(WARN, "timetravel (%s): SPI_saveplan returned %d", relname, SPI_result);
		
		plan->splan = pplan;
	}
	
	/*
	 * Ok, execute prepared plan.
	 */
	ret = SPI_execp(plan->splan, cvals, cnulls, 0);
	
	if (ret < 0)
		elog(WARN, "timetravel (%s): SPI_execp returned %d", relname, ret);
	
	/* Tuple to return to upper Executor ... */
	if (newtuple)									/* UPDATE */
	{
		HeapTuple	tmptuple;
		
		tmptuple = SPI_copytuple (trigtuple);
		rettuple = SPI_modifytuple (rel, tmptuple, 1, &(attnum[1]), &newoff, NULL);
		/*
		 * SPI_copytuple allocates tmptuple in upper executor context -
		 * have to free allocation using SPI_pfree
		 */
		SPI_pfree (tmptuple);
	}
	else											/* DELETE */
		rettuple = trigtuple;
	
	SPI_finish();		/* don't forget say Bye to SPI mgr */
	
	pfree (relname);

	return (rettuple);
}

/*
 * set_timetravel () --
 * 					turn timetravel for specified relation ON/OFF
 */
int32
set_timetravel(Name relname, int32 on)
{
	char   *rname;
	char   *d;
	char   *s;
	int		i;
	
	for (i = 0; i < nTTOff; i++)
		if (namestrcmp (relname, TTOff[i]) == 0)
			break;
	
	if (i < nTTOff)				/* OFF currently */
	{
		if (on == 0)
			return (0);
		
		/* turn ON */
		free (TTOff[i]);
		if (nTTOff == 1)
			free (TTOff);
		else
		{
			if (i < nTTOff - 1)
				memcpy (&(TTOff[i]), &(TTOff[i + 1]), (nTTOff - i) * sizeof (char*));
			TTOff = realloc (TTOff, (nTTOff - 1) * sizeof (char*));
		}
		nTTOff--;
		return (0);
	}
	
	/* ON currently */
	if (on != 0)
		return (1);
	
	/* turn OFF */
	if (nTTOff == 0)
		TTOff = malloc (sizeof (char*));
	else
		TTOff = realloc (TTOff, (nTTOff + 1) * sizeof (char*));
	s = rname = nameout (relname);
	d = TTOff[nTTOff] = malloc (strlen (rname) + 1);
	while (*s)
		*d++ = tolower (*s++);
	*d = 0;
	pfree (rname);
	nTTOff++;
	
	return (1);

}

AbsoluteTime
currabstime ()
{
	return (GetCurrentAbsoluteTime ());
}

static EPlan *
find_plan(char *ident, EPlan ** eplan, int *nplans)
{
	EPlan	   *newp;
	int			i;

	if (*nplans > 0)
	{
		for (i = 0; i < *nplans; i++)
		{
			if (strcmp((*eplan)[i].ident, ident) == 0)
				break;
		}
		if (i != *nplans)
			return (*eplan + i);
		*eplan = (EPlan *) realloc(*eplan, (i + 1) * sizeof(EPlan));
		newp = *eplan + i;
	}
	else
	{
		newp = *eplan = (EPlan *) malloc(sizeof(EPlan));
		(*nplans) = i = 0;
	}

	newp->ident = (char *) malloc(strlen(ident) + 1);
	strcpy(newp->ident, ident);
	newp->splan = NULL;
	(*nplans)++;

	return (newp);
}
