/*
 * 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/ctype.h>
#include <common/ac/errno.h>
#include <common/ac/stdio.h>
#include <common/ac/string.h>
#include <common/ac/unistd.h>
#include <libexplain/fclose.h>
#include <libexplain/fopen.h>
#include <libexplain/output.h>
#include <libexplain/pathconf.h>
#include <libexplain/rename.h>
#include <libexplain/strndup.h>
#include <libexplain/unlink.h>

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

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


int
history_file_exists(void)
{
    FILE            *fp;

    fp = fopen(sc.historyname, "rb");
    if (fp)
    {
        explain_fclose_or_die(fp);
        return 1;
    }
    if (errno != ENOENT)
    {
        explain_output_error_and_die("%s", explain_fopen(sc.historyname, "rb"));
    }
    return 0;
}


/*
 * Routine used to open the history file and read the first line.
 * This saves the information about the first and last edit numbers,
 * and the position of the position table in variables.
 *
 * 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.
 */

FILE *
openhistoryfile(int mode)
{
    FILE            *fp;                /* file handle for history file */
    char            *cp;                /* current line of file */
    const char      *flags;

    flags = (mode == OHF_READ ? "rb" : "r+b");
    fp = explain_fopen_or_die(sc.historyname, flags);
    cp = get_a_line(fp, 0L, T_HEADER, sc.historyname);
    if ((sc.linelen + 2) != HEADERLINELENGTH)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("header line has wrong length")
        );
    }
    cp = getnumber(cp, &sc.firstedit);
    cp = getnumber(cp, &sc.lastedit);
    cp = getnumber(cp, &sc.lastfile);
    cp = getnumber(cp, &sc.tablepos);
    if (cp == NULL)
    {
        fatal_with_filename(sc.historyname, 0, i18n("bad header line"));
    }
    if (sc.firstedit < 0 || sc.lastedit < sc.firstedit || sc.tablepos <= 0)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("bad edit values in header line")
        );
    }
    return fp;
}


/*
 * Find an edit number in the history file given the edit name string.
 * This string is either a straight number, a straight edit name, or a
 * combination of the two.  A combination is indicated by an imbedded plus
 * or minus sign.  Examples:  "45", "good2", "nice+5".  The resulting
 * edit number is returned if it is valid.
 */

