/*
*   Copyright (c) 2002-2003, Darren Hiebert
*
*   This source code is released for free distribution under the terms of the
*   GNU General Public License.
*
*   This module contains functions for generating tags for PL/SQL language
*   files.
*/

/*
*   INCLUDE FILES
*/
#include "general.h"	/* must always come first */

#include <ctype.h>	/* to define isalpha () */
#include <setjmp.h>
#ifdef DEBUG
#include <stdio.h>
#endif

#include "entry.h"
#include "keyword.h"
#include "parse.h"
#include "main.h"
#include "read.h"
#include "vstring.h"

/*
*   On-line PL/SQL Reference Guide:
*   http://info-it.umsystem.edu/oradocs/doc/server/doc/PLS23/toc.htm
*
*   Sample PL/SQL code is available from:
*   http://www.orafaq.com/faqscrpt.htm#GENPLSQL
*/

/*
*   MACROS
*/
#define isType(token,t)		(boolean) ((token)->type == (t))
#define isKeyword(token,k)	(boolean) ((token)->keyword == (k))

/*
*   DATA DECLARATIONS
*/

typedef enum eException { ExceptionNone, ExceptionEOF } exception_t;

/*  Used to specify type of keyword.
 */
typedef enum eKeywordId {
    KEYWORD_NONE = -1,
    KEYWORD_is,
    KEYWORD_begin,
    KEYWORD_body,
    KEYWORD_cursor,
    KEYWORD_declare,
    KEYWORD_end,
    KEYWORD_function,
    KEYWORD_if,
    KEYWORD_loop,
    KEYWORD_case,
    KEYWORD_for,
    KEYWORD_call,
    KEYWORD_package,
    KEYWORD_pragma,
    KEYWORD_procedure,
    KEYWORD_record,
    KEYWORD_object,
    KEYWORD_ref,
    KEYWORD_rem,
    KEYWORD_return,
    KEYWORD_returns,
    KEYWORD_subtype,
    KEYWORD_table,
    KEYWORD_trigger,
    KEYWORD_type,
    KEYWORD_index,
    KEYWORD_event,
    KEYWORD_publication,
    KEYWORD_service,
    KEYWORD_domain,
    KEYWORD_datatype,
    KEYWORD_result,
    KEYWORD_when,
    KEYWORD_then,
    KEYWORD_variable,
    KEYWORD_exception,
    KEYWORD_at,
    KEYWORD_on,
    KEYWORD_primary,
    KEYWORD_references,
    KEYWORD_unique,
    KEYWORD_check,
    KEYWORD_constraint,
    KEYWORD_foreign,
    KEYWORD_ml_table,
    KEYWORD_ml_conn,
    KEYWORD_local,
    KEYWORD_temporary,
    KEYWORD_drop,
    KEYWORD_view,
    KEYWORD_synonym,
    KEYWORD_handler
} keywordId;

/*  Used to determine whether keyword is valid for the token language and
 *  what its ID is.
 */
typedef struct sKeywordDesc {
    const char *name;
    keywordId id;
} keywordDesc;

typedef enum eTokenType {
    TOKEN_UNDEFINED,
    TOKEN_BLOCK_LABEL_BEGIN,
    TOKEN_BLOCK_LABEL_END,
    TOKEN_CHARACTER,
    TOKEN_CLOSE_PAREN,
    TOKEN_SEMICOLON,
    TOKEN_COMMA,
    TOKEN_IDENTIFIER,
    TOKEN_KEYWORD,
    TOKEN_OPEN_PAREN,
    TOKEN_OPERATOR,
    TOKEN_OTHER,
    TOKEN_STRING,
    TOKEN_PERIOD
} tokenType;

typedef struct sTokenInfo {
    tokenType	type;
    keywordId	keyword;
    vString *	string;
    vString *	scope;
    unsigned long lineNumber;
    MIOPos filePosition;
} tokenInfo;

/*
*   DATA DEFINITIONS
*/

static langType Lang_sql;

static jmp_buf Exception;

typedef enum {
    SQLTAG_CURSOR,
    SQLTAG_PROTOTYPE,
    SQLTAG_FUNCTION,
    SQLTAG_FIELD,
    SQLTAG_LOCAL_VARIABLE,
    SQLTAG_BLOCK_LABEL,
    SQLTAG_PACKAGE,
    SQLTAG_PROCEDURE,
    SQLTAG_RECORD,
    SQLTAG_SUBTYPE,
    SQLTAG_TABLE,
    SQLTAG_TRIGGER,
    SQLTAG_VARIABLE,
    SQLTAG_INDEX,
    SQLTAG_EVENT,
    SQLTAG_PUBLICATION,
    SQLTAG_SERVICE,
    SQLTAG_DOMAIN,
    SQLTAG_VIEW,
    SQLTAG_SYNONYM,
    SQLTAG_MLTABLE,
    SQLTAG_MLCONN,
    SQLTAG_COUNT
} sqlKind;

static kindOption SqlKinds [] = {
    { TRUE,  'c', "cursor",       "cursors"	           },
    { FALSE, 'd', "prototype",    "prototypes"	           },
    { TRUE,  'f', "function",     "functions"	           },
    { TRUE,  'F', "field",        "record fields"	   },
    { FALSE, 'l', "local",        "local variables"        },
    { TRUE,  'L', "label",        "block label"            },
    { TRUE,  'P', "package",      "packages"	           },
    { TRUE,  'n', "namespace",    "procedures"  	   },
    { FALSE, 'r', "record",       "records"     	   },
    { TRUE,  's', "subtype",      "subtypes"    	   },
    { TRUE,  't', "class",        "tables"		   },
    { TRUE,  'T', "macro",        "triggers"	           },
    { TRUE,  'v', "variable",     "variables"   	   },
    { TRUE,  'i', "struct",       "indexes"     	   },
    { TRUE,  'e', "event",        "events"	           },
    { TRUE,  'U', "publication",  "publications"           },
    { TRUE,  'R', "service",      "services"               },
    { TRUE,  'D', "domain",       "domains"                },
    { TRUE,  'm', "member",       "views"                  },
    { TRUE,  'n', "synonym",      "synonyms"               },
    { TRUE,  'x', "mltable",      "MobiLink Table Scripts" },
    { TRUE,  'y', "mlconn",       "MobiLink Conn Scripts"  }
};

