/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1994, 1998-2002, 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/>.
 */

#include <common/ac/libintl.h>
#include <common/ac/stdio.h>
#include <common/ac/string.h>
#include <common/ac/unistd.h>
#include <libexplain/fclose.h>

#include <common/cmalloc.h>
#include <common/compare.h>
#include <common/error_intl.h>
#include <common/fcheck.h>
#include <common/fileio.h>
#include <common/output/file.h>
#include <common/output/quotprindeco.h>
#include <common/output/stdout.h>

#include <fhist/extract.h>
#include <fhist/fhist.h>
#include <fhist/modlin.h>
#include <fhist/subroutine.h>


/*
 * States for examining history file.
 */
#define S_OUTSIDE 0 /* outside of an edit */
#define S_INSIDE  1 /* just inside of an edit */
#define S_CHANGE  2 /* saw changes in an edit */
#define S_FILE    3 /* saw file reference in an edit */


/*
 * In-memory edit history structure.
 */
typedef struct edit EDIT;
struct edit
{
    EDIT            *e_next;       /* next edit history structure in list */
    EDIT            *e_prev;       /* previous edit history structure in list */
    long            e_count;       /* line count */
    char            *e_text;       /* text line if any */
    long            e_textlen;     /* length of text line */
};


/*
 * Local storage.
 */
static EDIT     *editfreelist;  /* free list for edit structures */
static EDIT     *editnewbegin;  /* pointer to chunk of new edit structures */
static EDIT     *editnewend;    /* pointer to end of new edit structures */
static EDIT     edithead;       /* head of edit list */
static EDIT     edittail;       /* tail of edit list */
static EDIT     newedithead;    /* header of new edit list */
static EDIT     newedittail;    /* tail of new edit list */
static long     curline;        /* current line number */
static char     fromsource;     /* dummy addr to indicate source file lines */


/*
 * Initialize an edit list.
 */

static void
initlist(EDIT *hp, EDIT *tp)
{
    hp->e_count = 0;
    hp->e_textlen = 0;
    hp->e_text = NULL;
    hp->e_prev = NULL;
    hp->e_next = tp;
    tp->e_count = 0x40000000; /* large count of lines */
    tp->e_text = &fromsource; /* all from the source file */
    tp->e_textlen = 0;
    tp->e_next = NULL;
    tp->e_prev = hp;
    curline = 1;
}


/*
 * Allocate a new edit structure and insert it in front of the
 * specified edit structure in a list.
 */

static EDIT *
allocedit(EDIT *ep)
{
    EDIT            *np;            /* new edit structure */

    np = editfreelist;
    if (np)
        editfreelist = np->e_next;
    else
    {
        if (editnewbegin == editnewend)
        {
            np = (EDIT *) cm_alloc_and_check(sizeof(EDIT) * EDITALLOCSIZE);
            editnewbegin = np;
            editnewend = np + EDITALLOCSIZE;
        }
        np = editnewbegin++;
    }
    np->e_text = NULL;
    np->e_textlen = 0;
    np->e_count = 0;
    np->e_next = ep;
    np->e_prev = ep->e_prev;
    np->e_prev->e_next = np;
    ep->e_prev = np;
    return np;
}


/*
 * Append a change to the new edit list.
 */

static void
addtolist(FILE *fp, long line, long insertcount, long deletecount)
{
    EDIT            *ep;            /* pointer to current element structure */

    if (curline < line)
    {
        /* edit for reading data from source */
        ep = allocedit(&newedittail);
        ep->e_text = &fromsource;
        ep->e_count = line - curline;
        curline = line;
    }
    if (deletecount > 0)
    {
        /* edit for deleting from source */
        ep = allocedit(&newedittail);
        ep->e_count = deletecount;
        curline += deletecount;
    }
    while (--insertcount >= 0)
    {
        /* edits for inserting new text */
        ep = allocedit(&newedittail);
        ep->e_text = get_a_line(fp, NOSEEK, T_TEXT, sc.historyname);
        ep->e_textlen = sc.linelen;
        ep->e_count = 1;
    }
}


static void
printone(const char *str, EDIT *ep)
{
    const char      *blab;

    blab = "insert";
    if (ep->e_text == NULL)
        blab = "delete";
    if (ep->e_text == &fromsource)
        blab = "source";
    fprintf
    (
        stderr,
        "  %s at %08lX: %s %ld\n",
        str,
        (unsigned long)ep,
        blab,
        ep->e_count
    );
}


/*
 * Print out an edit list
 */