long
findeditnumber(FILE *fp, const char *editname)
{
    const char      *cp;        /* current character */
    const char      *name;      /* edit name */
    const char      *endname;   /* end of name */
    const char      *testname;  /* name to test with */
    long            edit;       /* resulting edit number */
    long            reledit;    /* relative edit number */
    long            curedit;    /* current edit number */
    long            tempedit;   /* edit number just read */
    long            temppos;    /* position number just read */
    short           isneg;      /* 1 if negative number given */
    sub_context_ty  *scp;

    if (!editname)
        return sc.lastedit;
    edit = 0;
    reledit = 0;
    isneg = 0;

    /*
     * First, see if there is a name present.  If so, remember the beginning
     * of the name and skip over it to where a number can start.
     */
    name = NULL;
    endname = 0;
    cp = editname;
    if ((*cp != '-') && (*cp != '+') && !isdigit((unsigned char)*cp))
    {
        name = cp;
        while (*cp && (*cp != '-') && (*cp != '+'))
            cp++;
        endname = cp;
    }

    /*
     * Now look for an edit number offset.
     */
    if ((*cp == '+') || (*cp == '-'))
        isneg = (*cp++ == '-');
    while (isdigit((unsigned char)*cp))
        reledit = reledit * 10 + (*cp++ - '0');
    if (*cp || (reledit < 0))
    {
        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_charstar(scp, "Name", editname);
        fatal_intl
        (
            scp,
            i18n("bad edit name \"$name\" given for module \"$module\"")
        );
    }
    if (isneg)
        reledit = -reledit;

    /*
     * If a name was specified, then search the history file for that name.
     * Remember the corresponding edit number if found.
     * Handle the magic 'oldest' and 'newest' edit names specially.
     */
    if (name)
    {
        char *n = explain_strndup_or_die(name, endname - name);
        if (strcasecmp(n, OLDESTNAME) == 0)
        {
            edit = sc.firstedit;
            goto foundedit;
        }
        if (strcasecmp(n, NEWESTNAME) == 0)
        {
            edit = sc.lastedit;
            goto foundedit;
        }
#if 0
        if (fc.verbosity > VERBOSE_DEFAULT)
            explain_output_error("[Searching for edit name \"%s\"]", editname);
#endif
        curedit = sc.lastedit + 1;
        seekf(fp, sc.tablepos);
        while (edit == 0)
        {
            char            *lp;

            if (--curedit < sc.firstedit)
            {
                fclose(fp);
                fp = 0;
                scp = sub_context_new();
                sub_var_set_charstar(scp, "Module", sc.modulename);
                sub_var_set_charstar(scp, "Name", editname);
                fatal_intl
                (
                    scp,
                    i18n
                    (
                        "edit name \"$name\" does not exist for "
                        "module \"$name\""
                    )
                );
            }
            lp = get_a_line(fp, NOSEEK, T_POSITION, sc.historyname);
            lp[sc.linelen - 1] = '\0';
            lp = getnumber(lp, &tempedit);
            lp = getnumber(lp, &temppos);
            if ((lp == NULL) || (tempedit != curedit))
            {
                scp = sub_context_new();
                sub_var_set_long(scp, "Number", curedit);
                fatal_with_filename
                (
                    sc.historyname,
                    scp,
                    i18n("bad position line for edit $number")
                );
            }

            /*
             * Search the line for the edit name
             */
            while (*lp)
            {
                while (isspace((unsigned char)*lp))
                    lp++;
                testname = lp;
                while (*lp && !isspace((unsigned char)*lp))
                    lp++;
                if (*lp)
                    *lp++ = '\0';
                if (strcasecmp(testname, n) == 0)
                    edit = curedit;
            }
        }
    }

    /*
     * We either found the name, or else we just had a number.
     * Combine the two values, and complain if out of range.
     * When given just a number, negative means backwards from last one.
     */
foundedit:
    if ((edit == 0) && (reledit <= 0))
        edit = sc.lastedit;
    edit += reledit;
    if ((edit < sc.firstedit) || (edit > sc.lastedit))
    {
        fclose(fp);
        fp = 0;

        scp = sub_context_new();
        sub_var_set_charstar(scp, "Module", sc.modulename);
        sub_var_set_long(scp, "Number", edit);
        sub_var_set_long(scp, "First", sc.firstedit);
        sub_var_set_long(scp, "Last", sc.lastedit);
        fatal_intl
        (
            scp,
            i18n
            (
                "edit number $number for module \"$module\" is not in the "
                "range $first to $last"
            )
        );
    }
    return edit;
}


/*
 * Read in the position table from the history file and save it into memory.
 * This reserves the first entry of the table for a possible new entry.
 * The history file was already opened and the header line was read.
 * Returns a pointer to the beginning of the table.
 */