static const keywordDesc SqlKeywordTable [] = {
    /* keyword		keyword ID */
    { "as",		KEYWORD_is		        },
    { "begin",		KEYWORD_begin		        },
    { "body",		KEYWORD_body		        },
    { "cursor",		KEYWORD_cursor		        },
    { "declare",	KEYWORD_declare		        },
    { "end",		KEYWORD_end		        },
    { "function",	KEYWORD_function	        },
    { "if",		KEYWORD_if		        },
    { "is",		KEYWORD_is		        },
    { "loop",		KEYWORD_loop		        },
    { "case",		KEYWORD_case		        },
    { "for",		KEYWORD_for		        },
    { "call",		KEYWORD_call		        },
    { "package",	KEYWORD_package		        },
    { "pragma",		KEYWORD_pragma		        },
    { "procedure",	KEYWORD_procedure	        },
    { "record",		KEYWORD_record		        },
    { "object",		KEYWORD_object		        },
    { "ref",		KEYWORD_ref		        },
    { "rem",		KEYWORD_rem		        },
    { "return",		KEYWORD_return		        },
    { "returns",	KEYWORD_returns		        },
    { "subtype",	KEYWORD_subtype		        },
    { "table",		KEYWORD_table		        },
    { "trigger",	KEYWORD_trigger		        },
    { "type",		KEYWORD_type		        },
    { "index",		KEYWORD_index		        },
    { "event",		KEYWORD_event		        },
    { "publication",	KEYWORD_publication	        },
    { "service",	KEYWORD_service		        },
    { "domain",		KEYWORD_domain		        },
    { "datatype",	KEYWORD_datatype	        },
    { "result",	        KEYWORD_result		        },
    { "when",	        KEYWORD_when		        },
    { "then",	        KEYWORD_then		        },
    { "variable",	KEYWORD_variable	        },
    { "exception",	KEYWORD_exception	        },
    { "at",	        KEYWORD_at		        },
    { "on",	        KEYWORD_on		        },
    { "primary",        KEYWORD_primary		        },
    { "references",     KEYWORD_references	        },
    { "unique",         KEYWORD_unique		        },
    { "check",          KEYWORD_check		        },
    { "constraint",     KEYWORD_constraint	        },
    { "foreign",        KEYWORD_foreign 	        },
    { "ml_add_table_script",      KEYWORD_ml_table      },
    { "ml_add_connection_script", KEYWORD_ml_conn       },
    { "local",          KEYWORD_local                   },
    { "temporary",      KEYWORD_temporary               },
    { "drop",           KEYWORD_drop                    },
    { "view",           KEYWORD_view                    },
    { "synonym",        KEYWORD_synonym                 },
    { "handler",	KEYWORD_handler		        }
};

/*
*   FUNCTION DECLARATIONS
*/

static void parseBlock (tokenInfo *const token, const boolean local);
static void makeConstTag (tokenInfo *const token, const sqlKind kind);

/*
*   DEBUG function
*/

static void dispToken (tokenInfo *const token, const char * location)
{
#ifdef DEBUG
    if ( isKeyword(token, KEYWORD_NONE) )
    {
        if ( isType(token, TOKEN_IDENTIFIER) || isType(token, TOKEN_STRING)  )
        {
            printf( "\n%s: token string  t:%s  s:%s  l:%lu  p:%d\n"
                    , location
                    , vStringValue(token->string)
                    , vStringValue(token->scope)
                    , token->lineNumber
                    , token->bufferPosition
                  );
        } else {
            printf( "\n%s: token  t:%d  s:%s  l:%lu  p:%d\n"
                    , location
                    , token->type
                    , vStringValue(token->scope)
                    , token->lineNumber
                    , token->bufferPosition
                  );
        }
    } else {
        printf( "\n%s: keyword:%s k:%d  s:%s  l:%lu  p:%d\n"
                , location
                , vStringValue(token->string)
                , token->keyword
                , vStringValue(token->scope)
                , token->lineNumber
                , token->bufferPosition
              );
    }
#endif
}

/*
*   FUNCTION DEFINITIONS
*/

static boolean isIdentChar1 (const int c)
{
    /* Other databases are less restrictive on the first character of
     * an identifier.
     * isIdentChar1 is used to identify the first character of an
     * identifier, so we are removing some restrictions. */
    return (boolean)
	(isalpha (c) || c == '@' || c == '_' );
}

static boolean isIdentChar (const int c)
{
    return (boolean)
	(isalpha (c) || isdigit (c) || c == '$' ||
            c == '@' || c == '_' || c == '#');
}

static void buildSqlKeywordHash (void)
{
    const size_t count = sizeof (SqlKeywordTable) /
			 sizeof (SqlKeywordTable [0]);
    size_t i;
    for (i = 0  ;  i < count  ;  ++i)
    {
	const keywordDesc* const p = &SqlKeywordTable [i];
	addKeyword (p->name, Lang_sql, (int) p->id);
    }
}

static tokenInfo *newToken (void)
{
    tokenInfo *const token = xMalloc(1, tokenInfo);

    token->type         = TOKEN_UNDEFINED;
    token->keyword      = KEYWORD_NONE;
    token->string       = vStringNew ();
    token->scope        = vStringNew ();

    return token;
}

static void deleteToken (tokenInfo *const token)
{
    vStringDelete (token->string);
    eFree (token);
}

/*
*   Tag generation functions
*/

