/*
 * fhist - file history and comparison tools
 * Copyright (C) 1991-1994, 1997-2000, 2002, 2005, 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/stdio.h>
#include <common/ac/string.h>
#include <common/ac/libintl.h>
#include <libexplain/fclose.h>
#include <libexplain/fopen.h>

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

#include <fmerge/work.h>

#define CONFLICT_BEGIN  \
"/-/-/-/-/-/-/-/-/-/ BEGIN CONFLICT  [O%ld A%ld B%ld] /-/-/-/-/-/-/-/-/-/-/\n"
#define CONFLICT_MIDDLE \
"/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/\n"
#define CONFLICT_END    \
"/-/-/-/-/-/-/-/-/-/-/-/-/  END CONFLICT   /-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/-/\n"

#define F_OLDDATA       0x0000  /* lines the same as the old ones */
#define F_DELETEA       0x0001  /* lines deleted by file A */
#define F_INSERTA       0x0002  /* lines inserted by file A */
#define F_DELETEB       0x0004  /* lines deleted by file B */
#define F_INSERTB       0x0008  /* lines inserted by file B */
#define F_CONFLICT      0x0010  /* this line is a conflict */

#define MAXSHORT        0x7fff  /* highest positive short value */


typedef struct MERGE MERGE;
struct MERGE
{
    long            m_baseline;     /* line number in base file */
    long            m_fileAline;    /* line number in file A */
    long            m_fileBline;    /* line number in file B */
    short           m_basecount;    /* lines in base file used or deleted */
    short           m_fileAcount;   /* number of lines inserted by file A */
    short           m_fileBcount;   /* number of lines inserted by file B */
    short           m_flags;        /* what is happening to this line */
    MERGE           *m_next;        /* next line */
};


long            conflicts;          /* number of conflicts */
long            failcount;          /* maximum conflicts allowable */
long            unchangecount;      /* maximum unchanged lines to output */
short           ignoreflag;         /* ignore conflicts */
short           ignore_identical;   /* ignore identical conflicts */

static MERGE    *begmergelist;      /* head of merge list */
static MERGE    *endmergelist;      /* end of merge list */
static LINE     **baselines;        /* lines of base file */
static LINE     **fileAlines;       /* lines of file A */
static LINE     **fileBlines;       /* lines of file B */


/*
 * Allocate a new merge structure and append it to the end of the merge list.
 */

static MERGE *
newmerge(void)
{
    MERGE           *mp;            /* new merge structure */

    mp = (MERGE *)cm_alloc_and_check(sizeof(MERGE));
    mp->m_next = NULL;
    mp->m_flags = 0;
    mp->m_baseline = 0;
    mp->m_fileAline = 0;
    mp->m_fileBline = 0;
    mp->m_basecount = 0;
    mp->m_fileAcount = 0;
    mp->m_fileBcount = 0;
    if (begmergelist == NULL)
        begmergelist = mp;
    else
        endmergelist->m_next = mp;
    endmergelist = mp;
    return mp;
}


/*
 * Merge differences between two divergent files, based on a common
 * precursor file.  The resulting output is saved in memory so that it
 * can be examined or output later.
 */

