/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
/*
//
// htp.c
//
// main(), major functionality modules
//
// 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 "defs.h"

#include "htp-files.h"
#include "macro.h"
#include "option.h"

/* The global and project specific htp.def files.  These are used for
 * dependency checking.
 */
static char globalFilename[MAX_PATHNAME_LEN];
static char projectFilename[MAX_PATHNAME_LEN];

/*
// HTML file stream functions
*/

BOOL ProcessTask(TASK *task)
{
    char *newPlaintext;
    uint markupResult;
    HTML_MARKUP htmlMarkup;
    BOOL result;
    uint markupType;

    assert(task != NULL);
    assert(task->infile != NULL);
    assert(task->outfile != NULL);
    assert(task->varstore != NULL);

    task->conditionalLevel = 0;

    for(;;)
    {
        result = ReadHtmlMarkup(task->infile, task->outfile,
                                &markupType, &htmlMarkup);
        if(result == ERROR)
        {
            /* problem reading in the file */
            return FALSE;
        }
        else if(result == FALSE)
        {
            /* end-of-file */
            break;
        }

        newPlaintext = NULL;
        markupResult = ExpandAll(task, &htmlMarkup, &newPlaintext, markupType);
        
        /* destroy the structure, now only interested in the markup string */
        DestroyMarkupStruct(&htmlMarkup);

        switch(markupResult)
        {
            case MARKUP_OKAY:
            {
                /* add the markup to the output file as it should appear */
                StreamPrintF(task->outfile, "%c%s%c", 
                             MARKUP_OPEN_DELIM(markupType),
                             newPlaintext, MARKUP_CLOSE_DELIM(markupType));
            }
            break;

            case NEW_MARKUP:
            case MARKUP_REPLACED:
            {
                /* the markup has been replaced by a normal string */
                PutStreamString(task->outfile, newPlaintext);
            }
            break;

            case DISCARD_MARKUP:
            {
                /* markup will not be included in final output */
            }
            break;

            case MARKUP_ERROR:
            {
                /* (need to destroy plaintext buffer before exiting) */
                FreeMemory(newPlaintext);
                return FALSE;
            }

            default:
            {
                FreeMemory(newPlaintext);
                printf("%s: serious internal error\n", PROGRAM_NAME);
                exit(1);
            }
        }

        /* free the plain text buffer and continue with processing */
        if (newPlaintext)
        {
            FreeMemory(newPlaintext);
            newPlaintext = NULL;
        }
    }

    if (task->conditionalLevel > 0)
    {
        HtpMsg(MSG_ERROR, task->infile, "Missing </IF> for <IF> in line %d",
               task->conditionalLine[task->conditionalLevel-1]);
    }

    return TRUE;
}