static void makeSqlTag (tokenInfo *const token, const sqlKind kind)
{
    vString *	fulltag;

    if (SqlKinds [kind].enabled)
    {
        /*
         * If a scope has been added to the token, change the token
         * string to include the scope when making the tag.
         */
        if ( vStringLength(token->scope) > 0 )
        {
            fulltag = vStringNew ();
            vStringCopy(fulltag, token->scope);
            vStringCatS (fulltag, ".");
            vStringCatS (fulltag, vStringValue(token->string));
            vStringTerminate(fulltag);
            vStringCopy(token->string, fulltag);
            vStringDelete (fulltag);
        }
        makeConstTag (token, kind);
    }
}

static void makeConstTag (tokenInfo *const token, const sqlKind kind)
{
    if (SqlKinds [kind].enabled)
    {
        const char *const name = vStringValue (token->string);
	tagEntryInfo e;
	initTagEntry (&e, name);

	e.lineNumber   = token->lineNumber;
	e.filePosition = token->filePosition;
	e.kindName     = SqlKinds [kind].name;
	e.kind         = SqlKinds [kind].letter;

	makeTagEntry (&e);
    }
}

/*
*   Parsing functions
*/

static int skipToCharacter (const int c)
{
    int d;
    do
    {
	d = fileGetc ();
    } while (d != EOF  &&  d != c);
    return d;
}

static void parseString (vString *const string, const int delimiter)
{
    boolean end = FALSE;
    int c;
    while (! end)
    {
	c = fileGetc ();
/*        printf( "\nps: %c\n", c  ); */
	if (c == EOF)
	    end = TRUE;
	else if (c == delimiter)
	    end = TRUE;
	else
	    vStringPut (string, c);
    }
    vStringTerminate (string);
}

/*  Read a C identifier beginning with "firstChar" and places it into "name".
 */
static void parseIdentifier (vString *const string, const int firstChar)
{
    int c = firstChar;
    Assert (isIdentChar1 (c));
    do
    {
	vStringPut (string, c);
	c = fileGetc ();
    } while (isIdentChar (c));
    vStringTerminate (string);
    if (!isspace (c))
	fileUngetc (c);		/* unget non-identifier character */
}

static keywordId analyzeToken (vString *const name)
{
    static vString *keyword = NULL;
    if (keyword == NULL)
	keyword = vStringNew ();
    vStringCopyToLower (keyword, name);
    return (keywordId) lookupKeyword (vStringValue (keyword), Lang_sql);
}

static void readToken (tokenInfo *const token)
{
    int c;

    token->type         = TOKEN_UNDEFINED;
    token->keyword      = KEYWORD_NONE;
    vStringClear (token->string);

getNextChar:
    do
    {
	c = fileGetc ();
/*        printf( "\nrtc: %c\n", c ); */
        /*
         * Added " to the list of ignores, not sure what this
         * might break but it gets by this issue:
         *    create table "t1" (...)
         */
    }
    while (c == '\t'  ||  c == ' ' ||  c == '\n');

    switch (c)
    {
	case EOF: longjmp (Exception, (int)ExceptionEOF);	break;
	case '(': token->type = TOKEN_OPEN_PAREN;		break;
	case ')': token->type = TOKEN_CLOSE_PAREN;		break;
	case ';': token->type = TOKEN_SEMICOLON;		break;
	case '.': token->type = TOKEN_PERIOD;	        	break;
	case ',': token->type = TOKEN_COMMA;			break;

	case '\'':
	case '"':
	    token->type = TOKEN_STRING;
	    parseString (token->string, c);
            token->lineNumber = getSourceLineNumber ();
	    token->filePosition	= getInputFilePosition ();
	    break;

	case '-':
	    c = fileGetc ();
	    if (c == '-')		/* is this the start of a comment? */
	    {
		skipToCharacter ('\n');
		goto getNextChar;
	    }
	    else
	    {
		if (!isspace (c))
		    fileUngetc (c);
 		token->type = TOKEN_OPERATOR;
	    }
	    break;

	case '<':
	case '>':
	{
	    const int initial = c;
	    int d = fileGetc ();
	    if (d == initial)
	    {
		if (initial == '<')
		    token->type = TOKEN_BLOCK_LABEL_BEGIN;
		else
		    token->type = TOKEN_BLOCK_LABEL_END;
	    }
	    else
	    {
		fileUngetc (d);
		token->type = TOKEN_UNDEFINED;
	    }
	    break;
	}

	case '/':
	{
	    int d = fileGetc ();
	    if (d != '*')		/* is this the start of a comment? */
		fileUngetc (d);
	    else
	    {
		do
		{
		    skipToCharacter ('*');
		    c = fileGetc ();
		    if (c == '/')
			break;
		    else
			fileUngetc (c);
		} while (c != EOF && c != '\0');
		goto getNextChar;
	    }
	    break;
	}

	default:
	    if (! isIdentChar1 (c))
		token->type = TOKEN_UNDEFINED;
	    else
	    {
		parseIdentifier (token->string, c);
		token->lineNumber = getSourceLineNumber ();
		token->filePosition = getInputFilePosition ();
		token->keyword = analyzeToken (token->string);
		if (isKeyword (token, KEYWORD_rem))
		{
		    vStringClear (token->string);
		    skipToCharacter ('\n');
		    goto getNextChar;
		}
		else if (isKeyword (token, KEYWORD_NONE))
		    token->type = TOKEN_IDENTIFIER;
		else
		    token->type = TOKEN_KEYWORD;
	    }
	    break;
    }
    /*dispToken(token, "rte");*/
}

/*
*   Token parsing functions
*/

/* - unused - I don't know (enrico)
static void addContext (tokenInfo* const parent, const tokenInfo* const child)
{
    if (vStringLength (parent->string) > 0)
    {
        vStringCatS (parent->string, ".");
    }
    vStringCatS (parent->string, vStringValue(child->string));
    vStringTerminate(parent->string);
}
*/
static void addToScope (tokenInfo* const token, vString* const extra)
{
    if (vStringLength (token->scope) > 0)
    {
        vStringCatS (token->scope, ".");
    }
    vStringCatS (token->scope, vStringValue(extra));
    vStringTerminate(token->scope);
}