void
fmerge(const char *basename, const char *nameA, const char *nameB)
{
    SNAKE           *spA;           /* snake for edits of first file */
    SNAKE           *spB;           /* snake for edits of second file */
    MERGE           *mp;            /* current merge structure */
    SNAKE           *fileAsnakes;   /* snake for first edit */
    SNAKE           *fileBsnakes;   /* snake for second edit */
    long            baseline;       /* current line of base file */
    long            lineA;          /* current line of file A */
    long            lineB;          /* current line of file B */
    long            deletesA;       /* deletes needed for file A */
    long            insertsA;       /* inserts needed for file A */
    long            deletesB;       /* deletes needed for file B */
    long            insertsB;       /* inserts needed for file B */
    long            count;          /* number of lines being examined */
    int             bad;            /* 1 if bad merge detected */

    fc.maxjoin = 0;
    fc.maxchanges = INFINITY;
    fcomp(basename, nameA);
    baselines = fc.fileA.f_lines;
    fileAlines = fc.fileB.f_lines;
    fc.fileB.f_lines = NULL;
    fileAsnakes = fc.snakelist;
    fc.snakelist = NULL;
    fcomp(NULL, nameB);
    fileBsnakes = fc.snakelist;
    fileBlines = fc.fileB.f_lines;
    fc.fileB.f_lines = NULL;
    fc.snakelist = NULL;

    /*
     * Now walk through the snakes and merge the lines.
     */
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[Creating merge list]");
#endif
    spA = fileAsnakes;
    spB = fileBsnakes;
    baseline = 0;
    lineA = 0;
    lineB = 0;
    conflicts = 0;
    begmergelist = NULL;
    endmergelist = NULL;
    for (;;)
    {
        int             det;

        deletesA = spA->line1 - baseline;
        insertsA = spA->line2 - lineA;
        deletesB = spB->line1 - baseline;
        insertsB = spB->line2 - lineB;
        if (deletesA > MAXSHORT)
            deletesA = MAXSHORT;
        if (insertsA > MAXSHORT)
            insertsA = MAXSHORT;
        if (deletesB > MAXSHORT)
            deletesB = MAXSHORT;
        if (insertsB > MAXSHORT)
            insertsB = MAXSHORT;
        mp = newmerge();
        mp->m_baseline = baseline;
        mp->m_fileAline = lineA;
        mp->m_fileBline = lineB;
        mp->m_fileAcount = insertsA;
        mp->m_fileBcount = insertsB;

        bad = ((deletesA < 0) || (insertsA < 0) ||
            (deletesB < 0) || (insertsB < 0));

        det =
            (
                (deletesA > 0 ? F_DELETEA : 0)
            +
                (insertsA > 0 ? F_INSERTA : 0)
            +
                (deletesB > 0 ? F_DELETEB : 0)
            +
                (insertsB > 0 ? F_INSERTB : 0)
            );

        if (fc.debugflag || bad)
        {
#if 0
            explain_output_error
            (
                "SA %ld %ld %ld, SB %ld %ld %ld, Lines %ld %ld %ld, Case %ld",
                spA->line1,
                spA->line2,
                spA->count,
                spB->line1,
                spB->line2,
                spB->count,
                baseline,
                lineA,
                lineB,
                det
            );
#endif
            if (bad)
                fatal_intl(0, i18n("bad merge calculation"));
        }

        switch (det)
        {
        case 0:
            /* no inserts or deletes at all */
            if ((spA->next == NULL) && (spB->next == NULL))
            {
                if (ignoreflag)
                    conflicts = 0;
                return;
            }
            count = spA->count;
            if (count > spB->count)
                count = spB->count;
            if (count > MAXSHORT)
                count = MAXSHORT;
            mp->m_flags = F_OLDDATA;
            mp->m_basecount = count;
            mp->m_fileAcount = count;
            mp->m_fileBcount = count;
            spA->line1 += count;
            spA->line2 += count;
            spA->count -= count;
            spB->line1 += count;
            spB->line2 += count;
            spB->count -= count;
            break;

        case F_INSERTA:
            /* insert for file A */
            mp->m_flags = F_INSERTA;
            break;

        case (F_INSERTA | F_DELETEA):
            /* insert and delete for file A */
            mp->m_flags = F_INSERTA;
            /* Fall through... */

        case F_DELETEA:
            /* delete for file A */
            count = deletesA;
            if (count > spB->count)
                count = spB->count;
            mp->m_flags |= F_DELETEA;
            mp->m_basecount = count;
            mp->m_fileBcount = count;
            spB->line1 += count;
            spB->line2 += count;
            spB->count -= count;
            break;

        case (F_INSERTB | F_DELETEB):
            /* insert and delete for file B */
            mp->m_flags = F_INSERTB;
            /* Fall through... */

        case F_DELETEB:
            /* delete for file B */
            count = deletesB;
            if (count > spA->count)
                count = spA->count;
            mp->m_flags |= F_DELETEB;
            mp->m_basecount = count;
            mp->m_fileAcount = count;
            spA->line1 += count;
            spA->line2 += count;
            spA->count -= count;
            break;

        case (F_DELETEA | F_DELETEB):
            /* files A and B both delete */
            count = deletesA;
            if (count > deletesB)
                count = deletesB;
            mp->m_flags |= F_DELETEA | F_DELETEB;
            mp->m_basecount = count;
            if (ignore_identical)
            {
                /*
                 * This is not an actual conflict, even though it
                 * is a logical conflict, according to most users'
                 * expectations.
                 */
                break;
            }
            mp->m_flags |= F_CONFLICT;
            conflicts += count;
            break;

        case (F_INSERTA | F_DELETEA | F_DELETEB):
            /* file A inserts and deletes, file B deletes */
            count = deletesA;
            if (count > deletesB)
                count = deletesB;
            mp->m_flags = (F_INSERTA | F_DELETEA | F_DELETEB | F_CONFLICT);
            mp->m_basecount = count;
            conflicts += count;
            break;

        case F_INSERTB:
            /* insert for file B */
            mp->m_flags = F_INSERTB;
            break;

        case (F_INSERTA | F_INSERTB):
            /* file A and file B both insert */
            if (ignore_identical)
            {
                for
                (
                    count = 0;
                    count < mp->m_fileAcount && count < mp->m_fileBcount;
                    ++count
                )
                {
                    LINE *lp_a = fileAlines[mp->m_fileAline + count];
                    LINE *lp_b = fileBlines[mp->m_fileBline + count];
                    if (0 != strcmp(lp_a->l_data, lp_b->l_data))
                        break;
                }
                if (count > 0)
                {
                    /*
                     * This is not an actual conflict according to most
                     * users' expectations, even though it is a logical
                     * conflict.
                     */
                    mp->m_flags = F_INSERTA;
                    mp->m_basecount = 0;
                    mp->m_fileAcount = count;
                    mp->m_fileBcount = count;
                    break;
                }
            }
            mp->m_flags = F_INSERTA | F_INSERTB | F_CONFLICT;
            conflicts++;
            break;

        case (F_INSERTA | F_DELETEA | F_INSERTB):
            /* insert and delete for A, insert for B */
            mp->m_flags = F_INSERTA;
            /* Fall through... */

        case (F_DELETEA | F_INSERTB):
            /* delete for file A and insert for file B */
            mp->m_flags |= (F_DELETEA | F_INSERTB | F_CONFLICT);
            mp->m_basecount = 1;
            spB->line1++;
            spB->line2++;
            spB->count--;
            lineB++;
            conflicts++;
            break;

        case (F_DELETEA | F_INSERTB | F_DELETEB):
            /* file A deletes, file B inserts and deletes */
            count = deletesA;
            if (count > deletesB)
                count = deletesB;
            mp->m_flags = (F_DELETEA | F_INSERTB | F_DELETEB | F_CONFLICT);
            mp->m_basecount = count;
            conflicts += count;
            break;

        case (F_INSERTA | F_INSERTB | F_DELETEB):
            /* insert for A, insert and delete for B */
            mp->m_flags = F_INSERTB;
            /* Fall through... */

        case (F_INSERTA | F_DELETEB):
            /* insert for file A and delete for file B */
            mp->m_flags |= (F_INSERTA | F_DELETEB | F_CONFLICT);
            mp->m_basecount = 1;
            spA->line1++;
            spA->line2++;
            spA->count--;
            lineA++;
            conflicts++;
            break;

        case (F_INSERTA | F_DELETEA | F_INSERTB | F_DELETEB):
            /* insert and delete for A and for B */
            count = deletesA;
            if (count > deletesB)
                count = deletesB;
            mp->m_basecount = count;
            if (deletesA == deletesB && ignore_identical)
            {
                for
                (
                    count = 0;
                    count < mp->m_fileAcount && count < mp->m_fileBcount;
                    ++count
                )
                {
                    LINE *lp_a = fileAlines[mp->m_fileAline + count];
                    LINE *lp_b = fileBlines[mp->m_fileBline + count];
                    if (0 != strcmp(lp_a->l_data, lp_b->l_data))
                        break;
                }
                if (count > 0)
                {
                    /*
                     * This is not an actual conflict according to most
                     * users' expectations, even though it is a logical
                     * conflict.
                     */
                    mp->m_flags = F_INSERTA | F_DELETEA | F_DELETEB;
                    break;
                }
            }
            mp->m_flags =
                (F_INSERTA | F_DELETEA | F_INSERTB | F_DELETEB | F_CONFLICT);
            conflicts += count;
            break;
        }
        if ((conflicts > failcount) && !ignoreflag)
        {
            sub_context_ty  *scp;

            scp = sub_context_new();
            sub_var_set_long(scp, "Number", failcount);
            fatal_intl
            (
                scp,
                i18n("more than $number conflicts found during merge")
            );
        }
        baseline += mp->m_basecount;
        lineA += mp->m_fileAcount;
        lineB += mp->m_fileBcount;
        if ((spA->count <= 0) && (spA->next))
            spA = spA->next;
        if ((spB->count <= 0) && (spB->next))
            spB = spB->next;
    }
}


