/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1995, 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/>.
 *
 *
 *
 * This program is based on the algorithm in:
 *      An O(ND) Difference Algorithm and Its Variations
 *      Eugene W. Myers
 *      (TR 85-6, April 10, 1985)
 *      Department of Computer Science
 *      The University of Arizona
 *      Tuscon, Arizona 85721
 *
 * Also see:
 *      A File Comparison Program
 *      Webb Miller and Eugene W. Myers
 *      Software Practice and Experience
 *      (Volume 15, No. 11, November 1985)
 *
 * Actual implementation and features by David I. Bell.
 * Enhancements and bug fixes by Peter Miller.
 */

#include <common/ac/ctype.h>
#include <common/ac/stddef.h>
#include <common/ac/stdio.h>
#include <common/ac/string.h>
#include <common/ac/libintl.h>
#include <libexplain/fclose.h>
#include <libexplain/fopen.h>
#include <libexplain/malloc.h>
#include <libexplain/output.h>
#include <libexplain/realloc.h>

#include <common/compare.h>
#include <common/cmalloc.h>
#include <common/error.h>
#include <common/error_intl.h>
#include <common/fcheck.h>
#include <common/input.h>
#include <common/input/file.h>
#include <common/input/file_text.h>
#include <common/input/quotprinenco.h>
#include <common/input/hexify.h>
#include <common/str.h>
#include <common/trace.h>


#define SNAKEALLOCSIZE (size_t)500 /* chunk size for snake allocation */
#define CHARALLOCSIZE (size_t)10240 /* allocation size for file data */
#define UNIQUEALLOCSIZE (size_t)512 /* unique handling size for allocation */
#define LINEALLOCSIZE (size_t)2000 /* number of lines to allocate */
#define HASHSIZE 2003           /* size of hash table */
#define CHANGEBLABINTERVAL 100  /* how often to talk about changes */
#define READBLABINTERVAL 1000   /* how often to talk about reading */
#define SNAKEBLABINTERVAL 1000  /* how often to talk about snakes */
#define TABSIZE 8               /* tab size */
#define DETABALLOCSIZE (size_t)128 /* buffer allocation size for detab */

static size_t   tablesize;      /* needed table size */
static size_t   snakecount;     /* number of snake structures used */
static size_t   availchars;     /* number of available characters */
static char     *nextchar;      /* next allocable character storage */
static long     *V1;            /* the row containing the last d */
static long     *V2;            /* another row */
static SNAKE    *nextsnake;     /* next allocable snake structure */
static SNAKE    *lastsnake;     /* end of allocable snake structures */
static SNAKE    *endsnake;      /* last snake structure in list */
static LINE     **hashtable;    /* hash table for lines */
static modifyline_type modifyline;

FCOMP_DATA      fc;             /* externally referencable data */


/*
 * NAME
 *      canonical
 *
 * SYNOPSIS
 *      char *canonical(char *s);
 *
 * DESCRIPTION
 *      The canonical function is used to apply the -Upcase and -Spaces
 *      options to produce a canonical line for comparison purposes.
 *
 * ARGUMENTS
 *      s       - string to canonicalize
 *
 * RETURNS
 *      A pointer to the canonical line is returned.  This will be valid
 *      until the next call to this function.  If the canonical string
 *      would be the same as the input string, the input string is returned.
 */

static char *
canonical(char *s)
{
    static size_t   bufsiz;
    static char     *buf;
    size_t          len;
    int             changed;
    int             space;
    char            *cp1;
    char            *cp2;

    /*
     * if no modifiers are in effect,
     * no alterations will be required
     */
    if (!fc.upcaseflag && !fc.spaceflag)
        return s;

    /*
     * make sure the buffer is large enough
     */
    len = strlen(s) + 1;
    if (len > bufsiz)
    {
        if (buf)
        {
            bufsiz = len;
            buf = explain_realloc_or_die(buf, bufsiz);
        }
        else
        {
            bufsiz = len;
            buf = explain_malloc_or_die(bufsiz);
        }
    }

    /*
     * simple upcase is easier to handle
     */
    if (fc.upcaseflag)
    {
        changed = 0;
        cp1 = s;
        cp2 = buf;
        while (*cp1)
        {
            if (islower((unsigned char)*cp1))
            {
                *cp2++ = toupper((unsigned char)*cp1);
                changed = 1;
            }
            else
                *cp2++ = *cp1;
            ++cp1;
        }
        if (!changed)
            return s;
        *cp2 = 0;
        return buf;
    }

    /*
     * spaces are rather harder to handle
     *
     * keep track of whether a run of spaces has been seen.
     * replace this run of spaces with a single space
     * provided it is not at either end of a line.
     */
    cp1 = s;
    cp2 = buf;
    space = 0;
    changed = 0;
    while (*cp1)
    {
        int             c;

        c = (unsigned char)*cp1++;
        if (fc.upcaseflag && islower(c))
        {
            c = toupper(c);
            changed = 1;
        }
        if (c == '\n')
        {
            if (space)
                changed = 1;
            *cp2++ = c;
            space = 0;
        }
        else if (isspace(c))
        {
            if (space || c != ' ')
                changed = 1;
            space = 1;
        }
        else
        {
            if (space)
            {
                if (cp2 != buf)
                    *cp2++ = ' ';
                else
                    changed = 1;
            }
            *cp2++ = c;
            space = 0;
        }
    }
    if (!changed)
        return s;
    *cp2 = 0;
    return buf;
}