/*
*   Scanning functions
*/

static void findToken (tokenInfo *const token, const tokenType type)
{
    while (! isType (token, type))
    {
	readToken (token);
    }
}

static void skipArgumentList (tokenInfo *const token)
{
    int nest_level = 0;

    /*
     * Other databases can have arguments with fully declared
     * datatypes:
     *   (  name varchar(30), text binary(10)  )
     * So we must check for nested open and closing parantheses
     */

    if (isType (token, TOKEN_OPEN_PAREN))	/* arguments? */
    {
        nest_level++;
	/*findToken (token, TOKEN_CLOSE_PAREN);*/
        while (! (isType (token, TOKEN_CLOSE_PAREN) && (nest_level == 0)))
        {
            readToken (token);
            if (isType (token, TOKEN_OPEN_PAREN))
            {
                nest_level++;
            }
            if (isType (token, TOKEN_CLOSE_PAREN))
            {
                if (nest_level > 0)
                {
                    nest_level--;
                }
            }
        } /*while*/
	readToken (token);
    }
}

static void parseSubProgram (tokenInfo *const token)
{
    tokenInfo *const name  = newToken ();

    /*
     * Prototype:
     *     FUNCTION func_name RETURN integer;
     *     PROCEDURE proc_name( parameters );
     * Procedure
     *     FUNCTION GET_ML_USERNAME RETURN VARCHAR2
     *     IS
     *     BEGIN
     *         RETURN v_sync_user_id;
     *     END GET_ML_USERNAME;
     *
     *     PROCEDURE proc_name( parameters )
     *         IS
     *         BEGIN
     *         END;
     *     CREATE PROCEDURE proc_name( parameters )
     *         EXTERNAL NAME ... ;
     *     CREATE PROCEDURE proc_name( parameters )
     *         BEGIN
     *         END;
     *
     *     CREATE FUNCTION f_GetClassName(
     *         IN @object VARCHAR(128)
     *        ,IN @code   VARCHAR(128)
     *     )
     *     RETURNS VARCHAR(200)
     *     DETERMINISTIC
     *     BEGIN
     *
     *         IF( @object = 'user_state' ) THEN
     *             SET something = something;
     *         END IF;
     *
     *         RETURN @name;
     *     END;
     */
    const sqlKind kind = isKeyword (token, KEYWORD_function) ?
	    SQLTAG_FUNCTION : SQLTAG_PROCEDURE;
    Assert (isKeyword (token, KEYWORD_function) ||
	    isKeyword (token, KEYWORD_procedure));
    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }
    skipArgumentList (token);

    if (kind == SQLTAG_FUNCTION)
    {
        if (isKeyword (token, KEYWORD_return))
        {
            /* Read RETURN */
            readToken (token);
            /* Read datatype */
            readToken (token);
        }
    }
    if( isType (token, TOKEN_SEMICOLON) )
    {
	makeSqlTag (name, SQLTAG_PROTOTYPE);
    } else {
        while (!(isKeyword (token, KEYWORD_is) ||
                    isKeyword (token, KEYWORD_begin) ||
                    isType (token, TOKEN_SEMICOLON)
                )
              )
            readToken (token);			/* read return type */
        if (isKeyword (token, KEYWORD_is) ||
                isKeyword (token, KEYWORD_begin) )
        {
            addToScope(token, name->string);
            if (isType (name, TOKEN_IDENTIFIER) ||
                    isType (name, TOKEN_STRING))
                makeSqlTag (name, kind);

            /*dispToken(name, "SubProgram: parseBlock name");*/
            /*dispToken(token, "SubProgram: parseBlock token");*/
            parseBlock (token, TRUE);
            vStringClear (token->scope);
        }
    }
    deleteToken (name);
}

static void parseRecord (tokenInfo *const token)
{
    /* Make it a bit forgiving, this is called from
     * multiple functions, parseTable, parseType */
    if (!isType (token, TOKEN_OPEN_PAREN))
	readToken (token);

    Assert (isType (token, TOKEN_OPEN_PAREN));
    do
    {
	if ( isType (token, TOKEN_COMMA) || isType (token, TOKEN_OPEN_PAREN) )
            readToken (token);

        /*
         * Create table statements can end with various constraints
         * which must be excluded from the SQLTAG_FIELD.
         *    create table t1 (
         *        c1 integer,
         *        c2 char(30),
         *        c3 numeric(10,5),
         *        c4 integer,
         *        constraint whatever,
         *        primary key(c1),
         *        foreign key (),
         *        check ()
         *    )
         */
        if (! (isKeyword(token, KEYWORD_primary) ||
                isKeyword(token, KEYWORD_references) ||
                isKeyword(token, KEYWORD_unique) ||
                isKeyword(token, KEYWORD_check) ||
                isKeyword(token, KEYWORD_constraint) ||
                isKeyword(token, KEYWORD_foreign) ) )
        {
            if (isType (token, TOKEN_IDENTIFIER) ||
                    isType (token, TOKEN_STRING))
                makeSqlTag (token, SQLTAG_FIELD);
        }

	while (!(isType (token, TOKEN_COMMA) ||
		 isType (token, TOKEN_CLOSE_PAREN) ||
		 isType (token, TOKEN_OPEN_PAREN)
                ))
        {
            readToken (token);
            /*
             * A table structure can look like this:
             *    create table t1 (
             *        c1 integer,
             *        c2 char(30),
             *        c3 numeric(10,5),
             *        c4 integer
             *    )
             * We can't just look for a COMMA or CLOSE_PAREN
             * since that will not deal with the numeric(10,5)
             * case.  So we need to skip the argument list
             * when we find an open paren.
             */
            if (isType (token, TOKEN_OPEN_PAREN))
            {
                /* Reads to the next token after the TOKEN_CLOSE_PAREN */
                skipArgumentList(token);
            }
        }
    } while (! isType (token, TOKEN_CLOSE_PAREN));
}

