/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1994, 1998-2000, 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/errno.h>
#include <common/ac/stdio.h>
#include <common/ac/stdlib.h>
#include <common/ac/string.h>
#include <common/ac/unistd.h>
#include <libexplain/fclose.h>
#include <libexplain/output.h>
#include <libexplain/fopen.h>
#include <libexplain/unlink.h>

#include <common/cmalloc.h>
#include <common/compare.h>
#include <common/error.h>
#include <common/error_intl.h>
#include <common/fcheck.h>
#include <common/fileio.h>
#include <common/str.h>

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


/*
 * Compare two different versions of a module.
 * Count specifies how many edit numbers were supplied to the command.
 * If zero, then we compare inputname to the latest edit.  If one, then
 * we compare inputname to the specified edit number.  If two, then we
 * compare the specified edit numbers.  If matchblab is 1, then we
 * output a message even if the files are identical.
 */
void
diffhistory(const char *inputname, const char *outputname,
    const char *editname1, const char *editname2, int matchblab)
{
    long            edit1;          /* first edit number */
    long            edit2;          /* second edit number */
    const char      *temp1;
    const char      *temp2;         /* temporary file names being compared */
    int             pid;            /* process id */
    short           savedforceflag; /* saved force flag */
    string_ty       *namebuf1;      /* first temporary name */
    string_ty       *namebuf2;      /* second temporary name */
    string_ty       *numbuf;        /* buffer for number string */
    FILE            *fp;

    fc.maxchanges = INFINITY;
    fc.fileAmodify = sc.modifylines;
    fc.fileBmodify = sc.modifylines;
    pid = getpid();
    temp1 = getenv("TMPDIR");
    temp2 = PATH_STR;
    if ((temp1 == NULL) || (*temp1 == '\0'))
    {
        temp1 = "";
        temp2 = temp1;
    }
    fp = openhistoryfile(OHF_READ);
    assert(fp);
    if (editname1 == NULL)
    {
        /* compare with latest version */
        explain_fclose_or_die(fp);
        fp = 0;
        if (fc.verbosity > VERBOSE_DEFAULT)
        {
            explain_output_error
            (
                "[Comparing files \"%s\" and \"%s\"]",
                sc.sourcename,
                inputname
            );
        }
        fc.fileAskip = 1;
        fcomp(sc.sourcename, inputname);
        if (fc.whatflag)
            dumpwhat(outputname);
        else if (matchblab || fc.inserts || fc.deletes)
            dumpnormal(outputname);
        return;
    }
    namebuf1 = str_format("%s%sT$1_%d", temp1, temp2, pid);
    namebuf2 = str_format("%s%sT$2_%d", temp1, temp2, pid);

    /*
     * Here when we have to retrieve a previous version.
     */
    edit2 = 0;
    temp1 = NULL;
    temp2 = NULL;
    savedforceflag = sc.forcewriteflag;
    sc.forcewriteflag = 1;
    edit1 = findeditnumber(fp, editname1);
    if (edit1 != sc.lastedit)
        temp1 = namebuf1->str_text;
    if (editname2)
    {
        edit2 = findeditnumber(fp, editname2);
        if (edit2 != sc.lastedit)
            temp2 = namebuf2->str_text;
    }
    explain_fclose_or_die(fp);
    fp = 0;
    if (temp1)
    {
        numbuf = str_format("%ld", edit1);
        extracthistory(numbuf->str_text, temp1, VERBOSE_NONE);
        str_free(numbuf);
    }
    else
        fc.fileAskip = 1;

    if (editname2 == NULL)
    {
        sc.forcewriteflag = savedforceflag;
        fcomp(temp1 ? temp1 : sc.sourcename, inputname);
        if (temp1)
        {
            if (fc.verbosity > VERBOSE_DEFAULT)
                explain_output_error("[Deleting temporary file \"%s\"]", temp1);
            explain_unlink_or_die(temp1);
        }
        if (fc.whatflag)
            dumpwhat(outputname);
        else if (matchblab || fc.inserts || fc.deletes)
            dumpnormal(outputname);
        str_free(namebuf1);
        str_free(namebuf2);
        return;
    }

    /*
     * Here if we have to compare against another edit
     */
    if (temp2)
    {
        numbuf = str_format("%ld", edit2);
        extracthistory(numbuf->str_text, temp2, VERBOSE_NONE);
        str_free(numbuf);
    }
    else
        fc.fileBskip = 1;

    sc.forcewriteflag = savedforceflag;
    fcomp(temp1 ? temp1 : sc.sourcename, temp2 ? temp2 : sc.sourcename);
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        if (temp1 && temp2)
        {
            explain_output_error
            (
                "[Deleting temporary files \"%s\" and \"%s\"]",
                temp1,
                temp2
            );
        }
        else if (temp1 || temp2)
        {
            explain_output_error
            (
                "[Deleting temporary file \"%s\"]",
                (temp1 ? temp1 : temp2)
            );
        }
    }
    if (temp1)
        unlink(temp1);
    if (temp2)
        unlink(temp2);
    if (fc.whatflag)
        dumpwhat(outputname);
    else if (matchblab || fc.inserts || fc.deletes)
        dumpnormal(outputname);
    str_free(namebuf1);
    str_free(namebuf2);
}