/*
 * Allocate a string of a certain size.  This routine is used instead of
 * simply calling malloc in order to reduce the overhead.  The allocated
 * strings cannot be freed or realloced.
 */

void *
allocstr(size_t len)
{
    char            *cp;
    static unsigned alignment_mask;

    if (!alignment_mask)
    {
        struct test
        {
            char            a;
            double          b;
        };
        unsigned        alignment;

        alignment = offsetof(struct test, b);
        /* make sure is a power of 2 */
        while ((alignment & (~alignment + 1)) != alignment)
            ++alignment;
        alignment_mask = alignment - 1;
    }

    /* round length to a multiple of alignment */
    len = (len + alignment_mask) & ~alignment_mask;

    if (len >= UNIQUEALLOCSIZE)
        return cm_alloc_and_check(len);
    cp = nextchar;
    if (len > availchars)
    {
        cp = cm_alloc_and_check(CHARALLOCSIZE);
        nextchar = cp;
        availchars = CHARALLOCSIZE;
    }
    nextchar += len;
    availchars -= len;
    return cp;
}


/*
 * Subroutine to allocate a line structure of the appropriate size and
 * add it to the hash table.  If it matches an already existing line, then
 * the old structure will be returned.
 */

static LINE *
addline(char *cp)
{
    LINE            *lp;        /* current line element */
    char            *curcp;     /* current character of string */
    LINE            **hashentry; /* hash entry to use */
    long            hash;       /* hash value */
    long            len;        /* line length */
    int             ch;         /* current char */
    char            *canon;

    /*
     * Compute line length and hash value.
     * This has to take into account the space and uppercase flags.
     */
    canon = canonical(cp);
    hash = 0;
    for (curcp = canon; *curcp; curcp++)
    {
        ch = *curcp;
        hash += ((hash * 101) + ch);
        if (hash < 0)
            hash = (hash + 1) & INFINITY;
    }
    len = curcp - canon;

    /*
     * Search proper hash chain for already existing line.
     * If the hash accidentally matches, modify it and try again.
     *
     * Search on the real data, not the canonical form,
     * that way fcomp -w -s can report more natural looking results.
     */
    again:
    hashentry = &hashtable[hash % HASHSIZE];
    for (lp = *hashentry; lp; lp = lp->l_next)
    {
        if (lp->l_hash != hash)
            continue;
        if (strcmp(cp, lp->l_data) == 0)
            return lp;
        if
        (
            (cp != canon || lp->l_canon != lp->l_data)
        &&
            strcmp(canon, lp->l_canon) == 0
        )
            continue;
        hash = ((hash * 1234321) + 1) & INFINITY;
        goto again;
    }

    /*
     * Line not found, allocate a new one
     */
    if (canon != cp)
    {
        lp = (LINE *)allocstr(LINE_SIZE(strlen(cp)));
        strcpy(lp->l_data, cp);
        lp->l_canon = allocstr(len + 1);
        strcpy(lp->l_canon, canon);
    }
    else
    {
        lp = (LINE *)allocstr(LINE_SIZE(len));
        strcpy(lp->l_data, cp);
        lp->l_canon = lp->l_data;
    }
    lp->l_hash = hash;
    lp->l_next = *hashentry;
    *hashentry = lp;
    return lp;
}


static int
is_blank_string(const char *s)
{
    while (*s)
        if (!isspace((unsigned char)*s))
            return 0;
    return 1;
}


/*
 * Readfile - read in a file and remember the maximum number of lines it needs.
 * This routine squeezes out blank lines if specified.
 * The first specified number of lines is skipped first.
 * Then the specified number of lines is modified.
 */