static void
printlist(const char *msg, EDIT *ep)
{
    long            line;
    const char      *blab;

    fprintf(stderr, "%s\n", msg);
    line = 1;
    while (ep)
    {
        blab = "insert";
        if (ep->e_text == NULL)
            blab = "delete";
        if (ep->e_text == &fromsource)
            blab = "source";
        fprintf
        (
            stderr,
            "edit at %08lX: line %ld: %s %ld\n",
            (unsigned long)ep,
            line,
            blab,
            ep->e_count
        );
        if ((ep->e_text == NULL) || (ep->e_text == &fromsource))
            line += ep->e_count;
        ep = ep->e_next;
    }
}


/*
 * Remove an edit from an edit list and put it onto the free list.
 * Returns the next edit structure on the list.
 */

static EDIT *
remedit(EDIT *ep)
{
    EDIT            *np;            /* next edit structure in list */

    np = ep->e_prev;
    np->e_next = ep->e_next;
    np = ep->e_next;
    np->e_prev = ep->e_prev;
    ep->e_next = editfreelist;
    editfreelist = ep;
    return np;
}


/*
 * Routine to merge the current and new edit lists together to form
 * the new current edit list containing both changes.  This works by
 * conceptually walking through each 'line' and determining the result
 * of interactions of deleting, inserting, and copying from the source.
 */

static void
mergelists(void)
{
    EDIT            *op;            /* pointer to current old edit structure */
    EDIT            *np;            /* pointer to current new edit structure */
    long            count;          /* number of lines in common */
    int             selectvalue;    /* select value */

    op = edithead.e_next;
    np = newedithead.e_next;
    if (fc.debugflag)
    {
        printlist("old list", op);
        printlist("new list", np);
        fprintf(stderr, "MERGES:\n");
    }

    while (np != &newedittail)
    {
        count = np->e_count;
        if (op->e_count < count)
            count = op->e_count;
        selectvalue = 0;
        if (np->e_text == NULL)
            selectvalue = 1;
        if (np->e_text == &fromsource)
            selectvalue = 2;
        if (op->e_text == NULL)
            selectvalue += 3;
        if (op->e_text == &fromsource)
            selectvalue += 6;
        if (fc.debugflag)
        {
            fprintf(stderr, "case %d:\n", selectvalue);
            printone("op:", op);
            printone("np:", np);
        }

        switch (selectvalue)
        {
        case 0: /* old inserts and new inserts */
        case 2: /* old inserts and new from source */
            if (fc.debugflag)
                printone("out:", op);
            op = op->e_next;
            break;

        case 1: /* old inserts and new deletes */
        case 4: /* old and new both delete */
        case 7: /* old from source and new deletes */
            op = allocedit(op);
            op->e_count = np->e_count;
            if (fc.debugflag)
                printone("out:", op);
            op = op->e_next;
            np = np->e_next;
            break;

        case 3: /* old deletes and new inserts */
            np->e_count -= count;
            if (np->e_count <= 0)
            {
                cm_free(np->e_text - 2); /* HACK */
                np = np->e_next;
            }
            op->e_count--;
            if (op->e_count <= 0)
                op = remedit(op);
            break;

        case 5: /* old deletes and new from source */
            np->e_count -= count;
            if (np->e_count <= 0)
                np = np->e_next;
            if (op->e_count == count)
            {
                if (fc.debugflag)
                    printone("out:", op);
                op = op->e_next;
                break;
            }
            op = allocedit(op);
            op->e_count = count;
            if (fc.debugflag)
                printone("out:", op);
            op = op->e_next;
            op->e_count -= count;
            break;

        case 6: /* old from source and new inserts */
            op = allocedit(op);
            op->e_text = np->e_text;
            op->e_textlen = np->e_textlen;
            op->e_count = 1;
            if (fc.debugflag)
                printone("out:", op);
            op = op->e_next;
            np = np->e_next;
            if (op == &edittail)
                break;
            op->e_count--;
            if (op->e_count <= 0)
                op = remedit(op);
            break;

        case 8: /* old and new both from source */
            np->e_count -= count;
            if (np->e_count <= 0)
                np = np->e_next;
            if (op->e_count <= count)
            {
                if (op != &edittail)
                {
                    if (fc.debugflag)
                        printone("out:", op);
                    op = op->e_next;
                }
                break;
            }
            op = allocedit(op);
            op->e_text = &fromsource;
            op->e_count = count;
            if (fc.debugflag)
                printone("out:", op);
            op = op->e_next;
            if (op != &edittail)
                op->e_count -= count;
            break;
        }
        if (fc.debugflag)
            fprintf(stderr, "\n");
    }
    if (fc.debugflag)
    {
        printlist("Updated old list", edithead.e_next);
        fprintf(stderr, "\n");
    }
}