POS *
readpostable(FILE *fp)
{
    char            *cp;        /* current line of file */
    POS             *pp;        /* current position table */
    POS             *postable;  /* beginning of position table */
    long            edit;       /* current edit number */
    long            tempedit;   /* temporary edit number */
    long            temppos;    /* temporary position */
    long            eofpos;     /* end of file position */
    int             len;        /* length of string */
    sub_context_ty  *scp;

#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        error("[Saving old position table]");
#endif
    pp =
        (POS *)
        cm_alloc_and_check(sizeof(POS) * (sc.lastedit - sc.firstedit + 2));
    postable = pp;
    pp->p_pos = -1;
    pp->p_names = NULL;
    pp++;

    /*
     * Read the position table lines from the file and save the positions.
     * Save the names also if there are any.
     */
    seekf(fp, sc.tablepos);
    for (edit = sc.lastedit; edit >= sc.firstedit; edit--)
    {
        cp = get_a_line(fp, NOSEEK, T_POSITION, sc.historyname);
        cp = getnumber(cp, &tempedit);
        cp = getnumber(cp, &temppos);
        if (cp == NULL)
        {
            scp = sub_context_new();
            sub_var_set_long(scp, "Number", edit);
            fatal_with_filename
            (
                sc.historyname,
                scp,
                i18n("bad numbers in position entry for edit $number")
            );
        }
        if (tempedit != edit)
        {
            scp = sub_context_new();
            sub_var_set_long(scp, "Number", edit);
            fatal_with_filename
            (
                sc.historyname,
                scp,
                i18n("wrong edit number in position entry for edit $number")
            );
        }
        pp->p_pos = temppos;
        pp->p_names = NULL;
        while (*cp == ' ')
            cp++;
        if (*cp != '\n')
        {
            /* have a name so save it */
            len = strlen(cp);
            cp[len - 1] = '\0';
            pp->p_names = allocstr((unsigned long) len);
            strcpy(pp->p_names, cp);
        }
        pp++;
    }

    /*
     * That should have been all of the position lines.
     * Now verify that the next line is the end of file line.
     */
    eofpos = ftell(fp);
    cp = get_a_line(fp, NOSEEK, T_EOF, sc.historyname);
    cp = getnumber(cp, &temppos);
    if (cp == NULL)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("no position in end of file line")
        );
    }
    if (eofpos != temppos)
    {
        scp = sub_context_new();
        sub_var_set_long(scp, "Number", eofpos);
        fatal_with_filename
        (
            sc.historyname,
            scp,
            i18n("wrong position $number in end of file line")
        );
    }
    return postable;
}


/*
 * Find the position of the beginning of an edit in the history file,
 * position to it, and verify that it is correct.  Then if infoline is
 * non-NULL, read the first remark line for the edit (which is information
 * about the edit), and return that into infoline.  The file position is
 * left ready to read the remainder of the edit sequence.
 */
void
startedit(FILE *fp, long num, char *infoline)
{
    char            *cp;        /* current line of file */
    long            editnumber; /* current edit number read */
    long            editpos;    /* position of beginning of edit */
    long            temp;       /* temporary value read */

    cp = get_a_line(fp, sc.tablepos, T_POSITION, sc.historyname);
    cp = getnumber(cp, &editnumber);
    if (cp == NULL)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("no edit number in start of position table")
        );
    }
    if (editnumber != sc.lastedit)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("wrong edit number at start of position table")
        );
    }
    while (editnumber != num)
    {
        editnumber--;
        cp = get_a_line(fp, NOSEEK, T_POSITION, sc.historyname);
        cp = getnumber(cp, &temp);
        if (cp == NULL)
        {
            fatal_with_filename
            (
                sc.historyname,
                0,
                i18n("no edit number in position entry")
            );
        }
        if (temp != editnumber)
        {
            fatal_with_filename
            (
                sc.historyname,
                0,
                i18n("non-decreasing position table entry")
            );
        }
    }
    cp = getnumber(cp, &editpos);
    if (cp == NULL)
        fatal_with_filename(sc.historyname, 0, i18n("no edit position"));
    cp = get_a_line(fp, editpos, T_BEGINEDIT, sc.historyname);
    cp = getnumber(cp, &temp);
    if (temp != editnumber)
    {
        fatal_with_filename
        (
            sc.historyname,
            0,
            i18n("not at beginning of correct edit")
        );
    }
    if (infoline == NULL)
        return;
    cp = get_a_line(fp, NOSEEK, T_REMARK, sc.historyname);
    strncpy(infoline, cp, MAX_INFO);
    infoline[MAX_INFO-1] = '\0';
    infoline[MAX_INFO-2] = '\0';
}


