/*
 * parselsb2rdf.c
 * The parser.
 *
 * Copyright (C) Leni Mayo 2002
 *
 *	This program is free software; you can redistribute it and/or modify
 *	it under the terms of the GNU General Public License, version 2, as
 *	published by the Free Software Foundation.
 *
 *	This program is distributed in the hope that it will be useful,
 *	but WITHOUT ANY WARRANTY; without even the implied warranty of
 *	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
 *	GNU General Public License for more details.
 *
 *	You should have received a copy of the GNU General Public License
 *	along with this program; if not, write to the Free Software
 *	Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */

#include <stdlib.h>
#include <ctype.h>
#include <string.h>

#include "servercommon.h"
#include "serverconfig.h"
#include "rdfcommon.h"
#include "parselsb2rdf.h"

typedef enum
{
	eFsmInitial,
	eFsmEnclosed,
	eFsmFinished
} Fsm;

/*
 * See:
 * http://www.linuxbase.org/spec/refspecs/LSB_1.2.0/gLSB.html#INITSCRCOMCONV
 * for the definition of the syntax of the sh comments.
 *
 * Essentially the comments are of the form:
 *	key : value1, value2, ...
 *
 */
typedef enum
{
	eKeyIgnore        = 0,
	eKeyContinuation  = 1,
	eKeyProvides      = 2,
	eKeyRequiredStart = 3,
	eKeyRequiredStop  = 4,
	                       /* the following are extensions to the Linux Standards Base syntax */
	eKeySerelRuntime  = 5, /* script calls serel provide/need directly    */
	eKeySerelLast     = 6, /* script starts after all others are finished */
	eKeySerelLate     = 7, /* script starts before 'last', but after any other script with a dependency */
	eKeyLast          = 8  /* not used, except to size array           */
} Key;

typedef enum
{
	eOccurNotPossible,
	eOccurDontCare,
	eOccurMustHave,
	eOccurCantHave
} ValidOccurrence;

typedef struct
{
	int found;
	GSList *pList;
} KeywordTable;

static Fsm  parse_comment_line(Fsm oldstate, Fsm *pNewState, FILE *fpIn, FILE *fpOut, Key *pKey, GSList **ppList);

static int  generate_xml(FILE *fp, KeywordTable table[], const char *filename);
static void generate_xml_for_edges(FILE *fp, GSList *pNodes, GSList *pEdges);
static void generate_xml_for_edge_statements(FILE *fp, GSList *pNodes, GSList *pEdges);

static const char *stringFromKey(Key key);
static Key keywordStringToKey(const char *psz);
static void stream_to_word_list(FILE *fp, GSList **ppList, int *pIsContinuation);
static ValidOccurrence validPair(Key row, Key col);
static void mangledollar(GSList *pList);

