/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1994, 1998-2002, 2004, 2006, 2008, 2010, 2012 Peter Miller
 *
 * Derived from a work
 * Copyright (C) 1990 David I. Bell.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or (at
 * your option) any later version.
 *
 * 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, see <http://www.gnu.org/licenses/>.
 *
 *
 * Program to perform source control operations for modules.
 */

#include <common/ac/ctype.h>
#include <common/ac/sys/types.h>
#include <common/ac/stdio.h>
#include <common/ac/errno.h>
#include <setjmp.h>
#include <common/ac/dirent.h>
#include <common/ac/stdlib.h>
#include <common/ac/string.h>
#include <common/ac/unistd.h>
#include <common/ac/sys/stat.h> /* for mkdir */
#include <libexplain/access.h>
#include <libexplain/closedir.h>
#include <libexplain/malloc.h>
#include <libexplain/opendir.h>
#include <libexplain/program_name.h>
#include <libexplain/readdir.h>
#include <libexplain/realloc.h>
#include <libexplain/strdup.h>

#include <common/arglex.h>
#include <common/cmalloc.h>
#include <common/compare.h>
#include <common/error_intl.h>
#include <common/expand.h>
#include <common/help.h>
#include <common/isdir.h>
#include <common/quit.h>
#include <common/str.h>
#include <common/trace.h>
#include <common/version.h>

#include <fhist/breaks.h>
#include <fhist/diff.h>
#include <fhist/extract.h>
#include <fhist/fhist.h>
#include <fhist/list.h>
#include <fhist/modlin.h>
#include <fhist/name.h>
#include <fhist/prune.h>
#include <fhist/subroutine.h>
#include <fhist/update.h>


/*
 * Actions to be performed for modules
 */
#define A_CREATE        0x0001 /* want to create */
#define A_UPDATE        0x0002 /* want to update */
#define A_EXTRACT       0x0004 /* want to extract */
#define A_LIST          0x0008 /* want to list changes */
#define A_DIFF          0x0010 /* want to list differences */
#define A_TYPE          0x0020 /* want to type output */
#define A_NAME          0x0080 /* want to set edit name */
#define A_CLEAN         0x0100 /* clean away up to date copy of module */
#define A_CONDUPDATE    0x0200 /* do update if there are differences */
#define A_CHECK         0x0400 /* check to see if a file is up to date */
#define A_PRUNE         0x0800 /* prune old edits from edithistory */


/*
 * Definitions of which actions accept various extra arguments
 */
#define REMARK_ACTIONS  (A_CREATE | A_UPDATE | A_CONDUPDATE)
#define NAME_ACTIONS    (REMARK_ACTIONS | A_NAME)
#define INPUT_ACTIONS   (REMARK_ACTIONS | A_DIFF | A_CHECK | A_CLEAN)
#define OUTPUT_ACTIONS  (A_EXTRACT | A_LIST | A_DIFF)


SC_DATA         sc;             /* storage for sc program */


enum
{
    arglex_token_all = arglex_token_MAX1,
    arglex_token_binary,
    arglex_token_check,
    arglex_token_clean,
    arglex_token_conditional_update,
    arglex_token_create,
    arglex_token_debug,
    arglex_token_difference,
    arglex_token_difference_update,
    arglex_token_extract,
    arglex_token_force_writing,
    arglex_token_forced_update,
    arglex_token_input,
    arglex_token_list,
    arglex_token_make_path,
    arglex_token_modified,
    arglex_token_nname,
    arglex_token_no_write,
    arglex_token_output,
    arglex_token_path,
    arglex_token_prune,
    arglex_token_quick,
    arglex_token_remark,
    arglex_token_remark_string,
    arglex_token_terminal,
    arglex_token_trace,
    arglex_token_update,
    arglex_token_verbose,
    arglex_token_what,
    arglex_token_keywords_not,
    arglex_token_MAX2
};