static void
readfile(FILEINFO *fi, long skip, long modify)
{
    input_ty        *fp;        /* file pointer for file to read in */
    LINE            **lines;    /* pointer to lines array */
    long            *lnums;     /* line numbers within file */
    char            *cp;        /* current line */
    long            linesavail; /* available number of lines */
    long            linecount;  /* current number of lines */
    long            linenumber; /* current line number */
    long            linelen;    /* length of line read */

    fi->is_binary = 0;
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Reading \"%s\"]", fi->f_name);
    fp = fi->f_file;
    if (skip > 0)
        input_skip_lines(fp, skip);
    linecount = 0;
    linenumber = 0;
    linesavail = LINEALLOCSIZE;
    lines = (LINE **)cm_alloc_and_check(sizeof(LINE *) * LINEALLOCSIZE);
    lnums = (long *)cm_alloc_and_check(sizeof(long) * LINEALLOCSIZE);
    for (;;)
    {
        cp = input_readline(fp, &linelen, 0, &fi->is_binary);
        if (!cp)
            break;
        if (++linenumber <= modify && modifyline)
            cp = modifyline(cp, &linelen, (struct INFO *)0);
        if
        (
            (fc.verbosity > VERBOSE_DEFAULT)
        &&
            ((linenumber % READBLABINTERVAL) == 0)
        )
            explain_output_error("[%ld lines]", linenumber);
        if (fc.blankflag && is_blank_string(cp))
            continue;
        if (linecount >= linesavail)
        {
            linesavail += LINEALLOCSIZE;
            lines =
                (LINE **)
                cm_realloc_and_check(lines, sizeof(LINE *) * linesavail);
            lnums =
                (long *)cm_realloc_and_check(lnums, sizeof(long) * linesavail);
        }
        lnums[linecount] = linenumber;
        lines[linecount] = addline(cp);
        linecount++;
    }
    fi->f_lnums = lnums;
    fi->f_lines = lines;
    fi->f_linecount = linecount;
    fi->f_linestotal = linenumber;
    if (linecount > fc.maxlines)
        fc.maxlines = linecount;
    input_delete(fp);
    if (fc.verbosity > VERBOSE_DEFAULT)
    {
        if (linecount == linenumber)
            explain_output_error("[Done, %ld lines]", linenumber);
        else
        {
            explain_output_error
            (
                "[Done, %ld lines, %ld non-blanks]",
                linenumber,
                linecount
            );
        }
    }
}


/*
 * Allocate a new snake structure and append it to the snake list.
 */

static SNAKE *
allocsnake(void)
{
    SNAKE           *sp;        /* snake being allocated */

    sp = nextsnake;
    if (sp >= lastsnake)
    {
        sp = (SNAKE *)cm_alloc_and_check(sizeof(SNAKE) * SNAKEALLOCSIZE);
        nextsnake = sp;
        lastsnake = sp + SNAKEALLOCSIZE;
    }
    nextsnake++;
    sp->next = NULL;
    if (!fc.snakelist)
        fc.snakelist = sp;
    else
        endsnake->next = sp;
    endsnake = sp;
    if (fc.verbosity > VERBOSE_DEFAULT && !(++snakecount % SNAKEBLABINTERVAL))
        explain_output_error("[%ld snake elements]", snakecount);
    return sp;
}


/*
 * Routine to find the middle snake of an optimial D-path spanning
 * lines A to A+N in file A to lines B to B+N in file B.  Returns the
 * length D of the D-path as a return value, and the upper left and
 * lower right relative coordinates of a snake midway through the D-path.
 */