ParseBootResult parseLsbToRdf(FILE *fpin, FILE *fpout, const char *filename)
{
	int ch;
	GSList *pList;
	Fsm state = eFsmInitial;
	Key key;
	Key oldkey = eKeyIgnore;
	KeywordTable table[eKeyLast];
	int fUseListToGenerate;
	int i;
	ParseBootResult ret = eParseBootEmpty;

	assert(fpin && fpout);

	for (i = 0; i < eKeyLast; i++)
	{
		table[i].found = FALSE;
		table[i].pList = NULL;
	}

	while (!feof(fpin) && state != eFsmFinished && ret != eParseBootFailed)
	{
		ch = fgetc(fpin);

		if (ch == '#')
		{
			ungetc(ch, fpin);

			pList = NULL;
			fUseListToGenerate = TRUE;

			ret = parse_comment_line(state, &state, fpin, fpout, &key, &pList);

			if (state == eFsmEnclosed)
			{
				if (key != eKeyIgnore)
				{
					if (key == eKeyContinuation)
					{
						key = oldkey;
					}

					fUseListToGenerate = TRUE;
				}

				if (key != eKeyContinuation)
				{
					oldkey = key;
				}
			}

			if (fUseListToGenerate)
			{
				table[key].pList = g_slist_concat(table[key].pList, pList);
				table[key].found = TRUE;
			}
			else
			{
				free_list_and_items(pList);
			}
		}
		else
		{
			while (!feof(fpin) && ch != '\n')
			{
				ch = fgetc(fpin);
			}
		}
	}

	if ((!table[eKeyProvides].found || table[eKeyProvides].pList == NULL) && !table[eKeySerelRuntime].found)
	{
		ret = eParseBootEmpty;
	}
	else if (ret == eParseBootSuccess)
	{
		int fSuccess = TRUE;
		int j;
		ValidOccurrence v;
		char bufMsg[1000];

		for (i = 0; fSuccess && i < eKeyLast; i++)
		{
			for (j = 0; fSuccess && j < eKeyLast; j++)
			{
				v = validPair(i, j);

				switch(v)
				{
					case eOccurMustHave:
						if (table[i].found && !table[j].found)
						{
							snprintf(bufMsg, sizeof(bufMsg),
								"Error: keyword %s must be accompanied by keyword %s", 
								stringFromKey(i), stringFromKey(j) ); 

							fSuccess = FALSE;
						}
						break;
					case eOccurCantHave:
						if (table[i].found && table[j].found)
						{
							snprintf(bufMsg, sizeof(bufMsg),
								"Error: can't have keywords %s and %s appearing in the same script", 
								stringFromKey(i), stringFromKey(j) ); 

							fSuccess = FALSE;
						}
						break;
					case eOccurDontCare:
					case eOccurNotPossible:
						break;
					default:
						assert(NOTHERE);
				}

				if (!fSuccess)
				{
					fprintf(fpout, "<!-- %s -->\n", bufMsg);
				}
			}
		}
		
		fSuccess = fSuccess && generate_xml(fpout, table, filename);

		ret = fSuccess ? eParseBootSuccess : eParseBootFailed;
	}

	for (i = 0; i < eKeyLast; i++)
	{
		free_list_and_items(table[i].pList);
	}

	return ret;
}

ParseBootResult parse_comment_line(Fsm oldstate, Fsm *pNewState, FILE *fpIn, FILE *fpOut, Key *pKey, GSList **ppList)
{
	Fsm newstate = oldstate;
	int fContinuation;
	ParseBootResult ret = eParseBootSuccess;

	assert(oldstate != eFsmFinished);

	*pKey = eKeyIgnore;

	stream_to_word_list(fpIn, ppList, &fContinuation);

#ifdef TAKENOUT
	fprintf(stdout, "Inside parse_comment_line(), *ppList == ");
	stringsInListToFp(stdout, *ppList);
	fprintf(stdout, "\n");
#endif

	if (oldstate == eFsmInitial)
	{
		if (g_slist_length(*ppList) >= 4 && 
			(strcmp((const char *) g_slist_nth_data(*ppList, 1), "BEGIN") == 0) &&
			(strcmp((const char *) g_slist_nth_data(*ppList, 2), "INIT")  == 0) &&
			(strcmp((const char *) g_slist_nth_data(*ppList, 3), "INFO")  == 0))
		{
			newstate = eFsmEnclosed;
		}
	}
	else
	{
		assert(oldstate == eFsmEnclosed);

		if (g_slist_length(*ppList) >= 4 && 
			(strcmp((const char *) g_slist_nth_data(*ppList, 1), "END")   == 0) &&
			(strcmp((const char *) g_slist_nth_data(*ppList, 2), "INIT")  == 0) &&
			(strcmp((const char *) g_slist_nth_data(*ppList, 3), "INFO")  == 0))
		{
			newstate = eFsmFinished;
		}
		else if (g_slist_length(*ppList) >= 2)
		{
			if (fContinuation)
			{
				*pKey = eKeyContinuation;

				*ppList = remove_and_free_list_head(*ppList);
			}

			if (g_slist_length(*ppList) >= 2)
			{
				Key key = keywordStringToKey((const char *) g_slist_nth_data(*ppList, 1));

				if (key != eKeyLast)
				{
					*pKey = key;
				}
				else
				{
					ret = eParseBootFailed;
					fprintf(fpOut, "<!-- Error: Unrecognised keyword: %s -->\n", (const char *) g_slist_nth_data(*ppList, 1));
				}

				/*
				 * remove the '#' and 'keyword:', leaving just the facility names (if any)
				 */
				*ppList = remove_and_free_list_head(*ppList);
				*ppList = remove_and_free_list_head(*ppList);
			}

			mangledollar(*ppList);
		}
	}

	*pNewState = newstate;

	return ret;
}