static arglex_table_ty argtab[] =
{
    {"-All", arglex_token_all, },
    {"-BINary", arglex_token_binary, },
    {"-CLean", arglex_token_clean, },
    {"-CReate", arglex_token_create, },
    {"-Check", arglex_token_check, },
    {"-Conditional_Update", arglex_token_conditional_update, },
    {"-DEbug", arglex_token_debug, },
    {"-Difference", arglex_token_difference, },
    {"-Difference_Update", arglex_token_difference_update, },
    {"-Extract", arglex_token_extract, },
    {"-Force_Writing", arglex_token_force_writing, },
    {"-Forced_Update", arglex_token_forced_update, },
    {"-Input", arglex_token_input, },
    {"-List", arglex_token_list, },
    {"-MaKe_Path", arglex_token_make_path, },
    {"-Modify", arglex_token_modified, },
    {"-Name", arglex_token_nname, },
    {"-No_Keywords", arglex_token_keywords_not, },
    {"-No_Write", arglex_token_no_write, },
    {"-Output", arglex_token_output, },
    {"-PRune", arglex_token_prune, },
    {"-Path", arglex_token_path, },
    {"-Quick", arglex_token_quick, },
    {"-Remark", arglex_token_remark, },
    {"-Remark_String", arglex_token_remark_string, },
    {"-Terminal", arglex_token_terminal, },
    {"-TRace", arglex_token_trace, },
    {"-Update", arglex_token_update, },
    {"-Verbose", arglex_token_verbose, },
    {"-What", arglex_token_what, },
    {0, 0, }, /* end marker */
};


static void
usage(void)
{
    const char      *progname;

    progname = explain_program_name_get();
    fprintf(stderr, "usage: %s <filename> <option>...\n", progname);
    fprintf(stderr, "       %s -Help\n", progname);
    fprintf(stderr, "       %s -VERSion\n", progname);
    quit(1);
}


static void
main_help(void)
{
    help(NULL, usage);
}


/*
 * Routine called after errors to restart the program for the next file.
 * This reenables breaks, closes all files, frees all allocated memory,
 * resets static variables, and then possibly longjmps back to the top
 * level code.
 */

static void
restart(void)
{
    int             i;

    breakson();
    cm_reset();
    fcompreset();
    editreset();
    sc.modifybuffer = NULL;
    sc.firstedit = 0;
    sc.lastedit = 0;
    sc.tablepos = 0;
    for (i = 3; i < 10; i++)
        close(i);
}


/*
 * Perform the specified action for a particular module.
 * If the wildcard flag is 1, then any input or output name is a
 * directory which needs to have the module name appended to it.
 */