static long
midsnake(int depth, long A, long N, long B, long M, long *ulx, long *uly,
    long *lrx, long *lry)
{
    long            D;
    long            MAXD;
    long            DELTA;
    long            odd;
    long            x;
    long            y;
    long            k;
    long            changes;
    long            oldx;
    LINE            **lp1;
    LINE            **lp2;

    if (fc.debugflag)
    {
        explain_output_error
        (
            "%*ssearching: %ld,%ld to %ld,%ld   midsnake: ",
            depth * 2,
            "",
            A,
            B,
            A + N,
            B + M
        );
    }

    DELTA = N - M;
    odd = DELTA & 1;
    MAXD = (M + N + 1) / 2;
    V1[1] = 0;
    V2[-1] = 0;
    changes = -odd - 2;

    /*
     * This is the main loop for searching for the snake.
     * D is the distance off the diagonals, and is the number
     * of changes needed to get from the upper left to the
     * lower right corner of the region.
     */
    for (D = 0; D <= MAXD; D++)
    {
        changes += 2;
        if (changes > fc.maxchanges)
        {
            sub_context_ty  *scp;

            scp = sub_context_new();
            sub_var_set_long(scp, "Number", fc.maxchanges);
            fatal_intl(scp, i18n("More than $number changes required"));
            /* NOTREACHED */
            sub_context_delete(scp);
        }
        if
        (
            (fc.verbosity > VERBOSE_DEFAULT)
        &&
            (depth == 0)
        &&
            (changes > 1)
        &&
            ((changes % CHANGEBLABINTERVAL) <= 1)
        )
            explain_output_error("[%ld changes]", changes - odd);

        /*
         * Examine all diagonals within current distance.
         * First search from upper left to lower right,
         * and then search from lower right to upper left.
         */
        for (k = -D; k <= D; k += 2)
        {
            /*
             * Find the end of the furthest forward D-path
             * in diagonal k.
             */
            if (k == -D || (k != D && (V1[k - 1] < V1[k + 1])))
                x = V1[k + 1];
            else
                x = V1[k - 1] + 1;
            y = x - k;
            lp1 = &fc.fileA.f_lines[A + x];
            lp2 = &fc.fileB.f_lines[B + y];
            oldx = x;
            while ((x < N) && (y < M) && ((*lp1)->l_hash == (*lp2)->l_hash))
            {
                x++;
                y++;
                lp1++;
                lp2++;
            }
            V1[k] = x;

            /*
             * See if path overlaps furthest reverse D-path.
             * If so, then we have found the snake.
             */
            if (odd && (k >= (DELTA - (D - 1))) && (k <= (DELTA + (D - 1))))
            {
                if ((x + V2[k - DELTA]) >= N)
                {
                    *ulx = oldx;
                    *uly = oldx - k;
                    *lrx = x;
                    *lry = y;
                    if (fc.debugflag)
                    {
                        explain_output_error
                        (
                            "%ld,%ld to %ld,%ld (odd)",
                            *ulx,
                            *uly,
                            *lrx,
                            *lry
                        );
                    }
                    return changes;
                }
            }
        }

        for (k = -D; k <= D; k += 2)
        {
            /*
             * Find the end of the furthest reaching reverse
             * path in diagonal k+DELTA.
             */
            if (k == D || (k != -D && (V2[k + 1] < V2[k - 1])))
                x = V2[k - 1];
            else
                x = V2[k + 1] + 1;
            y = x + k;
            lp1 = &fc.fileA.f_lines[A + N - x - 1];
            lp2 = &fc.fileB.f_lines[B + M - y - 1];
            oldx = x;
            while ((x < N) && (y < M) && ((*lp1)->l_hash == (*lp2)->l_hash))
            {
                x++;
                y++;
                lp1--;
                lp2--;
            }
            V2[k] = x;

            /*
             * See if path overlaps furthest forward D-path.
             * If so, then we have found the snake.
             */
            if (!odd && (k <= D - DELTA) && (k >= -D - DELTA))
            {
                if ((x + V1[k + DELTA]) >= N)
                {
                    *ulx = N - x;
                    *uly = M - y;
                    *lrx = N - oldx;
                    *lry = *lrx + *uly - *ulx;
                    if (fc.debugflag)
                    {
                        explain_output_error
                        (
                            "%ld,%ld to %ld,%ld (even)",
                            *ulx,
                            *uly,
                            *lrx,
                            *lry
                        );
                    }
                    return changes;
                }
            }
        }
    }
    explain_output_error_and_die("Middle snake procedure failed (bug)");
    return 0;
}


/*
 * Recursive routine to find a minimal D-path through the edit graph
 * of the two input files.  Arguments are the beginning line numbers in
 * the files, and the number of lines to examine.  This is basically a
 * divide-and-conquer routine which finds the middle snake of an optimal
 * D-path, then calls itself to find the remainder of the path before the
 * snake and after the snake.
 */