static void parseType (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();
    vString * saveScope = vStringNew ();

    vStringCopy(saveScope, token->scope);
    /* If a scope has been set, add it to the name */
    addToScope (name, token->scope);
    readToken (name);
    if (isType (name, TOKEN_IDENTIFIER))
    {
	readToken (token);
	if (isKeyword (token, KEYWORD_is))
	{
	    readToken (token);
            addToScope (token, name->string);
	    switch (token->keyword)
	    {
		case KEYWORD_record:
		case KEYWORD_object:
		    makeSqlTag (name, SQLTAG_RECORD);
		    parseRecord (token);
		    break;

		case KEYWORD_table:
		    makeSqlTag (name, SQLTAG_TABLE);
		    break;

		case KEYWORD_ref:
		    readToken (token);
		    if (isKeyword (token, KEYWORD_cursor))
			makeSqlTag (name, SQLTAG_CURSOR);
		    break;

		default: break;
	    }
            vStringClear (token->scope);
	}
    }
    vStringCopy(token->scope, saveScope);
    deleteToken (name);
    vStringDelete(saveScope);
}

static void parseSimple (tokenInfo *const token, const sqlKind kind)
{
    readToken (token);
    if (isType (token, TOKEN_IDENTIFIER) ||
                    isType (token, TOKEN_STRING))
	makeSqlTag (token, kind);
}

static void parseDeclare (tokenInfo *const token, const boolean local)
{
    /*
     * PL/SQL declares are of this format:
     *    IS|AS
     *    [declare]
     *       CURSOR curname ...
     *       varname1 datatype;
     *       varname2 datatype;
     *       varname3 datatype;
     *    begin
     */

    if (isKeyword (token, KEYWORD_declare))
	readToken (token);
    while (! isKeyword (token, KEYWORD_begin) && ! isKeyword (token, KEYWORD_end))
    {
	switch (token->keyword)
	{
	    case KEYWORD_cursor:    parseSimple (token, SQLTAG_CURSOR); break;
	    case KEYWORD_function:  parseSubProgram (token); break;
	    case KEYWORD_procedure: parseSubProgram (token); break;
	    case KEYWORD_subtype:   parseSimple (token, SQLTAG_SUBTYPE); break;
	    case KEYWORD_trigger:   parseSimple (token, SQLTAG_TRIGGER); break;
	    case KEYWORD_type:      parseType (token); break;

	    default:
		if (isType (token, TOKEN_IDENTIFIER))
		{
		    if (local)
                    {
			makeSqlTag (token, SQLTAG_LOCAL_VARIABLE);
                    } else {
			makeSqlTag (token, SQLTAG_VARIABLE);
                    }
		}
		break;
	}
	findToken (token, TOKEN_SEMICOLON);
	readToken (token);
    }
}

static void parseDeclareANSI (tokenInfo *const token, const boolean local)
{
    tokenInfo *const type = newToken ();
    /*
     * ANSI declares are of this format:
     *   BEGIN
     *       DECLARE varname1 datatype;
     *       DECLARE varname2 datatype;
     *       ...
     *
     * This differ from PL/SQL where DECLARE preceeds the BEGIN block
     * and the DECLARE keyword is not repeated.
     */
    while (isKeyword (token, KEYWORD_declare))
    {
	readToken (token);
	readToken (type);

        if (isKeyword (type, KEYWORD_cursor))
            makeSqlTag (token, SQLTAG_CURSOR);
        else if (isKeyword (token, KEYWORD_local) &&
                isKeyword (type, KEYWORD_temporary))
        {
            /*
             * DECLARE LOCAL TEMPORARY TABLE table_name (
             *    c1 int,
             *    c2 int
             * );
             */
            readToken (token);
            if (isKeyword (token, KEYWORD_table))
            {
                readToken (token);
                if (isType(token, TOKEN_IDENTIFIER) ||
                        isType(token, TOKEN_STRING) )
                {
                    makeSqlTag (token, SQLTAG_TABLE);
                }
            }
        }
        else if (isType (token, TOKEN_IDENTIFIER) ||
                isType (token, TOKEN_STRING))
        {
            if (local)
                makeSqlTag (token, SQLTAG_LOCAL_VARIABLE);
            else
                makeSqlTag (token, SQLTAG_VARIABLE);
        }
        findToken (token, TOKEN_SEMICOLON);
        readToken (token);
    }
    deleteToken (type);
}

static void parseLabel (tokenInfo *const token)
{
    /*
     * A label has this format:
     *     <<tobacco_dependency>>
     *     DECLARE
     *        v_senator VARCHAR2(100) := 'THURMOND, JESSE';
     *     BEGIN
     *        IF total_contributions (v_senator, 'TOBACCO') > 25000
     *        THEN
     *           <<alochol_dependency>>
     *           DECLARE
     *              v_senator VARCHAR2(100) := 'WHATEVERIT, TAKES';
     *           BEGIN
     *              ...
     */

    Assert (isType (token, TOKEN_BLOCK_LABEL_BEGIN));
    readToken (token);
    if (isType (token, TOKEN_IDENTIFIER))
    {
        makeSqlTag (token, SQLTAG_BLOCK_LABEL);
        readToken (token);        /* read end of label */
    }
}