static void
domodule(int action, const char *modulename, const char *inputname,
    const char *outputname, const char *editname, const char *editstring,
    const char *editstring2, int wildcard)
{
    unsigned        module_max;
    char            fullinputname[1024]; /* full input name if a dir */
    char            fulloutputname[1024]; /* full output name if a dir */

    trace(("domodule(name = \"%s\")\n{\n", modulename));
    restart();
    if (strchr(modulename, PATH_STR[0]))
    {
        sub_context_ty  *scp;

        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", modulename);
        fatal_intl
        (
            scp,
            i18n("module name \"$module\" cannot contain path separators")
        );
        /* NOTREACHED */
        sub_context_delete(scp);
    }
    module_max = pathconf_name_max(sc.dirname) - 2;
    if (strlen(modulename) > module_max)
    {
        sub_context_ty  *scp;

        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", modulename);
        fatal_intl(scp, i18n("module name \"$module\" is too long"));
        /* NOTREACHED */
        sub_context_delete(scp);
    }
    if (inputname && wildcard)
    {
        strcpy(fullinputname, inputname);
        strcat(fullinputname, PATH_STR);
        strcat(fullinputname, modulename);
        inputname = fullinputname;
    }
    if (outputname && wildcard)
    {
        strcpy(fulloutputname, outputname);
        strcat(fulloutputname, PATH_STR);
        strcat(fulloutputname, modulename);
        outputname = fulloutputname;
    }
    sc.modulename = modulename;
    strcpy(sc.basename, sc.dirname);
    strcat(sc.basename, PATH_STR);
    strcat(sc.basename, modulename);
    trace_string(sc.basename);
    strcpy(sc.sourcename, sc.basename);
    strcat(sc.sourcename, EXT_SOURCE);
    trace_string(sc.sourcename);
    strcpy(sc.historyname, sc.basename);
    strcat(sc.historyname, EXT_HISTORY);
    trace_string(sc.historyname);

    switch (action)
    {
    case A_EXTRACT:
        trace(("extract\n"));
        if (outputname == NULL)
            outputname = modulename;
        extracthistory(editstring, outputname, (fc.verbosity != 0));
        break;

    case A_CREATE:
        trace(("create\n"));
        if (inputname == NULL)
            inputname = modulename;
        createhistory(inputname, editname);
        break;

    case A_DIFF:
        if (inputname && editstring2)
        {
            fatal_intl(0, i18n("conflicting options for differences"));
        }
        if (inputname == NULL)
            inputname = modulename;
        diffhistory(inputname, outputname, editstring, editstring2, 1);
        break;

    case A_DIFF | A_UPDATE:
        if (inputname == NULL)
            inputname = modulename;
        diffhistory(inputname, outputname, editstring, editstring2, 0);
        if ((fc.inserts == 0) && (fc.deletes == 0))
        {
            if (fc.verbosity)
            {
                sub_context_ty  *scp;

                scp = sub_context_new();
                sub_var_set_charstar(scp, "Module", modulename);
                error_intl
                (
                    scp,
                    i18n("module \"$module\" does not need updating")
                );
                sub_context_delete(scp);
            }
            break;
        }
        printf("\n");
        updatehistory(inputname, editname);
        break;

    case A_CREATE | A_CONDUPDATE:
        trace(("creaye | condupdate\n"));
        if (inputname == NULL)
            inputname = modulename;
        if (!history_file_exists())
        {
            createhistory(inputname, editname);
            break;
        }
        /* fall through... */

    case A_CONDUPDATE:
        trace(("condupdate\n"));
        if (inputname == NULL)
            inputname = modulename;
        comphistory(inputname);
        if ((fc.inserts == 0) && (fc.deletes == 0))
        {
            if (fc.verbosity)
            {
                sub_context_ty  *scp;

                scp = sub_context_new();
                sub_var_set_charstar(scp, "Module", modulename);
                error_intl
                (
                    scp,
                    i18n("module \"$module\" does not need updating")
                );
                sub_context_delete(scp);
            }
            break;
        }
        updatehistory(inputname, editname);
        break;

    case A_CREATE | A_UPDATE:
        trace(("create | update\n"));
        if (inputname == NULL)
            inputname = modulename;
        if (!history_file_exists())
        {
            createhistory(inputname, editname);
            break;
        }
        /* fall through... */

    case A_UPDATE:
        trace(("update\n"));
        if (inputname == NULL)
            inputname = modulename;
        updatehistory(inputname, editname);
        break;

    case A_LIST:
        trace(("list\n"));
        listhistory(editstring, editstring2, outputname);
        break;

    case A_NAME:
        trace(("name\n"));
        namehistory(editname);
        break;

    case A_TYPE:
        trace(("type\n"));
        extracthistory(editstring, outputname, VERBOSE_NONE);
        break;

    case A_CLEAN:
        trace(("clean\n"));
        if (inputname == NULL)
            inputname = modulename;
        cleanhistory(inputname);
        break;

    case A_CHECK:
        trace(("check\n"));
        if (inputname == NULL)
            inputname = modulename;
        checkhistory(inputname);
        break;

    case A_PRUNE:
        trace(("prune\n"));
        prunehistory(editstring);
        break;
    }
    trace(("}\n"));
}


/*
 * Perform the specified action for all modules in the edit history directory.
 */