BOOL ProcessFileByName(VARSTORE *parentVarStore, 
                       const char *in, const char *out)
{
    STREAM infile;
    STREAM outfile;
    TASK task;
    BOOL result;
    const char *templateFile;
    /*
    // the "file" variable store ... holds macros only for files in current
    // directory, or project (this is loaded from the project default file,
    // which is called htp.def)
    */
    static VARSTORE fileVarStore;

    assert(in != NULL);
    assert(out != NULL);


    /* assume no processing required */
    result = TRUE;

    /* check the global and project default files first */
    if(*globalFilename != NUL)
    {
        result = FullyCheckDependencies(globalFilename, out);
    }

    if((result == TRUE) && (*projectFilename != NUL))
    {
        result = FullyCheckDependencies(projectFilename, out);
    }

    /* check the dependencies of the target file to see whether or not */
    /* to proceed ... the global and project default files are checked as well */
    if(result == TRUE)
    {
        result = FullyCheckDependencies(in, out);
    }

    /* if TRUE, no need to go any further */
    if(result == TRUE)
    {
        /* explain why no processing required, and return as if processing */
        /* was completed */
        printf("%s: File \"%s\" is completely up to date.\n", PROGRAM_NAME,
            out);
        return TRUE;
    }

    /* continue, at least one file was found that requires out to be updated */

    /* initialize the project variable store and push it onto context */
    InitializeVariableStore(&fileVarStore);
    PushVariableStoreContext(parentVarStore, &fileVarStore);
    fileVarStore.isGlobal = TRUE;

    /* open the output file */
    if(CreateFileWriter(&outfile, out, FALSE) == FALSE)
    {
        printf("%s: unable to open file \"%s\" for writing\n", PROGRAM_NAME, out);
        DestroyVariableStore(&fileVarStore);
        return FALSE;
    }

    if(CreateFileReader(&infile, in) == FALSE)
    {
        printf("%s: unable to open file \"%s\" for reading\n", 
               PROGRAM_NAME, in);
        CloseStream(&outfile);
        DestroyVariableStore(&fileVarStore);
        return FALSE;
    }

    if(InitializeLocalOption() == FALSE)
    {
        printf("%s: unable to initialize local option store\n", 
               PROGRAM_NAME);
        CloseStream(&infile);
        CloseStream(&outfile);
        DestroyVariableStore(&fileVarStore);
        return FALSE;
    }

    /* build a task structure */
    task.infile = &infile;
    task.outfile = &outfile;
    task.varstore = &fileVarStore;
    task.sourceFilename = in;
    StoreVariable(&fileVarStore, "_htpfile_in", (void *) in, 
                  VAR_TYPE_SET_MACRO, VAR_FLAG_NONE, NULL, NULL);
    StoreVariable(&fileVarStore, "_htpfile_out", (void *) out, 
                  VAR_TYPE_SET_MACRO, VAR_FLAG_NONE, NULL, NULL);
    printf("%s: Processing file \"%s\" to output file \"%s\" ...\n",
        PROGRAM_NAME, in, out);

    result = ProcessTask(&task);

    /* done with this file, want to reuse struct */
    CloseStream(&infile);

    /* need to check for a template file */
    if((result == TRUE) && (VariableExists(&fileVarStore, VAR_TEMPLATE_NAME)))
    {
        
        templateFile = GetVariableValue(&fileVarStore, VAR_TEMPLATE_NAME);
        
        if(CreateFileReader(&infile, templateFile) == FALSE)
        {
            printf("%s: unable to open template file \"%s\"\n",
                   PROGRAM_NAME, templateFile);
            
            CloseStream(&outfile);
            DestroyLocalOption();
            DestroyVariableStore(&fileVarStore);
            
            return FALSE;
        }
        
        task.infile = &infile;
        task.outfile = &outfile;
        task.varstore = &fileVarStore;
        task.sourceFilename = in;
        
        printf("%s: Processing template file \"%s\" ...\n", PROGRAM_NAME,
               templateFile);
        
        result = ProcessTask(&task);
        CloseStream(&infile);
    }
        
    if(result == TRUE)
    {
        printf("%s: final output file \"%s\" successfully created\n\n",
               PROGRAM_NAME, outfile.name);
    }
    else
    {
        printf("\n%s: error encountered, file \"%s\" not completed\n\n",
               PROGRAM_NAME, outfile.name);
    }

    CloseStream(&outfile);

    /* destroy incomplete file if not configured elsewise */
    if(result != TRUE)
    {
        if(PRECIOUS == FALSE)
        {
            assert(out != NULL);
            remove(out);
        }
    }

    /* destroy the local options for these files */
    DestroyLocalOption();

    /* destroy the project store as well */
    DestroyVariableStore(&fileVarStore);

    return result;
}

BOOL ProcessDefaultFile(VARSTORE *varstore, const char *file)
{
    TASK defTask;
    STREAM infile;
    STREAM outfile;
    BOOL result;

    if(CreateFileReader(&infile, file) == FALSE)
    {
        printf("%s: unable to open default project file \"%s\"\n", 
               PROGRAM_NAME, file);
        return FALSE;
    }

    /* use a null outfile because there is no file to write to */
    CreateNullWriter(&outfile);
    
    /* build a task (just like any other) and process the file */
    /* use the global variable store to hold all the macros found */
    defTask.infile = &infile;
    defTask.outfile = &outfile;
    defTask.varstore = varstore;
    defTask.sourceFilename = file;
    
    printf("%s: Processing default file \"%s\" ... \n", 
           PROGRAM_NAME, file);
    
    result = ProcessTask(&defTask);
    
    CloseStream(&infile);
    CloseStream(&outfile);

    return result;
}