/*
 * Routine to collect edit scripts from the log file for a given edit.
 * This builds an in-memory edit list which can then be used to generate
 * the specified version of the source file.  The history file has already
 * been opened, the edit number checked, and the first line of the desired
 * edit sequence read and checked.
 */

static void
readedits(FILE *fp, long editnumber)
{
    char            *cp;            /* line of data */
    long            filenumber;     /* file number containing whole edit */
    long            linenumber;
    long            insertcount;
    long            deletecount;
    long            inscc;
    long            delcc;
    long            linecc;
    long            temp;
    short           more;
    short           state;          /* state of current edit */
    int             bin;            /* IGNORED */

    insertcount = 0;
    deletecount = 0;
    state = S_INSIDE;
    more = 1;
    linenumber = 1;
    initlist(&edithead, &edittail);
    initlist(&newedithead, &newedittail);

    /*
     * Here for each line of the edit history file.
     * Switch on the type of each line, verifying that the
     * sequence of types is legitimate.  Convert each change
     * line into one or more edit structures.  After each
     * set of changes for an edit has been read, then merge
     * together the old and new edit lists to form the final
     * edit list.
     */
    while (more)
    {
        cp = readlinef(fp, (long *)NULL, 0, sc.historyname, &bin);
        if (cp == NULL)
            fatal_intl(0, i18n("error reading edit"));
        switch (*cp++)
        {

        case T_BEGINEDIT: /* begin another edit */
            cp = getnumber(cp, &temp);
            if (cp == NULL)
                fatal_intl(0, i18n("no number in begin edit"));
            if (state != S_OUTSIDE)
            {
                sub_context_ty *scp;

                scp = sub_context_new();
                sub_var_set_long(scp, "Number", temp);
                fatal_intl
                (
                    scp,
                    i18n("beginning edit $number without ending previous one")
                );
            }
            state = S_INSIDE;
            if (temp != editnumber + 1)
                fatal_intl(0, i18n("non-consecutive begin edit"));
            editnumber = temp;
            insertcount = 0;
            deletecount = 0;
            linenumber = 1;
            initlist(&newedithead, &newedittail);
            break;

        case T_CHANGE: /* change some lines */
            if (state == S_OUTSIDE)
                fatal_intl(0, i18n("change line outside of edit"));
            if ((state != S_INSIDE) && (state != S_CHANGE))
                fatal_intl(0, i18n("change line not expected in edit"));
            state = S_CHANGE;
            cp = getnumber(cp, &linecc);
            if (cp == NULL)
                fatal_intl(0, i18n("missing line number in change line"));
            if (linecc < linenumber)
                fatal_intl(0, i18n("decreasing line number in change line"));
            cp = getnumber(cp, &inscc);
            cp = getnumber(cp, &delcc);
            if (cp == NULL)
                fatal_intl(0, i18n("bad line counts in change line"));
            if ((inscc == 0) && (delcc == 0))
                fatal_intl(0, i18n("change line does nothing"));
            addtolist(fp, linecc, inscc, delcc);
            insertcount += inscc;
            deletecount += delcc;
            if (delcc == 0)
                delcc = 1;
            linenumber = linecc + delcc;
            break;

        case T_TEXT: /* text line */
            if (state != S_OUTSIDE)
                fatal_intl(0, i18n("text line not within edit"));
            fatal_intl(0, i18n("unexpected text line"));
            break;

        case T_FILE: /* get results from explicit file */
            if (state == S_OUTSIDE)
                fatal_intl(0, i18n("file line not inside edit"));
            if (state != S_INSIDE)
                fatal_intl(0, i18n("file line not expected in edit"));
            state = S_FILE;
            cp = getnumber(cp, &filenumber);
            if (cp == NULL)
                fatal_intl(0, i18n("missing file number in file line"));
            /* NEEDS MORE WORK */
            break;

        case T_ENDEDIT: /* end of current edit */
            if (state == S_OUTSIDE)
                fatal_intl(0, i18n("ending edit not yet started"));
            state = S_OUTSIDE;
            cp = getnumber(cp, &temp);
            if (cp == NULL)
                fatal_intl(0, i18n("no edit number at end of edit"));
            if (temp != editnumber)
            {
                sub_context_ty *scp;

                scp = sub_context_new();
                sub_var_set_long(scp, "Number", editnumber);
                fatal_intl(scp, i18n("end of edit $number not correct number"));
            }
            cp = getnumber(cp, &inscc);
            cp = getnumber(cp, &delcc);
            if (cp == NULL)
                fatal_intl(0, i18n("bad total counts in end of edit line"));
            if (inscc != insertcount)
                fatal_intl(0, i18n("wrong insert count at end of edit"));
            if (delcc != deletecount)
                fatal_intl(0, i18n("wrong delete count at end of edit"));
            more = (editnumber != sc.lastedit);
            mergelists();
            break;

        case T_REMARK: /* remark line */
            if (state == S_OUTSIDE)
                fatal_intl(0, i18n("remark line outside of edit"));
            break;

        case T_EOF:
            fatal_intl(0, i18n("unexpected end of file line"));

        default:
            fatal_intl(0, i18n("unexpected line in edit"));
        }
    }
    explain_fclose_or_die(fp);
}