static void
findsnake(int depth, long A, long N, long B, long M)
{
    SNAKE           *sp;
    long            ulx;
    long            uly;
    long            lrx;
    long            lry;
    long            D;
    long            count;

    /*
     * If more than one change needed, then call ourself for each part.
     */
    D = midsnake(depth, A, N, B, M, &ulx, &uly, &lrx, &lry);
    if ((fc.verbosity > VERBOSE_DEFAULT) && (depth == 0))
    {
        explain_output_error("[%ld change%s total]", D, ((D == 1) ? "" : "s"));
        explain_output_error("[Finding edit path]");
    }

    if (D > 1)
    {
        if ((ulx > 0) && (uly > 0))
            findsnake(depth + 1, A, ulx, B, uly);
        count = lrx - ulx;
        if (count > fc.maxjoin)
        {
            sp = allocsnake();
            sp->line1 = A + ulx;
            sp->line2 = B + uly;
            sp->count = count;
        }
        N -= lrx;
        M -= lry;
        if ((N > 0) && (M > 0))
            findsnake(depth + 1, A + lrx, N, B + lry, M);
        return;
    }

    /*
     * Only 0 or 1 change needed, so we can compute the result directly.
     * First compute the snake coming from the upper left corner if any.
     */
    if (N > M)
        count = uly;
    else
        count = ulx;
    if (count > fc.maxjoin)
    {
        sp = allocsnake();
        sp->line1 = A;
        sp->line2 = B;
        sp->count = count;
    }

    /*
     * Finally compute the snake coming from the lower right corner if any.
     */
    count = lrx - ulx;
    if (count > fc.maxjoin)
    {
        sp = allocsnake();
        sp->line1 = A + ulx;
        sp->line2 = B + uly;
        sp->count = count;
    }
}


/*
 * Routine to generate the D-path list and compute the number
 * of insertions, deletions and matching lines found.
 */

static void
makesnakes(void)
{
    SNAKE           *sp;        /* current snake element */
    long            line1;      /* current line in file A */
    long            line2;      /* current line in file B */

    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Beginning comparisons]");
    tablesize = fc.maxlines * 2 + 1;
    V1 = (long *)cm_alloc_and_check(sizeof(long) * tablesize);
    V2 = (long *)cm_alloc_and_check(sizeof(long) * tablesize);
    V1 += fc.maxlines;
    V2 += fc.maxlines;
    if ((fc.fileA.f_linecount > 0) && (fc.fileB.f_linecount > 0))
        findsnake(0, 0L, fc.fileA.f_linecount, 0L, fc.fileB.f_linecount);

    /*
     * End the list with the lower right endpoint
     */
    sp = allocsnake();
    sp->line1 = fc.fileA.f_linecount;
    sp->line2 = fc.fileB.f_linecount;
    sp->count = 0;
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Comparisons complete]");
    if (fc.debugflag)
    {
        explain_output_error("%8s%8s%8s", "line1", "line2", "count");
        for (sp = fc.snakelist; sp; sp = sp->next)
        {
            explain_output_error
            (
                "%8ld%8ld%8ld",
                sp->line1,
                sp->line2,
                sp->count
            );
        }
    }

    /*
     * Scan the snake list and calculate the number of inserted,
     * deleted, and matching lines.
     */
    line1 = 0;
    line2 = 0;
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        fc.deletes += (sp->line1 - line1);
        fc.inserts += (sp->line2 - line2);
        fc.matches += sp->count;
        line1 = sp->line1 + sp->count;
        line2 = sp->line2 + sp->count;
    }
}


/*
 * Print the specified range of line numbers for both files.
 * The given string is also typed as part of the message.
 * This routine handles both full and line number output.
 * The real line numbers in each file are typed.
 */

static void
printrange(FILE *fp, const char *msg, long line1, long count1, long line2,
    long count2)
{
    long            beg1;
    long            end1;
    long            beg2;
    long            end2;       /* real beginning and ending line numbers */
    string_ty       *buf1;
    string_ty       *buf2;

    end1 = line1 + count1 - 1;
    end2 = line2 + count2 - 1;
    if (line1 >= fc.fileA.f_linecount)
    {
        beg1 = fc.fileA.f_linestotal + 1;
        end1 = beg1;
    }
    else
    {
        beg1 = fc.fileA.f_lnums[line1];
        end1 = fc.fileA.f_lnums[end1];
    }
    if (line2 >= fc.fileB.f_linecount)
    {
        beg2 = fc.fileB.f_linestotal + 1;
        end2 = beg2;
    }
    else
    {
        beg2 = fc.fileB.f_lnums[line2];
        end2 = fc.fileB.f_lnums[end2];
    }
    if (beg1 == end1)
    {
        if (fc.hexify_flag)
            buf1 = str_format("0x%lX", beg1 - 1);
        else
            buf1 = str_format("%ld", beg1);
    }
    else
    {
        if (fc.hexify_flag)
            buf1 = str_format("0x%lX-0x%lX", beg1 - 1, end1 - 1);
        else
            buf1 = str_format("%ld-%ld", beg1, end1);
    }
    if (beg2 == end2)
    {
        if (fc.hexify_flag)
            buf2 = str_format("0x%lX", beg2 - 1);
        else
            buf2 = str_format("%ld", beg2);
    }
    else
    {
        if (fc.hexify_flag)
            buf2 = str_format("0x%lX-0x%lX", beg2 - 1, end2 - 1);
        else
            buf2 = str_format("%ld-%ld", beg2, end2);
    }
    if (fc.editscriptflag)
    {
        fprintf(fp, "%c %s %s\n", *msg, buf1->str_text, buf2->str_text);
    }
    else
    {
        fprintf
        (
            fp,
            "\n******************** %s [A %s, B %s]:\n",
            msg,
            buf1->str_text,
            buf2->str_text
        );
    }
    str_free(buf1);
    str_free(buf2);
}


