/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
//
// macro.c
//
// specialized markup processors
//
// Copyright (c) 1995-96 Jim Nelson.  Permission to distribute
// granted by the author.  No warranties are made on the fitness of this
// source code.
//
*/

#include "macro.h"

#include "defs.h"
#include "def-proc.h"

/*
// specialized markup processors
*/

#include "option.h"
#include "image-proc.h"
#include "file-proc.h"
#include "def-proc.h"
#include "use-proc.h"
#include "set-proc.h"
#include "bool-proc.h"
#include "misc-proc.h"
#include "while-proc.h"

static MARKUP_PROCESSORS markupProcessor[] =
{
    { "IMG", MARKUP_TYPE_HTML, ImageProcessor },
    { "OPT", MARKUP_TYPE_HTP, OptionProcessor },
    { "FILE", MARKUP_TYPE_HTP, FileProcessor },
    { "INC", MARKUP_TYPE_HTP, IncProcessor },
    { "SET", MARKUP_TYPE_HTP, SetProcessor },
    { "BLOCK", MARKUP_TYPE_HTP, BlockProcessor },
    { "BLOCKDEF", MARKUP_TYPE_HTP, BlockProcessor },
    { "USE", MARKUP_TYPE_HTP, UseProcessor },
    { "IF", MARKUP_TYPE_HTP, BooleanProcessor },
    { "/IF", MARKUP_TYPE_HTP, BooleanProcessor },
    { "IFNOT", MARKUP_TYPE_HTP, ConditionalWarning },
    { "ELSE", MARKUP_TYPE_HTP, BooleanProcessor },
    { "ELSEIF", MARKUP_TYPE_HTP, BooleanProcessor },
    { "/HEAD", MARKUP_TYPE_HTML, HeadProcessor },
    { "UNSET", MARKUP_TYPE_HTP, UnsetProcessor },
    { "PRE", MARKUP_TYPE_HTML, PreProcessor },
    { "/PRE", MARKUP_TYPE_HTML, PreProcessor },
    { "ALTTEXT", MARKUP_TYPE_HTP, AltTextProcessor },
    { "DEF", MARKUP_TYPE_HTP, BlockProcessor },
    { "OUTPUT", MARKUP_TYPE_HTP, OutputProcessor },
    { "UNDEF", MARKUP_TYPE_HTP, UndefProcessor },
    { "WHILE", MARKUP_TYPE_HTP, WhileProcessor },
    { "/WHILE", MARKUP_TYPE_HTP, WhileProcessor },
    { "!---", MARKUP_TYPE_HTP, HtpCommentProcessor },
    { "QUOTE", MARKUP_TYPE_HTP, QuoteProcessor },
    { "IMAGEURL", MARKUP_TYPE_HTP, ImageUrlProcessor }
};
#define MARKUP_PROCESSOR_COUNT (sizeof(markupProcessor)/sizeof(markupProcessor[0]))

/*
// HTML processing
*/
static BOOL FindMatchingBrace(TASK *task, const char **text)
{
    const char *textPtr;
    int braceLevel;
    textPtr = *text;

    DEBUG_PRINT(("FindMatchingBrace on %s", textPtr));
    assert (*textPtr == '{');

    textPtr++;
    braceLevel = 0;
    while (TRUE) {
        switch(*textPtr) {
        case NUL:
            HtpMsg(MSG_ERROR, task->infile, "ending brace not found in macro name");
            return FALSE;

        case '}':
            if (!braceLevel) {
                *text = textPtr + 1;
                return TRUE;
            }
            braceLevel--;
            break;

        case '{':
            braceLevel++;
            break;

        }
        textPtr++;
    }
}
       