/*
 * Write out the extracted file using the information in the edit list
 * and the current source file.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

static void
writefile(const char *outputname, INFO *info)
{
    FILE            *sf;            /* source file */
    output_ty       *of;            /* output file */
    EDIT            *ep;            /* current edit */
    long            line;           /* current line number in source file */
    long            count;
    char            *cp;
    int             bin;

    line = 0;
    sf = opensourcefile();
    if (fc.binary)
    {
        of = output_file_binary_open(outputname);
        of = output_quoted_printable_decode(of, 1);
    }
    else
        of = output_file_text_open(outputname);

    /*
     * Loop over each of the edit structures applying them to the
     * source file to produce the output file.
     */
    for (ep = edithead.e_next; ep; ep = ep->e_next)
    {
        count = ep->e_count;

        /*
         * If we are deleting lines from the source file,
         * THEN just skip the required number of lines.
         */
        if (ep->e_text == NULL)
        {
            skipf(sf, count);
            continue;
        }

        /*
         * If we are inserting lines from the history file,
         * then insert them from the edit structure.
         */
        if (ep->e_text != &fromsource)
        {
            if (count != 1)
            {
                fatal_intl
                (
                    0,
                    i18n("edit command for text has bad count value")
                );
            }
            cp = ep->e_text;
            sc.linelen = ep->e_textlen;
            if (++line <= sc.modifylines)
                cp = modifyline(cp, &sc.linelen, info);
            output_write(of, cp, sc.linelen);
            continue;
        }

        /*
         * The final choice is preserving lines from the source file,
         * so copy the lines from the source file.
         */
        while (count-- > 0)
        {
            bin = 0;
            cp = readlinef(sf, &sc.linelen, 0, sc.sourcename, &bin);
            if (cp == NULL)
            {
                if (ep != &edittail)
                    fatal_intl(0, i18n("premature end of file"));
                output_delete(of);
                return;
            }
            if (bin)
            {
                /*
                 * Because all should be encoded using
                 * quoted printable.
                 */
                binary_fatal(sc.sourcename);
            }
            if (++line <= sc.modifylines)
                cp = modifyline(cp, &sc.linelen, info);
            output_write(of, cp, sc.linelen);
        }
    }
    fatal_intl(0, i18n("unexpected end of edit list"));
}


/*
 * Copy the data from one file to another.
 * This is used when extracting the latest version of a module.
 *
 * The history (.e) file is binary, because we need to seek in it.
 * The source (.s) file is text, because we don't need to seek in it.
 * The input files are text, by definition.
 * The output files are text, by definition.
 */

static void
copyfile(const char *outputname, INFO *info)
{
    FILE            *sf;            /* source file for copying */
    output_ty       *of;            /* output file for copying */
    long            linelen;        /* length of line */
    long            line;           /* current line number */
    char            *cp;            /* current line of file */
    int             bin;            /* IGNORED */

    sf = opensourcefile();
    if (fc.binary)
    {
        of = output_file_binary_open(outputname);
        of = output_quoted_printable_decode(of, 1);
    }
    else
        of = output_file_text_open(outputname);
    for (line = 0;; ++line)
    {
        cp = readlinef(sf, &linelen, 0, sc.sourcename, &bin);
        if (cp == NULL)
            break;
        if (line < sc.modifylines)
            cp = modifyline(cp, &linelen, info);
        output_write(of, cp, linelen);
    }
    output_delete(of);
    explain_fclose_or_die(sf);
}