static void parseStatements (tokenInfo *const token)
{
    do
    {
        if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
            parseLabel (token);
        else
        {
            switch (token->keyword)
            {
                /*
                 * EXCEPTION
                 *   <exception handler>;
                 *
                 * Where an exception handler could be:
                 *   BEGIN
                 *      WHEN OTHERS THEN
                 *          x := x + 3;
                 *   END;
                 * In this case we need to skip this keyword and
                 * move on to the next token without reading until
                 * TOKEN_SEMICOLON;
                 */
                case KEYWORD_exception:
                    readToken (token);
                    continue;

                    /*
                     * WHEN statements can be used in exception clauses
                     * and CASE statements.  The CASE statement should skip
                     * these given below we skip over to an END statement.
                     * But for an exception clause, we can have:
                     *     EXCEPTION
                     *         WHEN OTHERS THEN
                     *         BEGIN
                     *                x := x + 3;
                     *         END;
                     * If we skip to the TOKEN_SEMICOLON, we miss the begin
                     * of a nested BEGIN END block.  So read the next token
                     * after the THEN and restart the LOOP.
                     */
                case KEYWORD_when:
                    while (! isKeyword (token, KEYWORD_then))
                        readToken (token);
                    readToken (token);
                    continue;

                    /*
                     * We do not want to look for a ; since for an empty
                     * IF block, that would skip over the END.
                     *  IF...THEN
                     *  END IF;
                     */
                case KEYWORD_if:
                    while (! isKeyword (token, KEYWORD_then))
                        readToken (token);
                    /*readToken (token);*/
                    parseStatements (token);
                    break;

                    /*
                     *  LOOP...
                     *  END LOOP;
                     *
                     *  FOR loop_name AS cursor_name CURSOR FOR ...
                     *  END FOR;
                     */
                case KEYWORD_loop:
                case KEYWORD_case:
                case KEYWORD_for:
                    readToken (token);
                    parseStatements (token);
                    break;

                case KEYWORD_declare:
                case KEYWORD_begin:
                    parseBlock (token, TRUE);
                    break;

                default:
                    readToken (token);
                    break;
            }
            findToken (token, TOKEN_SEMICOLON);
        }
        readToken (token);
    } while (! isKeyword (token, KEYWORD_end));
}

static void parseBlock (tokenInfo *const token, const boolean local)
{
    if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
    {
        parseLabel (token);
        readToken (token);
    }
    if (! isKeyword (token, KEYWORD_begin))
    {
        readToken (token);
        /*dispToken(token, "parseBlock calling parseDeclare");*/
        parseDeclare (token, local);
    }
    if (isKeyword (token, KEYWORD_begin))
    {
        readToken (token);
        parseDeclareANSI (token, local);
        while (! isKeyword (token, KEYWORD_end))
            parseStatements (token);
        findToken (token, TOKEN_SEMICOLON);
    }
}

static void parsePackage (tokenInfo *const token)
{
    /*
     * Packages can be specified in a number of ways:
     *      CREATE OR REPLACE PACKAGE pkg_name AS
     * or
     *      CREATE OR REPLACE PACKAGE owner.pkg_name AS
     * or by specifying a package body
     *     CREATE OR REPLACE PACKAGE BODY pkg_name AS
     *     CREATE OR REPLACE PACKAGE BODY owner.pkg_name AS
     */
    tokenInfo *const name = newToken ();
    readToken (name);
    if (isKeyword (name, KEYWORD_body))
        readToken (name);
    /*dispToken(token, "parsePackage after BODY");*/
    /* Chceck for owner.pkg_name */
    while (! isKeyword (token, KEYWORD_is))
    {
        readToken (token);
        if ( isType(token, TOKEN_PERIOD) )
        {
            readToken (name);
            /*dispToken(name, "parsePackage new name");*/
        }
    }
    dispToken(name, "parsePackage name");
    if (isKeyword (token, KEYWORD_is))
    {
        /*dispToken(token, "parsePackage processing IS");*/
        if (isType (name, TOKEN_IDENTIFIER) ||
                    isType (name, TOKEN_STRING))
            makeSqlTag (name, SQLTAG_PACKAGE);
        parseBlock (token, FALSE);
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
}

static void parseTable (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create table t1 (c1 int);
     *     create global tempoary table t2 (c1 int);
     *     create table "t3" (c1 int);
     *     create table bob.t4 (c1 int);
     *     create table bob."t5" (c1 int);
     *     create table "bob"."t6" (c1 int);
     *     create table bob."t7" (c1 int);
     * Proxy tables use this format:
     *     create existing table bob."t7" AT '...'; */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }
    if (isType (token, TOKEN_OPEN_PAREN))
    {
        if (isType (name, TOKEN_IDENTIFIER) ||
                isType (name, TOKEN_STRING))
        {
            makeSqlTag (name, SQLTAG_TABLE);
            vStringCopy(token->scope, name->string);
            parseRecord (token);
            vStringClear (token->scope);
        }
    } else if (isKeyword (token, KEYWORD_at))
    {
        if (isType (name, TOKEN_IDENTIFIER))
        {
            makeSqlTag (name, SQLTAG_TABLE);
        }
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
}

static void parseIndex (tokenInfo *const token)
{
    tokenInfo *const name  = newToken ();
    tokenInfo *const owner = newToken ();

    /* This deals with these formats
     *     create index i1 on t1(c1) create index "i2" on t1(c1)
     *     create virtual unique clustered index "i3" on t1(c1)
     *     create unique clustered index "i4" on t1(c1)
     *     create clustered index "i5" on t1(c1)
     *     create bitmap index "i6" on t1(c1) */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }
    if ( isKeyword (token, KEYWORD_on) &&
            (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING) ) )
    {
        readToken (owner);
        readToken (token);
        if (isType (token, TOKEN_PERIOD))
        {
            readToken (owner);
            readToken (token);
        }
        addToScope(name, owner->string);
        makeSqlTag (name, SQLTAG_INDEX);
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
    deleteToken (owner);
}

static void parseEvent (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create event e1 handler begin end;
     *     create event "e2" handler begin end;
     *     create event dba."e3" handler begin end;
     *     create event "dba"."e4" handler begin end; */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
    }
    while (! (isKeyword (token, KEYWORD_handler) ||
                (isType (token, TOKEN_SEMICOLON)))    )
    {
        readToken (token);
    }

    if ( isKeyword (token, KEYWORD_handler) ||
            isType (token, TOKEN_SEMICOLON)   )
    {
        makeSqlTag (name, SQLTAG_EVENT);
    }

    if (isKeyword (token, KEYWORD_handler))
    {
        readToken (token);
        if ( isKeyword (token, KEYWORD_begin) )
        {
            parseBlock (token, TRUE);
        }
        findToken (token, TOKEN_SEMICOLON);
    }
    deleteToken (name);
}