/*
 * Walk through the merge list and output the resulting file.
 * Conflicts will be identified by conflict identification lines.
 */

void
dumpmergedfile(const char *outputname)
{
    MERGE           *mp;            /* current merge structure */
    LINE            **lp;           /* current line */
    FILE            *fp;            /* file to output to (or TERMINAL) */
    long            count;          /* line count */
    int             conflict;       /* 1 if inside of a conflict */
    int             newconflict;    /* new conflict value */

#if 0
    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
        explain_output_error("[Writing merged lines to \"%s\"]", outputname);
#endif
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = gettext("standard output");
    }
    conflict = 0;
    for (mp = begmergelist; mp; mp = mp->m_next)
    {
        newconflict = ((mp->m_flags & F_CONFLICT) && !ignoreflag);
        if (newconflict != conflict)
        {
            conflict = newconflict;
            fprintf
            (
                fp,
                conflict ? CONFLICT_BEGIN : CONFLICT_END,
                mp->m_baseline + 1,
                mp->m_fileAline + 1,
                mp->m_fileBline + 1
            );
        }
        if (mp->m_flags == F_OLDDATA)
        {
            lp = &baselines[mp->m_baseline];
            for (count = mp->m_basecount; count > 0; count--)
                fputs((*lp++)->l_data, fp);
            continue;
        }
        if (mp->m_flags & F_INSERTA)
        {
            lp = &fileAlines[mp->m_fileAline];
            for (count = mp->m_fileAcount; count > 0; count--)
                fputs((*lp++)->l_data, fp);
        }
        if (conflict && (mp->m_flags & F_INSERTA) && (mp->m_flags & F_INSERTB))
            fputs(CONFLICT_MIDDLE, fp);
        if (mp->m_flags & F_INSERTB)
        {
            lp = &fileBlines[mp->m_fileBline];
            for (count = mp->m_fileBcount; count > 0; count--)
                fputs((*lp++)->l_data, fp);
        }
    }
    if (conflict)
        fputs(CONFLICT_END, fp);
    explain_fclose_or_die(fp);
}