/*
 * Extract a revision of a module using the edit history.
 */

void
extracthistory(const char *editname, const char *outputname, int blabflag)
{
    char            *str;           /* string being manipulated */
    long            editnumber;     /* edit number being extracted */
    long            i;              /* temporary value */
    INFO            info;           /* information about edit */
    char            infoline[MAX_INFO]; /* first remark line containing info */
    char            editstr[16];    /* edit number string */
    FILE            *fp;

    fp = openhistoryfile(OHF_READ);
    editnumber = findeditnumber(fp, editname);

    /*
     * If there is an output file, then check to see if we are overwriting
     * an old file, and let the user know what is being extracted.
     */
    if (outputname)
    {
        if (sc.nowriteflag && (access(outputname, 0) == 0))
        {
            if (fc.verbosity)
            {
                sub_context_ty *scp;

                scp = sub_context_new();
                sub_var_set_charstar(scp, "File_Name", outputname);
                sub_var_set_charstar(scp, "Module", sc.modulename);
                error_intl
                (
                    scp,
                    i18n
                    (
                        "existing file \"$filename\" not overwritten by "
                        "module \"$module\""
                    )
                );
                sub_context_delete(scp);
            }
            explain_fclose_or_die(fp);
            return;
        }
        if (fc.verbosity || blabflag)
        {
            sub_context_ty *scp;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "Module", sc.modulename);
            sub_var_set_long(scp, "Number", editnumber);
            sub_var_set_charstar(scp, "File_Name", outputname);
            if (strcmp(outputname, sc.modulename))
            {
                if (editnumber == sc.firstedit)
                {
                    error_intl
                    (
                        scp,
                        i18n
                        (
                            "extracting initial edit $number of module "
                            "\"$module\" to file \"$filename\""
                        )
                    );
                }
                else if (editnumber == sc.lastedit)
                {
                    error_intl
                    (
                        scp,
                        i18n
                        (
                            "extracting latest edit $number of module "
                            "\"$module\" to file \"$filename\""
                        )
                    );
                }
                else
                {
                    error_intl
                    (
                        scp,
                        i18n
                        (
                            "extracting edit $number of module "
                            "\"$module\" to file \"$filename\""
                        )
                    );
                }
            }
            else
            {
                sub_var_optional(scp, "File_Name");
                if (editnumber == sc.firstedit)
                {
                    error_intl
                    (
                        scp,
                        i18n
                        (
                            "extracting initial edit $number of module "
                            "\"$module\""
                        )
                    );
                }
                else if (editnumber == sc.lastedit)
                {
                    error_intl
                    (
                        scp,
                        i18n
                        (
                            "extracting latest edit $number of module "
                            "\"$module\""
                        )
                    );
                }
                else
                {
                    error_intl
                    (
                        scp,
                        i18n("extracting edit $number of module \"$module\"")
                    );
                }
            }
            sub_context_delete(scp);
        }
    }
    if (checknewfile(outputname))
    {
        explain_fclose_or_die(fp);
        return;
    }
    startedit(fp, editnumber, infoline);

    /*
     * Fill in the information structure for line modifying to use.
     */
    str = infoline;
    while (*str == ' ')
        str++;
    info.i_user = str;
    while (*str && (*str != ' '))
        str++;
    *str++ = '\0';
    while (*str == ' ')
        str++;

    /*
     * Parse and modify the date:
     * Incoming:  Sun Sep 16 01:03:52 1985\n
     * Outgoing:  16-Sep-85
     */
    str[1] = str[8];
    str[2] = str[9];
    str[3] = '-';
    str[7] = '-';
    str[8] = str[22];
    str[9] = str[23];
    str[10] = '\0';
    if (*++str == ' ')
        str++;
    info.i_date = str;
    str = &editstr[15];
    *str = '\0';
    i = editnumber;
    do
    {
        *(--str) = (i % 10) + '0';
        i /= 10;
    }
    while (i);
    info.i_edit = str;

    /*
     * If we are extracting the latest edit, then just copy it from
     * the source file.  Otherwise, do the work of applying edits
     * from the history file.
     */
    if (editnumber == sc.lastedit)
    {
        explain_fclose_or_die(fp);
        copyfile(outputname, &info);
        return;
    }
    readedits(fp, editnumber);
    writefile(outputname, &info);
}


/*
 * Reset editing data for new file.
 */

void
editreset(void)
{
    editfreelist = NULL;
    editnewbegin = NULL;
    editnewend = NULL;
}


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