static void
doallmodules(int action, const char *inputname, const char *outputname,
    const char *editstring, const char *editstring2, const char *editname)
{
    char            *modulename;        /* current file name */
    DIR             *dirp;              /* directory handle */
    char            *buffer;            /* buffer for name storage */
    size_t          bufsize;            /* current size of buffer */
    size_t          bufindex;           /* index into buffer */

    bufindex = 0;
    bufsize = 1000;
    buffer = explain_malloc_or_die(bufsize);
    dirp = explain_opendir_or_die(sc.dirname);
    for (;;)
    {
        struct dirent   *dp;                /* current directory entry */
        size_t          len;

        dp = explain_readdir_or_die(dirp);
        if (!dp)
            break;

        modulename = dp->d_name;
        len = strlen(modulename);
        if (len <= 2)
            continue;
        len -= 2;
        if ((modulename[len] != '.') || (modulename[len + 1] != 'e'))
            continue;
        modulename[len] = '\0';
        if ((bufindex + len + 2) > bufsize)
        {
            bufsize = bufsize * 2 + 1024;
            buffer = explain_realloc_or_die(buffer, bufsize);
        }
        strcpy(&buffer[bufindex], modulename);
        bufindex += (len + 1);
    }
    explain_closedir_or_die(dirp);
    buffer[bufindex] = '\0';

    modulename = buffer;
    while (*modulename)
    {
        domodule
        (
            action,
            modulename,
            inputname,
            outputname,
            editname,
            editstring,
            editstring2,
            1
        );
        modulename += (strlen(modulename) + 1);
    }
}


/*
 * Routine to read an optional edit number string from the command line.
 * Returns the string converted to lower case, or NULL if one is not there.
 */

static const char *
geteditstring(void)
{
    const char      *str;

    switch (arglex_token)
    {
    default:
        str = 0;
        break;

    case arglex_token_number:
        {
            char buffer[20];
            snprintf(buffer, sizeof(buffer), "%ld", arglex_value.alv_number);
            str = explain_strdup_or_die(buffer);
            arglex();
        }
        break;

    case arglex_token_string:
        str = arglex_value.alv_string;
        arglex();
        break;
    }
    return str;
}


/*
 * Routine to read an optional non-negative number from the command line.
 * Returns the specified default value if no number is present.
 */

static long
getvalue(long value, int name)
{
    long            result;

    if (arglex_token != arglex_token_number)
        result = value;
    else
    {
        result = arglex_value.alv_number;
        arglex();
        if (result < 0)
        {
            sub_context_ty  *scp;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "Name", arglex_token_name(name));
            sub_var_set_long(scp, "Number", result);
            sub_var_optional(scp, "Number");
            fatal_intl
            (
                scp,
                i18n("the $name option needs a non-negative argument")
            );
            /* NOTREACHED */
            sub_context_delete(scp);
        }
    }
    return result;
}


static void
mkdir_minus_p(const char *path)
{
    char            *slash;

    if (mkdir(path, 0777) >= 0)
        return;
    if (errno != ENOENT)
        return;
    slash = strrchr(path, '/');
    if (slash && slash > path)
    {
        string_ty       *tmp;

        tmp = str_n_from_c(path, slash - path);
        mkdir_minus_p(tmp->str_text);
        str_free(tmp);
    }

    /*
     * Ignore any error codes coming back.  (The commonest will be
     * EEXISTS, which we won't be complaining about anyway.)  When we
     * try to access the directory contents, some other fatal error
     * will occur, so the user foinds out anyway.
     */
    mkdir(path, 0777);
}