/*
 * Walk down and dump out a list of merges, either for non-conflicts,
 * or for either file A or B of a conflict.  Returns the first merge
 * structure not processed due to the change of conflict.
 */

static MERGE *
walkmerges(FILE *fp, MERGE *mp, int flagmask)
{
    LINE            **lp;           /* current line */
    short           count;          /* number of lines to handle */
    short           flags;          /* flags to be checked */

    for (;; mp = mp->m_next)
    {
        if
        (
            (mp == NULL)
        ||
            ((flagmask & F_CONFLICT) == (mp->m_flags & F_CONFLICT))
        )
            return mp;
        flags = mp->m_flags;
        if (flags == F_OLDDATA)
        {
            /* show original lines */
            lp = &baselines[mp->m_baseline];
            count = mp->m_basecount;
            if (((count - 1) / 2) < unchangecount)
            {
                while (--count >= 0)
                {
                    writefx(fp, "   ", 3u);
                    fputs((*lp++)->l_data, fp);
                }
                continue;
            }
            /*
             * Number of unchanged lines exceeds specified limit.
             * So only show the first few and last few changes.
             */
            for (count = unchangecount; count > 0; count--)
            {
                writefx(fp, "   ", 3u);
                fputs((*lp++)->l_data, fp);
            }
            fprintf(fp, "U  %ld\n", mp->m_basecount - (unchangecount * 2));
            lp = &baselines[mp->m_baseline + mp->m_basecount - unchangecount];
            for (count = unchangecount; count > 0; count--)
            {
                writefx(fp, "   ", 3u);
                fputs((*lp++)->l_data, fp);
            }
            continue;
        }
        flags &= flagmask;
        if (flags & F_INSERTA)
        {
            lp = &fileAlines[mp->m_fileAline];
            for (count = mp->m_fileAcount; count > 0; count--)
            {
                writefx(fp, "IA ", 3u);
                fputs((*lp++)->l_data, fp);
            }
        }
        if (flags & F_DELETEA)
        {
            lp = &baselines[mp->m_baseline];
            for (count = mp->m_basecount; count > 0; count--)
            {
                writefx(fp, "DA ", 3u);
                fputs((*lp++)->l_data, fp);
            }
        }
        if (flags & F_INSERTB)
        {
            lp = &fileBlines[mp->m_fileBline];
            for (count = mp->m_fileBcount; count > 0; count--)
            {
                writefx(fp, "IB ", 3u);
                fputs((*lp++)->l_data, fp);
            }
        }
        if (flags & F_DELETEB)
        {
            lp = &baselines[mp->m_baseline];
            for (count = mp->m_basecount; count > 0; count--)
            {
                writefx(fp, "DB ", 3u);
                fputs((*lp++)->l_data, fp);
            }
        }
    }
}