/*
 * GSList * stream_to_word_list(FILE *fp)
 *	Take a line from fpin and break it up into words.
 *
 *	Pass back to the caller a (new) GSList of (new) strings.
 *	Also pass back to the caller a word comprising the white space
 *	between the first and second words, needed for the caller
 *	to figure out whether this is a continuation line or not.
 */
void stream_to_word_list(FILE *fp, GSList **ppList, int *pIsContinuation)
{
	char *p;
	char buf[1024];
	int i;
	int cStart = -1;
	int fFoundEol = FALSE;
	int fFirstFgets = TRUE;
	
	*pIsContinuation = FALSE;

	buf[0] = '\0';

	while (!feof(fp) && !fFoundEol)
	{
		fgets(buf, sizeof(buf), fp);

		if (fFirstFgets)
		{
			fFirstFgets = FALSE;

			/* printf("in stream_to_word_list(), buf = %s\n", buf); */

			if (strncmp(buf, "#  ", 3) == 0 || strncmp(buf, "#\t", 2) == 0)
			{
				*pIsContinuation = TRUE;
				/* printf("found continuation line\n"); */
			}
		}

		for (i = 0; i < sizeof(buf) && buf[i] != '\0'; i++)
		{
			if (buf[i] == ' ' || buf[i] == '\t' || buf[i] == '\n')
			{
				if (buf[i] == '\n')
				{
					fFoundEol = TRUE;
				}

				if (cStart != -1)
				{
					buf[i] = '\0';
					p = strdup(&(buf[cStart]));
					*ppList = g_slist_append(*ppList, p);

					/* printf("stream_to_word_list() adding word to list: %s\n", p); */

					cStart = -1;
				}
			}
			else
			{
				if (cStart == -1)
				{
					cStart = i;
				}
			}
		}
	}
}

int generate_xml(FILE *fp, KeywordTable table[], const char *filename)
{
	int ret = TRUE;
	int fOpenService = FALSE;

#ifdef TAKENOUT
	int i;

	printf("Inside generate_xml()\n");

	for (i = eKeyProvides; i <= eKeyRequiredStop; i++)
	{
		printf("Inside generate_xml(), key == %d, word list == ", i);
		stringsInListToFp(stdout, table[i].pList);
		printf("\n");
	}
#endif

	/*
	 * Do Nodes first.
	 * Even though the lsb syntax allows for multiple "facility" names,
	 * the current version of serel rdf syntax exactly one.
	 * Which is why we don't iterate the table[eKeyProvides].pList ...
	 */

	if (table[eKeyProvides].found)
	{
		if (table[eKeyProvides].pList)
		{
			assert(table[eKeyProvides].pList->data);

			fprintf(fp, "<%s rdf:ID=\"%s\">\n",
					rdfPropertyFragment(eGraphService),
					(const char *) table[eKeyProvides].pList->data);

			fOpenService = TRUE;
		}
		else
		{
			fprintf(fp, "<!-- Error: \"# Provides: \" must be followed with the name of a service - aborting parse for %s -->\n", filename);

			ret = FALSE;
		}
	}
	else if (table[eKeySerelRuntime].found)
	{
		fprintf(fp, "<%s serel:%s=\"true\">\n", rdfPropertyFragment(eGraphService), rdfPropertyFragment(eSerelRuntime));
	}
	else
	{
		fprintf(fp, "<!-- Error: A comment with keyword of \"Provides\" or \"X-serel-runtime\" must be present - aborting parse for %s -->\n", filename);

		ret = FALSE;
	}

	if (ret && table[eKeySerelLast].found)
	{
		fprintf(fp, "<serel:%s>true</serel:%s>\n", rdfPropertyFragment(eSerelLast), rdfPropertyFragment(eSerelLast));
	}

	if (ret && table[eKeySerelLate].found)
	{
		fprintf(fp, "<serel:%s>true</serel:%s>\n", rdfPropertyFragment(eSerelLate), rdfPropertyFragment(eSerelLate));
	}

	if (ret && table[eKeyRequiredStart].found)
	{
		if (table[eKeyRequiredStart].pList == NULL)
		{
			fprintf(fp, "<!-- Error: If \"Required-Start\" is present it must be followed with the name of a service - aborting parse for %s -->\n", filename);

			ret = FALSE;
		}
		else
		{
			fprintf(fp, "  <edges>\n");
			fprintf(fp, "    <rdf:Seq>\n");

			generate_xml_for_edges(fp, table[eKeyProvides].pList, table[eKeyRequiredStart].pList);

			fprintf(fp, "    </rdf:Seq>\n");
			fprintf(fp, "  </edges>\n");
		}

		fprintf(fp, "</%s>\n", rdfPropertyFragment(eGraphService));
		fprintf(fp, "\n");

		fOpenService = FALSE;

		generate_xml_for_edge_statements(fp, table[eKeyProvides].pList, table[eKeyRequiredStart].pList);
	}

	if (fOpenService)
	{
		fprintf(fp, "</%s>\n", rdfPropertyFragment(eGraphService));
	}

    /* RequiredStop isn't implemented in the server, so ignore it for the mo.
	*/

	return ret;
}