int
main(int argc, char **argv)
{
    const char      *outputname;        /* output name for extraction */
    const char      *inputname;         /* input name for update */
    const char      *editname;          /* name for this edit */
    const char      *editstring;        /* edit number string */
    const char      *editstring2;       /* second edit number string */
    int             modulecount;        /* number of modules */
    int             curmod;             /* current module number */
    int             action;             /* action to be performed */
    int             allflag;            /* 1 if want to do all files */
    const char      *modules[MAXMODULES]; /* list of modules */
    int             make_path;

    arglex_init(argc, argv, argtab);
    switch (arglex())
    {
    case arglex_token_help:
        main_help();
        quit(0);

    case arglex_token_version:
        version();
        quit(0);

    default:
        break;
    }
    modifyline_register(modifyline);
    fc.debugflag = 0;
    fc.quickflag = 0;
    fc.verbosity = VERBOSE_DEFAULT;
    fc.binary = 0;
    sc.nowriteflag = 0;
    sc.forcewriteflag = 0;
    sc.forceupdateflag = 0;
    sc.modifylines = MODIFY_LINES;
    sc.firstedit = 0;
    sc.lastedit = 0;
    sc.tablepos = 0;
    sc.dirname = DEFAULTPATH;
    sc.modulename = NULL;
    make_path = 0;
    allflag = 0;
    modulecount = 0;
    action = 0;
    outputname = NULL;
    inputname = NULL;
    editname = NULL;
    editstring = NULL;
    editstring2 = NULL;
    sc.remarkname = NULL;
    sc.remark_string = NULL;

    while (arglex_token != arglex_token_eoln)
    {
        switch (arglex_token)
        {
        default:
            bad_argument(usage);
            /* NOTREACHED */

        case arglex_token_number:
            /*
             * Modules names can look like numbers.
             * Fall through...
             */

        case arglex_token_string:
            if (modulecount >= MAXMODULES)
            {
                fatal_intl(0, i18n("too many modules specified"));
                /* NOTREACHED */
            }
            modules[modulecount++] = arglex_value.alv_string;
            break;

        case arglex_token_difference:
            /* generate differences */
            action |= A_DIFF;
            arglex();
            editstring = geteditstring();
            editstring2 = geteditstring();
            continue;

        case arglex_token_extract:
            /* extract edit */
            action |= A_EXTRACT;
            arglex();
            editstring = geteditstring();
            continue;

        case arglex_token_input:
            /* input file */
            if (arglex() != arglex_token_string)
            {
                error_intl(0, i18n("no input file supplied"));
                usage();
            }
            inputname = expand(arglex_value.alv_string);
            break;

        case arglex_token_list:
            /* list edit */
            action |= A_LIST;
            arglex();
            editstring = geteditstring();
            editstring2 = geteditstring();
            continue;

        case arglex_token_modified:
            /* set number of modified lines */
            arglex();
            sc.modifylines =
                getvalue((long)MODIFY_LINES, arglex_token_modified);
            continue;

        case arglex_token_output:
            /* output file */
            if (arglex() != arglex_token_string)
            {
                error_intl(0, i18n("no output file supplied"));
                usage();
            }
            outputname = expand(arglex_value.alv_string);
            break;

        case arglex_token_make_path:
            make_path = 1;
            break;

        case arglex_token_path:
            /* directory path for edit histories */
            if (arglex() != arglex_token_string)
            {
                error_intl(0, i18n("no directory path supplied"));
                usage();
            }
            sc.dirname = expand(arglex_value.alv_string);
            break;

        case arglex_token_quick:
            /* be quick on comparison or list output */
            fc.quickflag = 1;
            break;

        case arglex_token_remark:
            /* remark file */
            if (arglex() != arglex_token_string)
            {
                sc.remarkname = "";
                continue;
            }
            sc.remarkname = expand(arglex_value.alv_string);
            break;

        case arglex_token_remark_string:
            /* remark string */
            if (arglex() != arglex_token_string)
            {
                sc.remark_string = NULL;
                continue;
            }
            sc.remark_string = arglex_value.alv_string;
            break;

        case arglex_token_terminal:
            /* type edit to terminal */
            action |= A_TYPE;
            arglex();
            editstring = geteditstring();
            continue;

        case arglex_token_update:
            /* update history file */
            action |= A_UPDATE;
            break;

        case arglex_token_verbose:
            /* be verbose */
            arglex();
            fc.verbosity = getvalue((long)VERBOSE_FULL, arglex_token_verbose);
            continue;

        case arglex_token_what:
            /* descripe what changes are */
            fc.whatflag = 1;
            break;

        case arglex_token_debug:
            /* debugging enabled */
            fc.debugflag = 1;
            break;

        case arglex_token_create:
            /* create new module */
            action |= A_CREATE;
            break;

        case arglex_token_difference_update:
            /* do difference and update */
            action |= A_DIFF | A_UPDATE;
            break;

        case arglex_token_conditional_update:
            /* update if differences */
            action |= A_CONDUPDATE;
            break;

        case arglex_token_clean:
            /* clean up to date file */
            action |= A_CLEAN;
            break;

        case arglex_token_check:
            /* see if file is up to date */
            action |= A_CHECK;
            break;

        case arglex_token_forced_update:
            /* force update */
            sc.forceupdateflag = 1;
            break;

        case arglex_token_force_writing:
            /* force writing */
            sc.forcewriteflag = 1;
            break;

        case arglex_token_all:
            /* do all files */
            allflag = 1;
            break;

        case arglex_token_nname:
            /* set edit name */
            if (arglex() != arglex_token_string)
            {
                error_intl(0, i18n("no edit name supplied"));
                usage();
            }
            editname = arglex_value.alv_string;
            checkeditname(editname);
            break;

        case arglex_token_no_write:
            /* don't overwrite */
            sc.nowriteflag = 1;
            break;

        case arglex_token_prune:
            /* prune old edits */
            action |= A_PRUNE;
            arglex();
            editstring = geteditstring();
            continue;

        case arglex_token_binary:
            fc.binary = 1;
            break;

        case arglex_token_keywords_not:
            sc.modifylines = 0;
            break;

        case arglex_token_trace:
            for (;;)
            {
#ifdef DEBUG
                trace_enable(arglex_value.alv_string);
#endif
                if (arglex() != arglex_token_string)
                    break;
            }
#ifndef DEBUG
            error_intl(0, i18n("-TRace needs DEBUG"));
#endif
            continue;
        }
        arglex();
    }
    if (action == 0)
        action = (editname ? A_NAME : A_EXTRACT);
    if
    (
        action != (A_CREATE | A_CONDUPDATE)
    &&
        action != (A_CREATE | A_UPDATE)
    &&
        action != (A_DIFF | A_UPDATE)
    &&
        action != (action & -action)
    )
        fatal_intl(0, i18n("multiple actions specified"));
    if (sc.remarkname && ((action & REMARK_ACTIONS) == 0))
        fatal_intl(0, i18n("cannot use -Remark with specified action"));
    if (sc.remark_string && ((action & REMARK_ACTIONS) == 0))
        fatal_intl(0, i18n("cannot use -Remark_String with specified action"));
    if (inputname && ((action & INPUT_ACTIONS) == 0))
        fatal_intl(0, i18n("cannot use -Input with specified action"));
    if (outputname && ((action & OUTPUT_ACTIONS) == 0))
        fatal_intl(0, i18n("cannot use -Output with specified action"));
    if (editname && ((action & NAME_ACTIONS) == 0))
        fatal_intl(0, i18n("cannot use -Name with specified action"));
    if (sc.forcewriteflag && sc.nowriteflag)
        fatal_intl(0, i18n("cannot specify both -Force_Write and -No_Write"));
    if (fc.quickflag && fc.whatflag)
        fatal_intl(0, i18n("cannot specify both -What and -Quick"));
    if ((modulecount <= 0) && (action == A_CHECK))
        allflag = 1;
    if ((modulecount <= 0) && !allflag)
        fatal_intl(0, i18n("no module name specified"));
    if (allflag)
    {
        if (modulecount)
            fatal_intl(0, i18n("cannot specify module names with -ALL"));
        if (inputname && !isdir(inputname))
            fatal_intl(0, i18n("input name must be a directory for -ALL"));
        if (outputname && !isdir(outputname))
            fatal_intl(0, i18n("output name must be a directory for -ALL"));
    }
    if (modulecount > 1)
    {
        if (inputname && !isdir(inputname))
        {
            fatal_intl
            (
                0,
                i18n("input name must be a directory for multiple modules")
            );
        }
        if (outputname && !isdir(outputname))
        {
            fatal_intl
            (
                0,
                i18n("output name must be a directory for multiple modules")
            );
        }
    }

    if (make_path)
        mkdir_minus_p(sc.dirname);
    explain_access_or_die(sc.dirname, R_OK);

    if (allflag)
    {
        doallmodules
        (
            action,
            inputname,
            outputname,
            editstring,
            editstring2,
            editname
        );
        quit(0);
    }
    for (curmod = 0; curmod < modulecount; curmod++)
    {
        domodule
        (
            action,
            modules[curmod],
            inputname,
            outputname,
            editname,
            editstring,
            editstring2,
            (modulecount > 1)
        );
    }
    quit(0);
    return 0;
}


/* vim: set ts=8 sw=4 et : */