/*
 * Open the source file and verify that it has the correct version number
 * stored in the first line of it.  Further reads from the file will then
 * read in the real lines of the 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.
 */

FILE *
opensourcefile(void)
{
    FILE            *fp;        /* file handle for history file */
    char            *cp;        /* first line of file */
    long            tempedit;   /* edit number read from first line */

    fp = explain_fopen_or_die(sc.sourcename, "r");
    cp = get_a_line(fp, NOSEEK, T_BEGINEDIT, sc.sourcename);
    if ((getnumber(cp, &tempedit) == NULL))
    {
        fatal_with_filename(sc.sourcename, 0, i18n("no number in first line"));
    }
    if (tempedit != sc.lastedit)
    {
        sub_context_ty  *scp;

        scp = sub_context_new();
        sub_var_set_long(scp, "Number1", tempedit);
        sub_var_set_long(scp, "Number2", sc.lastedit);
        sub_var_set_charstar(scp, "Module", sc.modulename);
        fatal_with_filename
        (
            sc.sourcename,
            scp,
            i18n("edit $number1 instead of $number2 for module \"$module\"")
        );
    }
    return fp;
}


/*
 * Rename the temporary file with the EXT_NEW extension to the file with
 * the specified extension, deleting the destination file.  This is used
 * for atomically changing the history file (with extension EXT_HISTORY)
 * or the latest source file (with extension EXT_SOURCE).  This uses the
 * temporary file name with the extension EXT_OLD so that at all times
 * either the old or the new file is in existence.  Returns nonzero if
 * the rename failed.  Breaks should be off during this call.
 */
int
renamefiles(const char *extension)
{
    string_ty       *destname;  /* name of destination file */
    string_ty       *newname;   /* name of current new file */
    string_ty       *oldname;   /* name of old file */
    int             result;

    result = -1;
#if 0
    if (fc.verbosity > VERBOSE_DEFAULT)
        error("[Renaming files and deleting old version source]");
#endif
    destname = str_format("%s%s", sc.basename, extension);
    newname = str_format("%s%s", sc.basename, EXT_NEW);
    oldname = str_format("%s%s", sc.basename, EXT_OLD);
    unlink(oldname->str_text);
    if (explain_rename_on_error(destname->str_text, oldname->str_text) < 0)
        goto done;
    if (explain_rename_on_error(newname->str_text, destname->str_text) < 0)
    {
        if (explain_rename_on_error(oldname->str_text, destname->str_text))
        {
            /* silence gcc warning */
        }
        goto done;
    }
    explain_unlink_on_error(oldname->str_text);
    result = 0;
done:
    str_free(destname);
    str_free(newname);
    str_free(oldname);
    return result;
}


/*
 * Make sure that a file name is new.  If not, ask to see if we can
 * overwrite it.  Returns nonzero if no.  If the forcewrite flag is
 * specified, then skip this check.  If the nowrite flag is set, then
 * never overwrite it.
 */
int
checknewfile(const char *name)
{
    if (sc.forcewriteflag || (name == NULL))
        return 0;
    if (sc.nowriteflag)
        return 1;
    if (access(name, F_OK) < 0)
        return 0;
    printf("Output file \"%s\" already exists.  Ok to overwrite it? ", name);
    return queryuser();
}


/*
 * Read a yes-no response from the user after a question has been asked.
 * Returns nonzero on a NO or null response.
 */
int
queryuser(void)
{
    int             ch;         /* current input character */
    int             result;     /* result of question */

    fflush(stdout);
    result = -1;
    for (;;)
    {
        ch = getchar();
        if (ch == '\n')
            break;
        if ((result >= 0) || isspace(ch))
            continue;
        result = ((ch == 'y') || (ch == 'Y'));
    }
    return (result <= 0);
}