BOOL ProcessResponseFile(VARSTORE *parentVarStore, const char *resp)
{
    char textline[MAX_CMDLINE_LEN];
    char defResp[MAX_PATHNAME_LEN];
    char newDirectory[MAX_PATHNAME_LEN];
    char oldDirectory[MAX_PATHNAME_LEN];
    STREAM respfile;
    int result;
    char *in;
    char *out;
    char *ptr;
    BOOL useNewDir;
    BOOL respFileOpen;
    FIND_TOKEN findToken;
    uint numread;
    char nl_bitmap[3];
    VARSTORE projectVarStore;

    assert(resp != NULL);

    nl_bitmap[0] = '\n';
    nl_bitmap[1] = '\n';
    nl_bitmap[2] = '\n';
    useNewDir = FALSE;

    if(strchr(ALL_FILESYSTEM_DELIMITERS, resp[strlen(resp) - 1]) != NULL)
    {
        /* some tests as done to ensure that (a) newDirectory does not trail */
        /* with a directory separator and that (b) the separator is present */
        /* before appending the filename ... requirement (a) is a DOS issue */

        /* the response file is actually a directory the response file is */
        /* possibly kept in ... copy it to the newDirectory variable for */
        /* later use, but remove the trailing delimiter (MS-DOS issue) */
        strcpy(newDirectory, resp);
        newDirectory[strlen(newDirectory) - 1] = NUL;

        /* now, see if default response file is present */
        strcpy(defResp, newDirectory);
        strcat(defResp, DIR_DELIMITER_STRING);
        strcat(defResp, DEFAULT_RESPONSE_FILE);

        useNewDir = TRUE;

        respFileOpen = CreateFileReader(&respfile, defResp);
    }
    else
    {
        respFileOpen = CreateFileReader(&respfile, resp);
    }

    if(respFileOpen == FALSE)
    {
        printf("%s: unable to open \"%s\" as a response file\n", PROGRAM_NAME,
               resp);
        return FALSE;
    }

    printf("%s: Processing response file \"%s\" ...\n", PROGRAM_NAME,
           respfile.name);

    /* processing a response file in another directory, change to that */
    /* directory before processing the files */
    if(useNewDir)
    {
        getcwd(oldDirectory, sizeof oldDirectory);
        chdir(newDirectory);
    }

    /* initialize the variable store for response file and push it */
    InitializeVariableStore(&projectVarStore);
    PushVariableStoreContext(parentVarStore, &projectVarStore);
    projectVarStore.isGlobal = TRUE;

    /* find the local project default file */
    if(FileExists("htp.def"))
    {
        StringCopy(projectFilename, "htp.def", MAX_PATHNAME_LEN);
        ProcessDefaultFile(&projectVarStore, projectFilename);
    }
    else
    {
        projectFilename[0] = NUL;
    }

    result = TRUE;
    do
    {
        numread = GetStreamBlock(&respfile, textline, sizeof(textline), 
                                 nl_bitmap);
        if(numread == 0)
        {
            /* EOF */
            break;
        }
        /* Kill the newline at the end of the line */
        if (textline[numread-1] == '\n')
            textline[numread-1] = NUL;

        in = NULL;
        out = NULL;

        /* walk tokens ... allow for tab character as token and ignore */
        /* multiple token characters between filenames */
        ptr = StringFirstToken(&findToken, textline, " \t");
        while(ptr != NULL)
        {
            /* is this just a repeated token? */
            if((*ptr == ' ') || (*ptr == '\t') || !(*ptr))
            {
                ptr = StringNextToken(&findToken);
                continue;
            }

            /* found something ... like parsing the command-line, look for */
            /* options, then response files, then regular in and out filenames */
            if(*ptr == '-')
            {
                ParseToken(NULL, ptr+1);
            }
            else if(*ptr == ';')
            {
                /* comment, ignore the rest of the line */
                break;
            }
            else if(in == NULL)
            {
                in = ptr;
            }
            else if(out == NULL)
            {
                out = ptr;
            }
            else
            {
                /* hmm ... extra information on line */
                HtpMsg(MSG_WARNING, &respfile, 
                       "extra option \"%s\" specified in response file, ignoring",
                       ptr);
            }

            ptr = StringNextToken(&findToken);
        }

        if (in == NULL)
            continue;

        if (out == NULL)
        {
            char tempFilename[MAX_PATHNAME_LEN];
            StringCopy(tempFilename, projectFilename, MAX_PATHNAME_LEN);

            /* in is the response file.
             * recurse but with the parentVarStore.
             */
            PopVariableStoreContext(&projectVarStore);
            result = ProcessResponseFile(parentVarStore, in);
            PushVariableStoreContext(parentVarStore, &projectVarStore);

            /* restore the projectFilename for dependency checking */
            StringCopy(projectFilename, tempFilename, MAX_PATHNAME_LEN);
        }
        else if((in != NULL) && (out != NULL))
        {
            /* both in and out were specified, process a file */
            result = ProcessFileByName(&projectVarStore, in, out);
        }
    } while(result == TRUE);

    /* destroy the response files store */
    DestroyVariableStore(&projectVarStore);

    CloseStream(&respfile);

    /* restore the directory this all started in */
    if(useNewDir)
    {
        chdir(oldDirectory);
    }

    return result;
}   