static void parseTrigger (tokenInfo *const token)
{
    tokenInfo *const name  = newToken ();
    tokenInfo *const table = newToken ();

    /* This deals with these formats
     *     create or replace trigger tr1 begin end;
     *     create trigger "tr2" begin end;
     *     drop trigger "droptr1";
     *     create trigger "tr3" CALL sp_something();
     *     create trigger "owner"."tr4" begin end;
     *     create trigger "tr5" not valid;
     *     create trigger "tr6" begin end; */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }

    while (! (isKeyword (token, KEYWORD_on) ||
                (   isType (token, TOKEN_SEMICOLON)))    )
    {
        readToken (token);
    }

    if (! isType (token, TOKEN_SEMICOLON) )
    {
        readToken (table);
        readToken (token);
        if (isType (token, TOKEN_PERIOD))
        {
            readToken (table);
            readToken (token);
        }

        while (! (isKeyword (token, KEYWORD_begin) ||
                    (isKeyword (token, KEYWORD_call)) ||
                    (   isType (token, TOKEN_SEMICOLON)))    )
        {
            readToken (token);
            if ( isKeyword (token, KEYWORD_declare) )
            {
                addToScope(token, name->string);
                parseDeclare(token, TRUE);
                vStringClear(token->scope);
            }
        }

        if ( isKeyword (token, KEYWORD_begin) ||
                isKeyword (token, KEYWORD_call)   )
        {
            addToScope(name, table->string);
            makeSqlTag (name, SQLTAG_TRIGGER);
            addToScope(token, table->string);
            if ( isKeyword (token, KEYWORD_begin) )
            {
                parseBlock (token, TRUE);
            }
            vStringClear(token->scope);
        }
    }

    if (! isType (token, TOKEN_SEMICOLON) )
    {
	findToken (token, TOKEN_SEMICOLON);
    }
    deleteToken (name);
    deleteToken (table);
}

static void parsePublication (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create or replace publication pu1 ()
     *     create publication "pu2" ()
     *     create publication dba."pu3" ()
     *     create publication "dba"."pu4" () */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }
    if (isType (token, TOKEN_OPEN_PAREN))
    {
	if (isType (name, TOKEN_IDENTIFIER) ||
                isType (name, TOKEN_STRING))
	{
	    makeSqlTag (name, SQLTAG_PUBLICATION);
	}
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
}

static void parseService (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     CREATE SERVICE s1 TYPE 'HTML'
     *         AUTHORIZATION OFF USER DBA AS
     *         SELECT *
     *           FROM SYS.SYSTABLE;
     *     CREATE SERVICE "s2" TYPE 'HTML'
     *         AUTHORIZATION OFF USER DBA AS
     *         CALL sp_Something(); */

    readToken (name);
    readToken (token);
    if (isKeyword (token, KEYWORD_type))
    {
        if (isType (name, TOKEN_IDENTIFIER) ||
                isType (name, TOKEN_STRING))
        {
            makeSqlTag (name, SQLTAG_SERVICE);
        }
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
}

static void parseDomain (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     CREATE DOMAIN|DATATYPE [AS] your_name ...; */

    readToken (name);
    if (isKeyword (name, KEYWORD_is))
    {
        readToken (name);
    }
    readToken (token);
    if (isType (name, TOKEN_IDENTIFIER) ||
            isType (name, TOKEN_STRING))
    {
        makeSqlTag (name, SQLTAG_DOMAIN);
    }
    findToken (token, TOKEN_SEMICOLON);
    deleteToken (name);
}

static void parseDrop (tokenInfo *const token)
{
    /* This deals with these formats
     *     DROP TABLE|PROCEDURE|DOMAIN|DATATYPE name;
     *
     * Just simply skip over these statements.
     * They are often confused with PROCEDURE prototypes
     * since the syntax is similar, this effectively deals with
     * the issue for all types. */

    /*dispToken(token, "parseDrop");*/
    findToken (token, TOKEN_SEMICOLON);
}

static void parseVariable (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create variable varname1 integer;
     *     create variable @varname2 integer;
     *     create variable "varname3" integer;
     *     drop   variable @varname3; */

    readToken (name);
    readToken (token);
    if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
            && !isType (token, TOKEN_SEMICOLON) )
    {
        makeSqlTag (name, SQLTAG_VARIABLE);
    }
    if (! isType (token, TOKEN_SEMICOLON) )
        findToken (token, TOKEN_SEMICOLON);

    deleteToken (name);
}

static void parseSynonym (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create variable varname1 integer;
     *     create variable @varname2 integer;
     *     create variable "varname3" integer;
     *     drop   variable @varname3; */

    readToken (name);
    readToken (token);
    if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
            && isKeyword (token, KEYWORD_for) )
    {
        makeSqlTag (name, SQLTAG_SYNONYM);
    }
    if (! isType (token, TOKEN_SEMICOLON) )
        findToken (token, TOKEN_SEMICOLON);

    deleteToken (name);
}

static void parseView (tokenInfo *const token)
{
    tokenInfo *const name = newToken ();

    /* This deals with these formats
     *     create variable varname1 integer;
     *     create variable @varname2 integer;
     *     create variable "varname3" integer;
     *     drop   variable @varname3; */

    readToken (name);
    readToken (token);
    if (isType (token, TOKEN_PERIOD))
    {
        readToken (name);
        readToken (token);
    }
    if ( isType (token, TOKEN_OPEN_PAREN) )
    {
        skipArgumentList(token);

    }

    while (!(isKeyword (token, KEYWORD_is) ||
                isType (token, TOKEN_SEMICOLON)
            ))
    {
        readToken (token);
    }

    if ( (isType (name, TOKEN_IDENTIFIER) || isType (name, TOKEN_STRING))
            && isKeyword (token, KEYWORD_is) )
    {
        makeSqlTag (name, SQLTAG_VIEW);
    }

    if (! isType (token, TOKEN_SEMICOLON) )
        findToken (token, TOKEN_SEMICOLON);

    deleteToken (name);
}