BOOL ExpandMacrosInString(TASK *task, const char *text, char **newText,
                          int *quotes, BOOL *changed)
{
    const char *expansion;
    char *macro, *newMacro;
    const char *textPtr;
    uint expansionLength;
    uint skipped;
    uint textLength;
    int singletonMacro, dummy;
    BOOL macroChanged, destAlloced;
    char *destText;
    uint destTextSize;
    uint copiedSoFar;

    assert(task != NULL);
    assert(text != NULL);
    assert(newText != NULL);
    assert(quotes != NULL);
    assert(changed != NULL);

    textLength = strlen(text);
    destText = (char *) text;
    destTextSize = textLength + 1;
    destAlloced = FALSE;

    *changed = FALSE;
    *newText = destText;

    
    /* assume singletonMacro until we find some other text */
    singletonMacro = 1;
    
    /* loop repeatedly to evaluate the string until no more macros are found */
    while ((textPtr = strchr(text, '$')) != NULL)
    {
        /* Copy skipped text to destText */
        if (textPtr > text) {
            int len = textPtr - text;
            if (destAlloced) {
                memcpy(destText, text, len);
            }
            singletonMacro = 0;
            text += len;
            textLength -= len;
            destText += len;
        }

        /* skip the '$' */
        text++;
        textLength--;

        /* process the macro */

        /* find the end of the macro name and let textPtr point to it. */
        /* macro specified with braces? */
        if(*text == '{')
        {
            textPtr = text;
            if (! FindMatchingBrace(task, &textPtr)) {
                if (destAlloced) {
                    FreeMemory(*newText);
                }
                *newText = NULL;
                
                return FALSE;
            }
            macro = DuplicateSubString(text + 1, textPtr - text - 2);
        }
        else if (isalnum(*text) || *text == '_' || *text == '-')
        {
            /* start of macro, no braces, copy until first non
             * alphanumeric (including '_' and '-') character.
             */
            textPtr = text+1;
            while (isalnum(*textPtr) || *textPtr == '_' || *textPtr == '-')
                textPtr++;
            macro = DuplicateSubString(text, textPtr - text);
        }
        else
        {
            /* macro-name consisting of a single control character.
             */
            int len = ((*text == NUL) ? 0 : 1);
            textPtr = text + len;
            macro = DuplicateSubString(text, len);
        }
        
        /* skip macro name */
        skipped = textPtr - text;
        text = textPtr;
        textLength -= skipped;

        if (strcmp(macro, "$") == 0)
        {
            /* special case, expand $$ to $. */
            expansion = "$";
            expansionLength = 1;
        }
        else
        {
            uint type;

            /* Recursivly expand macro */
            if (!ExpandMacrosInString(task, macro, &newMacro,
                                      &dummy, &macroChanged)) {
                FreeMemory(macro);
                if (destAlloced) {
                    FreeMemory(*newText);
                }
                *newText = NULL;
                return FALSE;
            }
            
            if (macroChanged) {
                FreeMemory(macro);
                macro = newMacro;
            }
            
            /* make sure variable exists in store */
            if(VariableExists(task->varstore, macro) != TRUE)
            {
                HtpMsg(MSG_ERROR, task->infile, 
                       "unrecognized macro name \"%s\"", macro);
                FreeMemory(macro);
                if (destAlloced) {
                    FreeMemory(*newText);
                }
                *newText = NULL;
                
                return FALSE;
            }
            
            type = GetVariableType(task->varstore, macro);
            /* block macros need a special expand routine */
            if (type == VAR_TYPE_BLOCK_MACRO)
            {
                STREAM *blockFile = 
                    (STREAM*) GetVariableValue(task->varstore, macro);
                assert(blockFile->sflags == STREAM_FLAG_BUFFER);
                
                expansion = blockFile->u.buffer.buffer;
                expansionLength = blockFile->u.buffer.offset;
            }
            else if (type == VAR_TYPE_SET_MACRO)
            {
                /* get the macros value and replace the attribute value */
                expansion = GetVariableValue(task->varstore, macro);
                if (expansion == NULL)
                    expansion = "";
                expansionLength = strlen(expansion);
            }
            else
            {
                if (destAlloced) {
                    FreeMemory(*newText);
                }
                *newText = NULL;
                
                HtpMsg(MSG_ERROR, task->infile, 
                       "macro \"%s\" is a meta-tag", macro);
                FreeMemory(macro);
                return FALSE;
            }
            
            HtpMsg(MSG_INFO, task->infile, "expanding macro \"%s\" to \"%s\"",
                   macro, expansion);
        }
        FreeMemory(macro);
        
        destTextSize += expansionLength - skipped - 1;

        if (expansionLength > skipped || !destAlloced) {
            copiedSoFar = destText - *newText;
            destText = AllocMemory(destTextSize);
            if (destText == NULL) {
                if (destAlloced) {
                    FreeMemory(*newText);
                }
                *newText = NULL;
                
                HtpMsg(MSG_ERROR, task->infile, "Out of memory");
                return FALSE;
            }

            memcpy(destText, *newText, copiedSoFar);
            if (destAlloced) {
                FreeMemory(*newText);
            }
            
            *newText = destText;
            destText += copiedSoFar;
            destAlloced = TRUE;
        }
        
        /* Macro did not change, copy it directly */
        memcpy(destText, expansion, expansionLength);
        destText += expansionLength;

        /* increment the change count */
        *changed = TRUE;

        if (singletonMacro && !textLength) {
            /* since the macro is the entire value, no harm (and */
            /* more robust) to surround it by quotes */
            *quotes = QUOTES_DOUBLE;
            *destText = 0;
            return TRUE;
        }

        /* need to go back and re-evaluate the value for more macros */
    }

    if (destAlloced) {
        memcpy(destText, text, textLength);
        destText += textLength;
        *destText = 0;
    }
    return TRUE;
}