void generate_xml_for_edges(FILE *fp, GSList *pNodes, GSList *pEdges)
{
	GSList *pNodeElement, *pEdgeElement;
	char buf[200];

	assert(fp && pNodes && pEdges);

	for (pNodeElement = pNodes; pNodeElement != NULL; pNodeElement = pNodeElement->next)
	{
		for (pEdgeElement = pEdges; pEdgeElement != NULL; pEdgeElement = pEdgeElement->next)
		{
			assert(pEdgeElement->data);

			fprintf(fp, "      <rdf:li rdf:resource=\"#%s\" />\n",
			      edgeFragmentFromPair(buf, sizeof(buf), 
				      (const char *) pEdgeElement->data,
			          (const char *) pNodeElement->data ) );
		}
	}
}

void generate_xml_for_edge_statements(FILE *fp, GSList *pNodes, GSList *pEdges)
{
	GSList *pNodeElement, *pEdgeElement;
	char buf[200];

	assert(fp);

	for (pNodeElement = pNodes; pNodeElement != NULL; pNodeElement = pNodeElement->next)
	{
		for (pEdgeElement = pEdges; pEdgeElement != NULL; pEdgeElement = pEdgeElement->next)
		{
			assert(pEdgeElement->data);

			fprintf(fp, "<edge rdf:ID=\"%s\" >\n",
			      edgeFragmentFromPair(buf, sizeof(buf), 
				      (const char *) pEdgeElement->data,
			          (const char *) pNodeElement->data) );
			fprintf(fp, "  <source rdf:resource=\"#%s\" />\n", (const char *) pEdgeElement->data);
			fprintf(fp, "  <target rdf:resource=\"#%s\" />\n", (const char *) pNodeElement->data);
			fprintf(fp, "</edge>\n");
		}
	}
}

const char *stringFromKey(Key key)
{
	static int initialised = FALSE;
	static const char *table[eKeyLast];
	int i;

	if (!initialised)
	{
		for (i = 0; i < eKeyLast; i++)
			table[i] = NULL;

		table[eKeyProvides]      = "Provides";
		table[eKeyRequiredStart] = "Required-Start";
		table[eKeyRequiredStop]  = "Required-Stop";
		table[eKeySerelRuntime]  = "X-serel-runtime";
		table[eKeySerelLast]     = "X-serel-last";
		table[eKeySerelLate]     = "X-serel-late";

		initialised = TRUE;
	}

	return table[key];
}

/*
 * Key keywordStringToKey(const char *psz)
 *	Note that the input string will likely have a trailing colon,
 *	but the routine uses strncasecmp() so that it only needs
 *	to match however many characters are in the able below.
 *	Is lazy, but easy...
 */
Key keywordStringToKey(const char *psz)
{
	Key key = eKeyLast;
	int i;

	for (i = 0; i < eKeyLast; i++)
	{
		if (stringFromKey(i) == NULL)
			continue;

		if (strncasecmp(psz, stringFromKey(i), strlen(stringFromKey(i))) == 0)
		{
			key = i;
			break;
		}
	}

	return key;
}

/*
 *	             Provides    Req-Start    Req-Stop   runtime   late   last
 *	Provides        X           DC           DC        CH       DC     DC
 *	Req-Start       MH          X            DC        CH       CH     CH
 *	Req-Stop        MH          DC           X         CH       CH     CH
 *	runtime         CH          CH           CH         X       CH     CH
 *	late            MH          CH           CH        CH       X      CH
 *	last            MH          CH           CH        CH       CH      X
 *
 *	Legend:
 *		X  - not possible
 *		DC - for this row item, we don't care whether the column is present
 *		CH - for this row item, we can't have the column present
 *		MH - for this row item, we must  have the column present
 */