/*
 * Routine to read a single line from the edit file, and verify that
 * its type is as desired.  If type is negative, then it is not checked
 * and the whole line is returned.  If type is nonnegative, then it is
 * checked and skipped over, and NULL is returned if it is wrong.
 * Length of the line is returned in the global variable linelen.
 * If type is text, then the line is returned fully allocated.
 */

char *
get_a_line(FILE *fp, long seekpos, int type, const char *filename)
{
    char            *cp;
    long            pos;
    int             bin;                /* IGNORED: edit file should be OK */

    if (seekpos != NOSEEK)
        seekf(fp, seekpos);
    pos = ftell(fp);
    cp = readlinef(fp, &sc.linelen, (type == T_TEXT), filename, &bin);
    if (cp == NULL)
        fatal_with_filename(filename, 0, i18n("premature end of file"));
    if (sc.linelen == 2)
    {
        /* pad out empty text lines */
        sc.linelen = 3;
        cp[1] = ' ';
        cp[2] = '\n';
        cp[3] = '\0';
    }
    if (fc.debugflag)
        printf("%5ld: %s", pos, cp);
    if (type < 0)
        return cp;
    sc.linelen -= 2;
    if ((sc.linelen <= 0) || (type != cp[0]) || (cp[1] != ' '))
        return NULL;
    return &cp[2];
}


/*
 * Routine to read a number from a string preceeded by optional spaces and
 * followed by a space or an end of line.  If the scanning is successful,
 * it is indirectly returned and the next position in the string is returned.
 * If the number was bad or if no string is given, NULL is returned.
 */
char *
getnumber(char *cp, long *value)
{
    int             nonum;              /* 1 if no number detected */
    int             isneg;              /* 1 if number is negative */

    if (cp == NULL)
        return NULL;
    nonum = 1;
    isneg = 0;
    *value = 0;
    while (isspace((unsigned char)*cp))
        cp++;
    isneg = (*cp == '-');
    if (isneg)
        cp++;
    while (isdigit((unsigned char)*cp))
    {
        *value = (*value * 10) + *cp++ - '0';
        if (*value < 0)
            return NULL;
        nonum = 0;
    }
    if (isneg)
        *value = -*value;
    if (nonum || (*cp && !isspace((unsigned char)*cp)))
        cp = NULL;
    return cp;
}


unsigned
pathconf_name_max(const char *path)
{
    long            result;
    const char      *actual_path;

#ifdef _PC_NAME_MAX
    /*
     * The pathconf system call may return -1 without setting errno
     * on some systems.  Usually this means "I don't know".
     * Pre-set errno with a suitable "I don't know" value to cope
     * with this behaviour.
     */
    errno = EINVAL;
    actual_path = path;
    result = pathconf(actual_path, _PC_NAME_MAX);
    if
    (
        result < 0
    &&
        (
            errno == EINVAL
        ||
            errno == ENOSYS
#ifdef EOPNOTSUPP
        ||
            errno == EOPNOTSUPP
#endif
        )
    )
    {
        /*
         * probably NFS mounted
         * (default to 14 if root is also NFS mounted)
         */
        errno = EINVAL;
        actual_path = "/";
        result = pathconf(actual_path, _PC_NAME_MAX);
        if
        (
            result < 0
        &&
            (
                errno == EINVAL
            ||
                errno == ENOSYS
#ifdef EOPNOTSUPP
            ||
                errno == EOPNOTSUPP
#endif
            )
        )
        {
#if HAVE_LONG_FILE_NAMES
            result = 255;
#else
            result = 14;
#endif
        }
    }
    if (result < 0)
    {
        explain_output_error_and_die
        (
            "%s",
            explain_pathconf(actual_path, _PC_NAME_MAX)
        );
    }
#else /* _PC_NAME_MAX */
#ifdef DOS
    result = 12;
#else
#if HAVE_LONG_FILE_NAMES
    result = 255;
#else
    result = 14;
#endif
#endif
#endif /* _PC_NAME_MAX */
    return result;
}


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