BOOL ExpandMacros(TASK *task, HTML_MARKUP *htmlMarkup)
{
    HTML_ATTRIBUTE *attrib;
    char *newName, *newValue, *newTag;
    int quotes;
    BOOL changed;

    assert(task != NULL);
    assert(htmlMarkup != NULL);

    /* expand any macros in the tags */
    if(htmlMarkup->tag != NULL)
    {
        if(ExpandMacrosInString(task, htmlMarkup->tag, &newTag,
                                &quotes, &changed) != TRUE)
        {
            return FALSE;
        }

        if(changed)
        {
            ChangeMarkupTag(htmlMarkup, newTag);
        }
    }
    
    if (stricmp(htmlMarkup->tag, "WHILE") == 0
        || stricmp(htmlMarkup->tag, "!---") == 0) {
        return TRUE;
    }

    /* do the same for all attributes, both name and value */
    attrib = htmlMarkup->attrib;
    while (attrib != NULL)
    {
        if(attrib->name != NULL)
        {
            if (!ExpandMacrosInString(task, attrib->name, &newName,
                                      &quotes, &changed))
            {
                return FALSE;
            }
        
            if(changed)
            {
                ChangeAttributeName(attrib, newName);
            }
        }


        if(attrib->value != NULL)
        {
            quotes = attrib->quotes;
            if (!ExpandMacrosInString(task, attrib->value, &newValue,
                                      &quotes, &changed))
            {
                return FALSE;
            }
        
            if(changed)
            {
                ChangeAttributeValue(attrib, newValue, quotes);
            }
        }
        attrib = attrib->next;
    } 
    
    return TRUE;
}   