ValidOccurrence validPair(Key row, Key col)
{
	static ValidOccurrence table[eKeyLast][eKeyLast];
	static int initialised = FALSE;

	if (!initialised)
	{
		int i, j;

		for (i = 0; i < eKeyLast; i++)
			for (j = 0; j < eKeyLast; j++)
				table[i][j] = eOccurNotPossible;

		i = eKeyProvides;
		table[i][eKeyRequiredStart] = eOccurDontCare;
		table[i][eKeyRequiredStop]  = eOccurDontCare;
		table[i][eKeySerelRuntime]  = eOccurCantHave;
		table[i][eKeySerelLate]     = eOccurDontCare;
		table[i][eKeySerelLast]     = eOccurDontCare;

		i = eKeyRequiredStart;
		table[i][eKeyProvides]      = eOccurMustHave;
		table[i][eKeyRequiredStop]  = eOccurDontCare;
		table[i][eKeySerelRuntime]  = eOccurCantHave;
		table[i][eKeySerelLate]     = eOccurCantHave;
		table[i][eKeySerelLast]     = eOccurCantHave;

		i = eKeyRequiredStop;
		table[i][eKeyProvides]      = eOccurMustHave;
		table[i][eKeyRequiredStart] = eOccurDontCare;
		table[i][eKeySerelRuntime]  = eOccurCantHave;
		table[i][eKeySerelLate]     = eOccurCantHave;
		table[i][eKeySerelLast]     = eOccurCantHave;

		i = eKeySerelRuntime;
		table[i][eKeyProvides]      = eOccurCantHave;
		table[i][eKeyRequiredStart] = eOccurCantHave;
		table[i][eKeyRequiredStop]  = eOccurCantHave;
		table[i][eKeySerelLate]     = eOccurCantHave;
		table[i][eKeySerelLast]     = eOccurCantHave;

		i = eKeySerelLate;
		table[i][eKeyProvides]      = eOccurMustHave;
		table[i][eKeyRequiredStart] = eOccurCantHave;
		table[i][eKeyRequiredStop]  = eOccurCantHave;
		table[i][eKeySerelRuntime]  = eOccurCantHave;
		table[i][eKeySerelLast]     = eOccurCantHave;

		i = eKeySerelLast;
		table[i][eKeyProvides]      = eOccurMustHave;
		table[i][eKeyRequiredStart] = eOccurCantHave;
		table[i][eKeyRequiredStop]  = eOccurCantHave;
		table[i][eKeySerelRuntime]  = eOccurCantHave;
		table[i][eKeySerelLate]     = eOccurCantHave;

		initialised = TRUE;
	}

	return table[row][col];
}

void mangledollar(GSList *pList)
{
	GSList *pElement;
	char *pszOld, *pszNew;
	typedef enum { eMangleDollarStrip, eMangleDollarLsbPrefix } MangleDollar;
	MangleDollar mangle;

	if (strcmp("strip", configAsString(eConfigKeyMangleDollar)) == 0)
	{
		mangle = eMangleDollarStrip;
	}
	else if (strcmp("lsb_prefix", configAsString(eConfigKeyMangleDollar)) == 0)
	{
		mangle = eMangleDollarLsbPrefix;
	}
	else
	{
		/*
		 * Hm, some unknown method of handling mangling...
		 * would normally do printLog() except this routine
		 * is also called from lsb2rdf which doesn't interact with
		 * the log (yet).  Meantime, silently ignore...
		 */

		return;
	}

	for (pElement = pList; pElement != NULL; pElement = pElement->next)
	{
		assert(pElement && pElement->data);

		pszOld = (char *) pElement->data;

		if (pszOld[0] == '$')
		{
			if (mangle == eMangleDollarStrip)
			{
				pszNew = strdup(pszOld + 1);
			}
			else
			{
				const char *pszLsbPrefix = "lsb_";

				pszNew = malloc(strlen(pszOld) + strlen(pszLsbPrefix) + 1);

				strcpy(pszNew, pszLsbPrefix);
				strcat(pszNew, pszOld+1);
			}

			free(pszOld);
			pElement->data = pszNew;
		}
	}

#ifdef TAKENOUT 
	printf("After implementng mangledollar, pList is: ");
	stringsInListToFp(stdout, pList);
	printf("\n");
#endif
}