int main(int argc, char *argv[])
{
    int result;
    uint ctr;
    char *in;
    char *out;
    char *resp;

    /*
    // the global variable store ... holds permanent, file-to-file macros
    // (these are set in the global default file) ... filename kept for
    // dependency checking
    */
    VARSTORE globalVarStore;

    DisplayHeader();

    if(argc == 1)
    {
        usage();
        return 1;
    }

    /* initialize debugging */
#if DEBUG
    DebugInit("htpdeb.out");
    atexit(DebugTerminate);
#endif

#ifdef USE_SUBALLOC
    /* initialize the suballoc memory module */
    InitializeMemory();
    atexit(TerminateMemory);
#endif

    /* initialize global variable options */
    if(InitializeGlobalOption() == FALSE)
    {
        printf("%s: fatal error, unable to initialize internal options\n",
            PROGRAM_NAME);
        return 1;
    }

    in = NULL;
    out = NULL;
    resp = NULL;

    /* search command-line for options */
    for(ctr = 1; ctr < (uint) argc; ctr++)
    {
        if(*argv[ctr] == '-')
        {
            /* command-line option specified */
            ParseToken(NULL, argv[ctr]+1);
        }
        else if(*argv[ctr] == '@')
        {
            /* response file specified */
            resp = argv[ctr] + 1;
            if(*resp == NUL)
            {
                resp = (char *) DEFAULT_RESPONSE_FILE;
            }
        }
        else if(in == NULL)
        {
            /* input file was specified */
            in = argv[ctr];
        }
        else if(out == NULL)
        {
            /* output file was specified */
            out = argv[ctr];
        }
        else
        {
            printf("%s: unknown argument \"%s\" specified\n",
                PROGRAM_NAME, argv[ctr]);
            return 1;
        }
    }
    
    if(USAGE == TRUE)
    {
        usage();
        return 1;
    }

    if((in == NULL || out == NULL) && (resp == NULL))
    {
        usage();
        return 1;
    }

    /* initialize the global variable store before proceeding */
    if(InitializeVariableStore(&globalVarStore) != TRUE)
    {
        printf("%s: unable to initialize global variable store (out of memory?)\n",
            PROGRAM_NAME);
        return 1;
    }
    globalVarStore.isGlobal = TRUE;

    /* store htp version number in global store */
    StoreVariable(&globalVarStore, "_htp_version", VER_STRING, 
                  VAR_TYPE_SET_MACRO, VAR_FLAG_NONE, NULL, NULL);

    /* before reading in the response file or processing any files, handle */
    /* the default file, if there is one ... all of its macros are held in */
    /* the global variable store */
    if(HtpDefaultFilename(globalFilename, MAX_PATHNAME_LEN))
    {
        ProcessDefaultFile(&globalVarStore, globalFilename);
    }
    else 
    {
        globalFilename[0] = NUL;
    }

    /* now, process the response file (if there is one) or the files */
    /* specified on the command-line */
    if(resp != NULL)
    {
        result = ProcessResponseFile(&globalVarStore, resp);
    }
    else
    {
        /* find the local project default file */
        if(FileExists("htp.def"))
        {
            StringCopy(projectFilename, "htp.def", MAX_PATHNAME_LEN);
            ProcessDefaultFile(&globalVarStore, projectFilename);
        }
        else
        {
            projectFilename[0] = NUL;
        }
        result = ProcessFileByName(&globalVarStore, in, out);
    }

    /* display varstore stats */
    DEBUG_PRINT(("Variable lookups=%u  string cmps=%u  rotations=%u\n",
                 variableLookups, variableStringCompares, variableRotations));

    /* destroy the global variable store */
    DestroyVariableStore(&globalVarStore);

    /* destroy global option */
    DestroyGlobalOption();

#ifdef USE_SUBALLOC
    /* display suballoc stats */
    DEBUG_PRINT(("suballoc  total allocations=%u  free pool hits=%u  system heap allocs=%u\n",
        totalAllocations, freePoolHits, totalAllocations - freePoolHits));
#endif

    return (result == TRUE) ? 0 : 1;
}