/*
 * Routine to do a quick comparison of two files.
 * Returns nonzero if the two files are different.
 *
 * 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 int
quickcomp(const char *nameA, const char *nameB, long skipA)
{
    FILE            *fpA;           /* first file */
    FILE            *fpB;           /* second file */
    char            *cp;            /* current temporary line */
    char            *cpA;           /* current line for file A */
    char            *cpB;           /* current line for file B */
    long            linenumber;     /* current line number */
    long            cplen;          /* length of current temporary line */
    long            linelenA;       /* length of first file line */
    long            linelenB;       /* length of second file line */
    int             bin_a;
    int             bin_b;

    fpA = explain_fopen_or_die(nameA, "r");
    fpB = explain_fopen_or_die(nameB, "r");
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        explain_output_error
        (
            "[Comparing files \"%s\" and \"%s\"]",
            nameA,
            nameB
        );
    }
    cplen = 132;
    cp = cm_alloc_and_check(cplen + 1);
    if (skipA > 0)
        skipf(fpA, skipA);
    linenumber = 0;
    bin_a = 0;
    bin_b = 0;
    for (;;)
    {
        cpA = readlinef(fpA, &linelenA, 0, nameA, &bin_a);
        if (cpA == NULL)
        {
            cpB = readlinef(fpB, &linelenB, 0, nameB, &bin_b);
            break;
        }
        if (linelenA > cplen)
        {
            cplen = linelenA;
            cp = cm_realloc_and_check(cp, cplen + 1);
        }
        strcpy(cp, cpA);
        cpA = cp;
        cpB = readlinef(fpB, &linelenB, 0, nameB, &bin_b);
        if (cpB == NULL)
            break;
        if (++linenumber <= sc.modifylines)
        {
            cpA = modifyline(cpA, &linelenA, (INFO *) NULL);
            cpB = modifyline(cpB, &linelenB, (INFO *) NULL);
        }
        if ((linelenA != linelenB) || strcmp(cpA, cpB))
            break;
    }
    if (bin_a)
        binary_warning(nameA);
    if (bin_b)
        binary_warning(nameB);
    explain_fclose_or_die(fpA);
    fpA = 0;
    explain_fclose_or_die(fpB);
    fpB = 0;
    cm_free(cp);
    return (cpA || cpB);
}


/*
 * See if a file is up to date, and if so, then delete it.
 * This is useful when cleaning up a directory.
 */

void
cleanhistory(const char *inputname)
{
    FILE            *fp;

    fp = openhistoryfile(OHF_READ);
    assert(fp);
    explain_fclose_or_die(fp);
    fp = 0;

    if (access(inputname, F_OK) < 0 && errno == ENOENT)
    {
        if (fc.verbosity > VERBOSE_DEFAULT)
        {
            sub_context_ty *scp;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "File_Name", inputname);
            sub_var_set_charstar(scp, "Module", sc.modulename);
            error_intl
            (
                scp,
                i18n("file \"$filename\" does not exist for module \"$module\"")
            );
            sub_context_delete(scp);
        }
        return;
    }
    if (quickcomp(sc.sourcename, inputname, 1L))
    {
        if (fc.verbosity > VERBOSE_DEFAULT)
        {
            sub_context_ty *scp;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "File_Name", inputname);
            sub_var_set_charstar(scp, "Module", sc.modulename);
            error_intl
            (
                scp,
                i18n
                (
                    "file \"$filename\" differs from latest edit of "
                    "module \"$module\""
                )
            );
        }
        return;
    }
    explain_unlink_or_die(inputname);
    if (fc.verbosity)
    {
        sub_context_ty *scp;

        scp = sub_context_new();
        sub_var_set_charstar(scp, "File_Name", inputname);
        error_intl(scp, i18n("deleted up to date file \"$filename\""));
    }
}


/*
 * Check to see if a file is up to date, and report it if not.
 */
void
checkhistory(const char *inputname)
{
    FILE            *fp;

    fp = openhistoryfile(OHF_READ);
    assert(fp);
    explain_fclose_or_die(fp);
    fp = 0;
    if (access(inputname, F_OK) < 0 && errno == ENOENT)
    {
        if (fc.verbosity > VERBOSE_DEFAULT)
        {
            sub_context_ty *scp;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "File_Name", inputname);
            sub_var_set_charstar(scp, "Module", sc.modulename);
            error_intl
            (
                scp,
                i18n("file \"$filename\" does not exist for module \"$module\"")
            );
            sub_context_delete(scp);
        }
        return;
    }
    if (quickcomp(sc.sourcename, inputname, 1L))
    {
        if (fc.quickflag)
            printf("%s\n", sc.modulename);
        else
        {
            sub_context_ty *scp;
            string_ty *s;

            scp = sub_context_new();
            sub_var_set_charstar(scp, "File_Name", inputname);
            sub_var_set_charstar(scp, "Module", sc.modulename);
            s =
                subst_intl
                (
                    scp,
                    i18n
                    (
                        "file \"$filename\" differs from latest edit of "
                        "module \"$module\""
                    )
                );
            sub_context_delete(scp);
            printf("%s\n", s->str_text);
            str_free(s);
        }
        return;
    }
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[File \"%s\" is up to date]", inputname);
}


/*
 * Compare a file to the newest version of a module, but output nothing.
 * Whether or not there are any differences is returned in the variables
 * inserts and deletes.  The edit list is built so that it can be
 * immediately used to perform an update.
 */
void
comphistory(const char *inputname)
{
    FILE            *fp;

    fp = openhistoryfile(OHF_READ);
    assert(fp);
    explain_fclose_or_die(fp);
    fp = 0;

    fc.maxchanges = INFINITY;
    fc.fileAmodify = sc.modifylines;
    fc.fileBmodify = sc.modifylines;
    fc.fileAskip = 1;
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        explain_output_error
        (
            "[Comparing files \"%s\" and \"%s\"]",
            sc.sourcename,
            inputname
        );
    }
    fcomp(sc.sourcename, inputname);
}


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