uint ExpandMetatag(TASK *task, HTML_MARKUP *htmlMarkup)
{
    const char *options;
    char *optionCopy;
    FIND_TOKEN findToken;
    char *optionPtr;
    HTML_ATTRIBUTE *attrib;
    VARSTORE defVarstore;
    uint flag;
    char *value;
    const char *defName;
    STREAM *blockStream;
    STREAM *defStream;
    STREAM defFile;
    TASK newTask;
    BOOL result;
    BOOL hasWildcard;
    uint macroType;

    /* first things first: find the tag in the metatag store */
    if(VariableExists(task->varstore, htmlMarkup->tag) == FALSE)
    {
        /* don't change a thing */
        return MARKUP_OKAY;
    }

    /* verify the macro in the store is a metatag definition */
    macroType = GetVariableType(task->varstore, htmlMarkup->tag);

    if(macroType != VAR_TYPE_DEF_MACRO
       && macroType != VAR_TYPE_BLOCKDEF_MACRO)
    {
        return MARKUP_OKAY;
    }

    /* get a pointer to the name */
    defName = htmlMarkup->tag;

    /* get the filename the DEF macro is held in */
    if ((defStream = (STREAM *) GetVariableValue(task->varstore, defName))
        == NULL)
    {
        /* this shouldnt be */
        HtpMsg(MSG_ERROR, task->infile,
               "DEF macro \"%s\" was not store properly", defName);
        return MARKUP_ERROR;
    }

    /* get options to compare against markups paramater list */
    options = GetVariableParam(task->varstore, defName);

    /* initialize a local variable store, even if its not used */
    InitializeVariableStore(&defVarstore);

    /* if NULL, then no options allowed */
    if (options != NULL) {
        /* options should be space-delimited, use StringToken() */
        if((optionCopy = DuplicateString(options)) == NULL)
        {
            HtpMsg(MSG_ERROR, task->infile, "Unable to duplicate option macro (out of memory?)");
            DestroyVariableStore(&defVarstore);
            return MARKUP_ERROR;
        }
        
        hasWildcard = FALSE;

        /* build array of pointers to null-terminated option */
        optionPtr = StringFirstToken(&findToken, optionCopy, " ");
        while (optionPtr != NULL)
        {
            /* ignore multiple spaces */
            if (*optionPtr == NUL)
                continue;
            if (strcmp(optionPtr, "*") == 0)
            {
                hasWildcard = TRUE;
            }
            else if ((attrib = UnlinkAttributeInMarkup(htmlMarkup,
                                                       optionPtr)) != NULL)
            {
                /* since this is a good attribute, add it to the
                 * local store 
                 */
                
                flag = VAR_FLAG_DEALLOC_NAME | VAR_FLAG_DEALLOC_VALUE;
                value = attrib->value;
                if (value == NULL) {
                    value = "";
                    flag &= ~VAR_FLAG_DEALLOC_VALUE;
                }
                
                if(StoreVariable(&defVarstore, attrib->name, attrib->value,
                                 VAR_TYPE_SET_MACRO, 
                                 flag, NULL, NULL) == FALSE)
                {
                    HtpMsg(MSG_ERROR, task->infile,
                           "Unable to store local macro for metatag");
                    DestroyVariableStore(&defVarstore);
                    DestroyAttribute(attrib);
                    FreeMemory(attrib);
                    FreeMemory(optionCopy);
                    return MARKUP_ERROR;
                }
                if (attrib->whitespace)
                FreeMemory(attrib->whitespace);
                FreeMemory(attrib);
            }
            else
            {
                char *name = DuplicateString(optionPtr);
                /* This option is undefined.  Add an explicit
                 * undefined value to hide variables with same
                 * name in outer scope.
                 */
                if (StoreVariable(&defVarstore, name, NULL,
                                  VAR_TYPE_UNKNOWN, 
                                  VAR_FLAG_DEALLOC_NAME
                                  | VAR_FLAG_UNDEFINED,
                                  NULL, NULL) == FALSE) {
                    HtpMsg(MSG_ERROR, task->infile,
                           "Unable to store local macro for metatag");
                    DestroyVariableStore(&defVarstore);
                    FreeMemory(name);
                    FreeMemory(optionCopy);
                    return MARKUP_ERROR;
                }
            }
            optionPtr = StringNextToken(&findToken);
        }
        FreeMemory(optionCopy);
    
        if (hasWildcard)
        {
            uint wildcardLength = 0;
            char *wildcards;
            
            /* get length of wildcard parameter */
            attrib = htmlMarkup->attrib;
            while (attrib != NULL)
            {
                wildcardLength += strlen(attrib->name) + 1;
                if (attrib->value) {
                    wildcardLength += strlen(attrib->value) + 1;
                    if (attrib->quotes)
                        wildcardLength += 2;
                }
                attrib = attrib->next;
            }

            if (wildcardLength > 0) {
                /* Build the wildcard string. */
                char *ptr;

                wildcards = AllocMemory(wildcardLength);
                ptr = wildcards;
                while ((attrib = htmlMarkup->attrib) != NULL) {
                    ptr = stpcpy(ptr, attrib->name);
                    if (attrib->value) {
                        *ptr++ = '=';
                        if (attrib->quotes == QUOTES_DOUBLE)
                            *ptr++ = '"';
                        else if (attrib->quotes == QUOTES_SINGLE)
                            *ptr++ = '\'';
                        ptr = stpcpy(ptr, attrib->value);
                        if (attrib->quotes == QUOTES_DOUBLE)
                            *ptr++ = '"';
                        else if (attrib->quotes == QUOTES_SINGLE)
                            *ptr++ = '\'';
                    }
                    *ptr++ = ' ';
                    htmlMarkup->attrib = attrib->next;
                    DestroyAttribute(attrib);
                    FreeMemory(attrib);
                }
                /* Replace last space with NUL to terminate wildcards */
                *(ptr-1) = 0;
                flag = VAR_FLAG_DEALLOC_VALUE;
            } else {
                wildcards = "";
                flag = VAR_FLAG_NONE;
            }

            if(StoreVariable(&defVarstore, "*", wildcards,
                             VAR_TYPE_SET_MACRO, flag,
                             NULL, NULL) == FALSE)
            {
                HtpMsg(MSG_ERROR, task->infile,
                       "Unable to store local macro for metatag");
                DestroyVariableStore(&defVarstore);
                if ((flag & VAR_FLAG_DEALLOC_VALUE))
                    FreeMemory(wildcards);
                return MARKUP_ERROR;
            }
        }
    }

    /* If this is a BLOCKDEF macro expand the trailing block into */
    /* the macro named block */
    if (macroType == VAR_TYPE_BLOCKDEF_MACRO) 
    {
        blockStream = (STREAM*) AllocMemory(sizeof(STREAM));
        if (!ReadinBlock(task, htmlMarkup, blockStream)) 
        {
            DestroyVariableStore(&defVarstore);
            return MARKUP_ERROR;
        }

        /* store the block file name and the block macro name as a variable */
        if(StoreVariable(&defVarstore, "BLOCK", blockStream, 
                         VAR_TYPE_BLOCK_MACRO, VAR_FLAG_NONE,
                         NULL, BlockDestructor) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, 
                   "unable to store macro information (out of memory?)");
            DestroyVariableStore(&defVarstore);
            return MARKUP_ERROR;
        }
    }

    if (htmlMarkup->attrib != NULL) {
        HtpMsg(MSG_ERROR, task->infile,
               "%s metatag \"%s\" does not accept a parameter named \"%s\"",
               macroType == VAR_TYPE_BLOCKDEF_MACRO ? "blockdef" : "def",
               defName, htmlMarkup->attrib->name);
        DestroyVariableStore(&defVarstore);
        return MARKUP_ERROR;
    }
    
    /* expand the DEF macro like a block macro ... */

    /* open the file the macro is held in */
    if(CreateBufferReader(&defFile, defStream) == FALSE)
    {
        HtpMsg(MSG_ERROR, task->infile,
               "unable to open block for %s metatag \"%s\"",
               macroType == VAR_TYPE_BLOCKDEF_MACRO ? "blockdef" : "def",
               defName);
        DestroyVariableStore(&defVarstore);
        return MARKUP_ERROR;
    }

    HtpMsg(MSG_INFO, task->infile, "dereferencing %s metatag \"%s\"", 
           macroType == VAR_TYPE_BLOCKDEF_MACRO ? "blockdef" : "def",
           defName);

    /* build a new task structure */
    newTask.infile = &defFile;
    newTask.outfile = task->outfile;
    newTask.sourceFilename = task->sourceFilename;

    if (options != NULL || macroType == VAR_TYPE_BLOCKDEF_MACRO) {
        /* Put in the defVarstore */
        PushVariableStoreContext(task->varstore, &defVarstore);
        newTask.varstore = &defVarstore;
    } else {
        newTask.varstore = task->varstore;
    }


    /* process the new input file */
    result = ProcessTask(&newTask);

    /* remove the new context if necessary */
    if(newTask.varstore == &defVarstore)
    {
        assert(defVarstore.child == NULL);
        PopVariableStoreContext(&defVarstore);
    }

    /* no matter what, destroy the local store */
    DestroyVariableStore(&defVarstore);

    CloseStream(&defFile);

    if (!result) {
        /* Error message was already spitted out.  However, we should
         * give a hint where the meta-tag was called.
         */
        HtpMsg(MSG_ERROR, task->infile,
               "... in metatag \"%s\"", defName);
        return MARKUP_ERROR;
    }

    return DISCARD_MARKUP;
}