/*
 * Translate all tabs to the appropriate number of spaces in a line.
 * This is used when the line is not output at a nice tab boundary,
 * so that the output still looks correct.  Returns the new converted
 * string, which is only valid until the next call.
 */

static char *
detab(const char *str)
{
    int             pos;        /* current character position */
    int             ch;         /* current character */
    char            *destcp;    /* destination pointer */
    static char     *strbuf;    /* string buffer */
    static int      maxlen;     /* maximum size of string buffer */

    if (!maxlen)
    {
        maxlen = DETABALLOCSIZE;
        strbuf = explain_malloc_or_die(maxlen + TABSIZE + 1);
    }

    destcp = strbuf;
    pos = 0;
    for (;;)
    {
        ch = *str++;
        if (!ch)
            break;
        if (pos >= maxlen)
        {
            maxlen += DETABALLOCSIZE;
            strbuf = explain_realloc_or_die(strbuf, maxlen + TABSIZE + 1);
            destcp = strbuf + pos;
        }
        if (ch != '\t')
        {
            *destcp++ = ch;
            pos++;
        }
        else
        {
            for (;;)
            {
                *destcp++ = ' ';
                pos++;
                if ((pos % TABSIZE) == 0)
                    break;
            }
        }
    }
    *destcp = '\0';
    return strbuf;
}


/*
 * Print the specified number of lines of a file beginning at the
 * specified line number.  This prints imbedded blank lines too.
 * The lines can be preceeded by line number if desired.  Context
 * lines can be added also.  Tabs are converted to the appropriate
 * number of spaces if line numbering or tags are supplied.
 */

static void
printlines(FILE *fp, FILEINFO *fi, long line, long count, const char *tagstr)
{
    LINE            **lines;    /* data of file */
    long            *lnum;      /* line numbers */
    char            *data;      /* current data line */
    long            curlnum;    /* current line number */

    line = line - fc.context - 1;
    count += (2 * fc.context);
    if (line < 0)
    {
        count += line;
        line = 0;
    }
    if ((line + count) > fi->f_linecount)
        count = fi->f_linecount - line;
    lines = fi->f_lines + line;
    lnum = fi->f_lnums + line;
    curlnum = *lnum;
    while (count-- > 0)
    {
        while (curlnum < *lnum)
        {
            if (fc.whatflag)
                fprintf(fp, "%s\n", tagstr);
            else if (fc.numflag)
                fprintf(fp, "%c%ld\n", fi->f_tag, curlnum);
            else
                putc('\n', fp);
            curlnum++;
        }
        if (fc.whatflag)
            fprintf(fp, "%s  ", tagstr);
        else if (fc.numflag)
            fprintf(fp, "%c%-7ld ", fi->f_tag, curlnum);
        data = (*lines)->l_data;
        if (fc.whatflag)
            data = detab(data);
        fputs(data, fp);
        lines++;
        lnum++;
        curlnum++;
    }
}


/*
 * Dump lines which match, either just the line numbers or with the text.
 */

void
dumpmatch(const char *outputname)
{
    FILE            *fp;
    SNAKE           *sp;

    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
        explain_output_error("[Writing matching lines to \"%s\"]", outputname);
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = gettext("standard output");
    }
    if (fc.editscriptflag)
        fprintf(fp, "A %s\nB %s\n", fc.fileA.f_name, fc.fileB.f_name);
    else
    {
        fprintf
        (
            fp,
            "FILE A: %s\nFILE B: %s\n",
            fc.fileA.f_name,
            fc.fileB.f_name
        );
        fprintf
        (
            fp,
            "TOTALS: %ld inserted  %ld deleted  %ld matched\n",
            fc.inserts,
            fc.deletes,
            fc.matches
        );
    }
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        if (sp->count <= 0)
            continue;
        printrange(fp, "MATCH", sp->line1, sp->count, sp->line2, sp->count);
        if (!fc.editscriptflag)
            printlines(fp, &fc.fileB, sp->line2 + 1, sp->count, "");
    }
    if (fc.editscriptflag)
        fprintf(fp, "T %ld %ld %ld\n", fc.inserts, fc.deletes, fc.matches);
    explain_fclose_or_die(fp);
}