/*
 * Dump out the possible conflicts to a file that indicates what both
 * divergent files did to the base file.  This is useful even if there
 * are no physical conflicts in order to manually find logical conflicts.
 */

void
dumpconflicts(const char *outputname)
{
    MERGE           *mp;            /* current merge structure */
    FILE            *fp;            /* file to output to */

#if 0
    if ((fc.verbosity > VERBOSE_DEFAULT) && outputname)
        explain_output_error("[Writing conflict lines to \"%s\"]", outputname);
#endif
    if (outputname)
    {
        fp = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        fp = stdout;
        outputname = gettext("standard output");
    }
    fprintf(fp, "T %ld conflict%s\n", conflicts, (conflicts == 1) ? "" : "s");
    mp = begmergelist;
    while (mp)
    {
        /* non-conflicts */
        mp = walkmerges(fp, mp, -1);
        if (mp == NULL)
            break;
        if (!ignoreflag)
        {
            writefx(fp, "X  ", 3u);
            fprintf(fp, CONFLICT_BEGIN, mp->m_baseline + 1,
                mp->m_fileAline + 1, mp->m_fileBline + 1);
        }
        /* file A conflicts */
        walkmerges(fp, mp, F_INSERTA | F_DELETEA);
        if (!ignoreflag)
            fprintf(fp, "X  %s", CONFLICT_MIDDLE);
        /* file B conflicts */
        mp = walkmerges(fp, mp, F_INSERTB | F_DELETEB);
        if (!ignoreflag)
            fprintf(fp, "X  %s", CONFLICT_END);
    }
    explain_fclose_or_die(fp);
}


/*
 * Read in a conflict file and produce an output file.
 */

void
convertconflicts(const char *inputname, const char *outputname)
{
    char            *cp;            /* current line */
    FILE            *ip;            /* input file */
    FILE            *op;            /* output file */
    long            inputlines;
    long            outputlines;    /* input and output line counts */
    sub_context_ty  *scp;

#if 0
    if (fc.verbosity > VERBOSE_DEFAULT && outputname)
    {
        explain_output_error
        (
            "[Converting conflict file \"%s\" to output file \"%s\"]",
            inputname,
            outputname
        );
    }
#endif
    inputlines = 0;
    outputlines = 0;
    ip = explain_fopen_or_die(inputname, "r");
    if (outputname)
    {
        op = explain_fopen_or_die(outputname, "w");
    }
    else
    {
        op = stdout;
        outputname = gettext("standard output");
    }
    for (;;)
    {
        int             bin;
        long            retlen;         /* length of current line */

        bin = 0;
        cp = readlinef(ip, &retlen, 0, inputname, &bin);
        if (cp == NULL)
            break;
        if (bin)
            binary_fatal(inputname);
#if 0
        if (((++inputlines % 1000) == 0) && (fc.verbosity > VERBOSE_DEFAULT))
            explain_output_error("[%ld lines]", inputlines);
#endif
        switch (*cp)
        {
        case 'I':       /* inserted lines */
        case ' ':       /* original base lines */
            if (retlen <= 3)
            {
                writefx(op, "\n", 1u);
                retlen = 1;
            }
            else
            {
                cp += 3;
                retlen -= 3;
                writefx(op, cp, retlen);
            }
            outputlines++;
            break;

        case '\n':      /* original blank line */
            writefx(op, "\n", 1u);
            break;

        case 'D':       /* deleted lines */
        case 'T':       /* the total line */
            break;

        case 'X':
            if (ignoreflag)
                break;
            scp = sub_context_new();
            sub_var_set_long(scp, "Number", inputlines);
            fatal_with_filename
            (
                inputname,
                scp,
                i18n("unresolved conflict remaining at line $number")
            );

        case 'U':
            fatal_with_filename
            (
                inputname,
                0,
                i18n("incomplete due to -u option being used")
            );

        default:
            scp = sub_context_new();
            sub_var_set_long(scp, "Number", inputlines);
            fatal_with_filename
            (
                inputname,
                scp,
                i18n("unknown line $number read")
            );
        }
    }
    explain_fclose_or_die(ip);
    explain_fclose_or_die(op);
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        explain_output_error("[%ld lines read, %ld lines written]",
            inputlines, outputlines);
#endif
}


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