uint ExpandAll(TASK *task, HTML_MARKUP *htmlMarkup, 
               char** newPlaintextPtr, uint markupType)
{
    uint ctr;
    uint markupResult;

    if(ExpandMacros(task, htmlMarkup) == FALSE)
    {
        /* problem encountered trying to expand macros */
        return MARKUP_ERROR;
    }

    /* give the metatag processor a chance to expand metatags */
    /* this is a little strange, but if MARKUP_OKAY it means the the */
    /* metatag processor didnt recognize the tag, and therefore should */
    /* be handled by the other processors */
    if((markupResult = ExpandMetatag(task, htmlMarkup)) == MARKUP_OKAY)
    {
        /* find the first processor that wants to do something with the */
        /* markup tag */
        for(ctr = 0; ctr < MARKUP_PROCESSOR_COUNT; ctr++)
        {
            if(markupProcessor[ctr].markupType & markupType)
            {
                if(IsMarkupTag(htmlMarkup, markupProcessor[ctr].tag))
                {
                    assert(markupProcessor[ctr].markupFunc != NULL);
                    
                    markupResult = markupProcessor[ctr]
                        .markupFunc(task, htmlMarkup, newPlaintextPtr);
                    
                    break;
                }
            }
        }
    }
    
    /* unless the function requested to use its new markup string, */
    /* take the HTML_MARKUP structure and build a new markup */
    if((markupResult != NEW_MARKUP) && (markupResult != DISCARD_MARKUP))
    {
        if(MarkupToPlaintext(htmlMarkup, newPlaintextPtr) == FALSE)
        {
            HtpMsg(MSG_ERROR, task->infile, "unable to build plain text from markup (out of memory?)");
            return MARKUP_ERROR;
        }
    }

    return markupResult;
}