/*
 * Dump the edit script in the normal manner, showing changed line
 * numbers along with the relevent text.
 */

void
dumpnormal(const char *outputname)
{
    FILE            *fp;        /* output file */
    SNAKE           *sp;        /* current snake */
    long            line1;
    long            line2;      /* current line numbers */
    long            count1;
    long            count2;     /* line counts */

    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
        explain_output_error("[Writing differences to \"%s\"]", outputname);
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = gettext("standard output");
    }
    if (fc.quickflag)
    {
        fprintf
        (
            fp,
            "Files \"%s\" and \"%s\":\n",
            fc.fileA.f_name,
            fc.fileB.f_name
        );
        fprintf
        (
            fp,
            "%ld inserted  %ld deleted  %ld matched\n",
            fc.inserts,
            fc.deletes,
            fc.matches
        );
        goto done;
    }
    if ((fc.inserts == 0) && (fc.deletes == 0))
    {
        if (fc.verbosity)
        {
            fprintf
            (
                fp,
                "Files \"%s\" and \"%s\" are identical\n",
                fc.fileA.f_name,
                fc.fileB.f_name
            );
        }
        goto done;
    }
    fprintf
    (
        fp,
     "FILE A: %s\nFILE B: %s\nTOTALS: %ld inserted  %ld deleted  %ld matched\n",
        fc.fileA.f_name,
        fc.fileB.f_name,
        fc.inserts,
        fc.deletes,
        fc.matches
    );
    line1 = 0;
    line2 = 0;
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        count1 = sp->line1 - line1;
        count2 = sp->line2 - line2;
        if (count1 && count2)
        {
            printrange(fp, "REPLACE", line1, count1, line2, count2);
            printlines(fp, &fc.fileA, line1 + 1, count1, "");
            fputs("******************** WITH:\n", fp);
            printlines(fp, &fc.fileB, line2 + 1, count2, "");
        }
        else if (count1)
        {
            printrange(fp, "DELETE", line1, count1, line2, 1L);
            printlines(fp, &fc.fileA, line1 + 1, count1, "");
        }
        else if (count2)
        {
            printrange(fp, "INSERT", line1, 1L, line2, count2);
            printlines(fp, &fc.fileB, line2 + 1, count2, "");
        }
        line1 = sp->line1 + sp->count;
        line2 = sp->line2 + sp->count;
    }

    done:
    explain_fclose_or_die(fp);
}


/*
 * Dump all of both files showing what happened to each line.
 */

void
dumpwhat(const char *outputname)
{
    FILE            *fp;
    SNAKE           *sp;
    long            line1;
    long            line2;      /* current line numbers */
    long            count1;
    long            count2;     /* line counts */

    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
    {
        explain_output_error
        (
            "[Writing what changes were made to \"%s\"]",
            outputname
        );
    }
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = "(stdandard output)";
    }
    fprintf(fp, "FILE A: %s\n", fc.fileA.f_name);
    fprintf(fp, "FILE B: %s\n", fc.fileB.f_name);
    fprintf
    (
        fp,
        "TOTALS: %ld inserted  %ld deleted  %ld matched\n",
        fc.inserts,
        fc.deletes,
        fc.matches
    );
    line1 = 0;
    line2 = 0;
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        count1 = sp->line1 - line1;
        count2 = sp->line2 - line2;
        if (count1 && count2)
        {
            printlines(fp, &fc.fileA, line1 + 1, count1, "|-");
            printlines(fp, &fc.fileB, line2 + 1, count2, "|+");
        }
        else if (count1)
            printlines(fp, &fc.fileA, line1 + 1, count1, "|-");
        else if (count2)
            printlines(fp, &fc.fileB, line2 + 1, count2, "|+");
        printlines(fp, &fc.fileB, sp->line2 + 1, sp->count, "  ");
        line1 = sp->line1 + sp->count;
        line2 = sp->line2 + sp->count;
    }
    explain_fclose_or_die(fp);
}


/*
 * Dump only the line numbers which have changed.
 * This is useful for machine processing of the differences.
 */