static void parseMLTable (tokenInfo *const token)
{
    tokenInfo *const version = newToken ();
    tokenInfo *const table   = newToken ();
    tokenInfo *const event   = newToken ();

    /* This deals with these formats
     *    call ml_add_table_script( 'version', 'table_name', 'event',
     *         'some SQL statement'
     *         ); */

    readToken (token);
    if ( isType (token, TOKEN_OPEN_PAREN) )
    {
        readToken (version);
        readToken (token);
        while (!(isType (token, TOKEN_COMMA) ||
                    isType (token, TOKEN_CLOSE_PAREN)
                ))
        {
            readToken (token);
        }

        if (isType (token, TOKEN_COMMA))
        {
            readToken (table);
            readToken (token);
            while (!(isType (token, TOKEN_COMMA) ||
                        isType (token, TOKEN_CLOSE_PAREN)
                    ))
            {
                readToken (token);
            }

            if (isType (token, TOKEN_COMMA))
            {
                readToken (event);

                if (isType (version, TOKEN_STRING) &&
                        isType (table, TOKEN_STRING) &&
                        isType (event, TOKEN_STRING) )
                {
                    addToScope(version, table->string);
                    addToScope(version, event->string);
                    makeSqlTag (version, SQLTAG_MLTABLE);
                }
            }
            if( !isType (token, TOKEN_CLOSE_PAREN) )
                findToken (token, TOKEN_CLOSE_PAREN);
        }
    }

    if (! isType (token, TOKEN_SEMICOLON) )
        findToken (token, TOKEN_SEMICOLON);

    deleteToken (version);
    deleteToken (table);
    deleteToken (event);
}

static void parseMLConn (tokenInfo *const token)
{
    tokenInfo *const version = newToken ();
    tokenInfo *const event   = newToken ();

    /* This deals with these formats
     *    call ml_add_connection_script( 'version', 'event',
     *         'some SQL statement'
     *         ); */

    readToken (token);
    if ( isType (token, TOKEN_OPEN_PAREN) )
    {
        readToken (version);
        readToken (token);
        while (!(isType (token, TOKEN_COMMA) ||
                    isType (token, TOKEN_CLOSE_PAREN)
                ))
        {
            readToken (token);
        }

        if (isType (token, TOKEN_COMMA))
        {
            readToken (event);

            if (isType (version, TOKEN_STRING) &&
                    isType (event, TOKEN_STRING) )
            {
                addToScope(version, event->string);
                makeSqlTag (version, SQLTAG_MLCONN);
            }
        }
        if( !isType (token, TOKEN_CLOSE_PAREN) )
            findToken (token, TOKEN_CLOSE_PAREN);

    }

    if (! isType (token, TOKEN_SEMICOLON) )
        findToken (token, TOKEN_SEMICOLON);

    deleteToken (version);
    deleteToken (event);
}


static void parseSqlFile (tokenInfo *const token)
{
    do
    {
	readToken (token);
        /*dispToken(token, "psf");*/

	if (isType (token, TOKEN_BLOCK_LABEL_BEGIN))
	    parseLabel (token);
	else switch (token->keyword)
	{
	    case KEYWORD_begin:       parseBlock (token, FALSE); break;
	    case KEYWORD_cursor:      parseSimple (token, SQLTAG_CURSOR); break;
	    case KEYWORD_datatype:    parseDomain (token); break;
	    case KEYWORD_declare:     parseBlock (token, FALSE); break;
	    case KEYWORD_domain:      parseDomain (token); break;
	    case KEYWORD_drop:        parseDrop (token); break;
	    case KEYWORD_event:       parseEvent (token); break;
	    case KEYWORD_function:    parseSubProgram (token); break;
	    case KEYWORD_index:       parseIndex (token); break;
	    case KEYWORD_ml_table:    parseMLTable (token); break;
	    case KEYWORD_ml_conn:     parseMLConn (token); break;
	    case KEYWORD_package:     parsePackage (token); break;
	    case KEYWORD_procedure:   parseSubProgram (token); break;
	    case KEYWORD_publication: parsePublication (token); break;
	    case KEYWORD_service:     parseService (token); break;
	    case KEYWORD_subtype:     parseSimple (token, SQLTAG_SUBTYPE); break;
	    case KEYWORD_synonym:     parseSynonym (token); break;
	    case KEYWORD_table:       parseTable (token); break;
	    case KEYWORD_trigger:     parseTrigger (token); break;
	    case KEYWORD_type:        parseType (token); break;
	    case KEYWORD_variable:    parseVariable (token); break;
	    case KEYWORD_view:        parseView (token); break;
	    default:                break;
	}
    } while (! isKeyword (token, KEYWORD_end));
}

static void initialize (const langType language)
{
    Assert (sizeof (SqlKinds) / sizeof (SqlKinds [0]) == SQLTAG_COUNT);
    Lang_sql = language;
    buildSqlKeywordHash ();
}

static void findSqlTags (void)
{
    tokenInfo *const token = newToken ();
    exception_t exception = (exception_t) (setjmp (Exception));
    while (exception == ExceptionNone)
	parseSqlFile (token);
    deleteToken (token);
}

extern parserDefinition* SqlParser (void)
{
    static const char *const extensions [] = { "sql", NULL };
    parserDefinition* def = parserNew ("SQL");
    def->kinds      = SqlKinds;
    def->kindCount  = KIND_COUNT (SqlKinds);
    def->extensions = extensions;
    def->parser     = findSqlTags;
    def->initialize = initialize;
    return def;
}

/* vi:set tabstop=8 shiftwidth=4: */