void
dumplines(const char *outputname)
{
    FILE            *fp;        /* output file */
    SNAKE           *sp;        /* current snake */
    long            line1;
    long            line2;      /* previous line numbers */
    long            count1;
    long            count2;     /* count of line numbers */

    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
        explain_output_error("[Writing edit script to \"%s\"]", outputname);
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = gettext("standard output");
    }
    line1 = 0;
    line2 = 0;
    fprintf(fp, "A %s\nB %s\n", fc.fileA.f_name, fc.fileB.f_name);
    for (sp = fc.snakelist; sp; sp = sp->next)
    {
        count1 = sp->line1 - line1;
        count2 = sp->line2 - line2;
        if (count1 && count2)
            printrange(fp, "R", line1, count1, line2, count2);
        else if (count1)
            printrange(fp, "D", line1, count1, line2, 1L);
        else if (count2)
            printrange(fp, "I", line1, 1L, line2, count2);
        line1 = sp->line1 + sp->count;
        line2 = sp->line2 + sp->count;
    }
    fprintf(fp, "T %ld %ld %ld\n", fc.inserts, fc.deletes, fc.matches);
    explain_fclose_or_die(fp);
}


/*
 * Reinitialize for a new file comparison.
 * This zeroes out our static variables so that another
 * file comparison will work correctly.
 */

void
fcompreset(void)
{
    tablesize = 0;
    snakecount = 0;
    availchars = 0;
    fc.inserts = 0;
    fc.deletes = 0;
    fc.matches = 0;
    fc.snakelist = NULL;
    nextchar = NULL;
    V1 = NULL;
    V2 = NULL;
    nextsnake = NULL;
    lastsnake = NULL;
    endsnake = NULL;
    hashtable = NULL;
}


void
binary_warning(const char *filename)
{
    sub_context_ty  *scp;

    scp = sub_context_new();
    sub_var_set_charstar(scp, "File_Name", filename);
    error_intl(scp, i18n("warning: $filename is binary"));
    sub_context_delete(scp);
}


void
binary_fatal(const char *filename)
{
    sub_context_ty  *scp;

    scp = sub_context_new();
    sub_var_set_charstar(scp, "File_Name", filename);
    fatal_intl(scp, i18n("$filename is binary"));
    /* NOTREACHED */
    sub_context_delete(scp);
}


/*
 * Routine to do the comparison of two files.
 * If either of the input filenames is NULL, then this indicates
 * that the file has already been read by an earlier call.
 */

void
fcomp(const char *nameA, const char *nameB)
{
    long            changes;    /* total number of changes */

    trace(("fcomp(nameA = \"%s\", nameB = \"%s\")\n{\n", nameA, nameB));
    fc.fileA.f_tag = 'A';
    fc.fileB.f_tag = 'B';
    if (nameA)
    {
        fc.fileA.f_name = nameA;
        if (fc.hexify_flag)
        {
            fc.fileA.f_file = input_hexify(input_file_open(nameA), 1);
        }
        else if (fc.binary)
        {
            fc.fileA.f_file =
                input_quoted_printable_encode(input_file_open(nameA), 1);
        }
        else
            fc.fileA.f_file = input_file_text_open(nameA);
    }
    if (nameB)
    {
        fc.fileB.f_name = nameB;
        if (fc.hexify_flag)
        {
            fc.fileB.f_file = input_hexify(input_file_open(nameB), 1);
        }
        else
            fc.fileB.f_file = input_file_text_open(nameB);
    }
    if (!nameA && !nameB)
    {
        fatal_intl(0, i18n("input file names not supplied"));
    }
    if (!hashtable)
    {
        int             j;

        hashtable = (LINE **)cm_alloc_and_check(HASHSIZE * sizeof(LINE *));
        for (j = 0; j < HASHSIZE; ++j)
            hashtable[j] = 0;
    }
    if (nameA)
    {
        readfile(&fc.fileA, fc.fileAskip, fc.fileAmodify);
        if (fc.fileA.is_binary)
        {
            binary_warning(nameA);
            if (fc.cant_do_binary)
                return;
        }
    }
    if (nameB)
    {
        readfile(&fc.fileB, fc.fileBskip, fc.fileBmodify);
        if (fc.fileB.is_binary)
        {
            binary_warning(nameB);
            if (fc.cant_do_binary)
                return;
        }
    }
    changes = fc.fileA.f_linecount - fc.fileB.f_linecount;
    if (changes < 0)
        changes = -changes;
    if (changes > fc.maxchanges)
    {
        sub_context_ty  *scp;

        scp = sub_context_new();
        sub_var_set_long(scp, "Number", fc.maxchanges);
        fatal_intl(scp, i18n("More than $number changes required"));
        /* NOTREACHED */
        sub_context_delete(scp);
    }
    makesnakes();
    trace(("}\n"));
}


void
modifyline_register(modifyline_type func)
{
    modifyline = func;
}


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