//
// tardy - a tar post-processor
// Copyright (C) 1991-1995, 1998-2002, 2008-2011 Peter Miller
//
// 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 <libtardy/ac/ctype.h>
#include <libtardy/ac/stdarg.h>
#include <libtardy/ac/stdio.h>
#include <libtardy/ac/stdlib.h>
#include <libtardy/ac/string.h>
#include <libexplain/ferror.h>
#include <libexplain/fflush.h>
#include <libexplain/output.h>
#include <libexplain/program_name.h>
#include <libexplain/strdup.h>

#include <libtardy/roff.h>
#include <libtardy/trace.h>
#include <libtardy/version_stmp.h>


roff::~roff()
{
    br();
    explain_fflush_or_die(stdout);
}


roff::roff() :
    ocol(0),
    icol(0),
    fill(true),
    in(0),
    in_base(0),
    ll(79),
    roff_line(0),
    roff_file(0),
    TP_line(0)
{
    ds_guts("n)", explain_program_name_get());
    ds_guts("v)", version_stamp());
    ds_guts("Y)", copyright_years());
    ds_guts("lq", "\"");
    ds_guts("rq", "\"");
    ds_guts("co", "(C)");
    ds_guts("bu", "*");
    lf(0, 0);
}


#define PAIR(a, b) ((a) * 256 + (b))


static const char *cr[] =
{
    "\\*(n) version \\*(v)",
    ".br",
    "Copyright (C) \\*(Y) Peter Miller",
    "",
    "The \\*(n) program comes with ABSOLUTELY NO WARRANTY;",
    "for details use the '\\*(n) -VERSion License' command.",
    "The \\*(n) program is free software, and you are welcome to",
    "redistribute it under certain conditions;",
    "for details use the '\\*(n) -VERSion License' command.",
};

static const char *au[] =
{
    ".nf",
    "Peter Miller   EMail: pmiller@opensource.org.au",
    "/\\e/\\e*    WWW: http://miller.emu.id.au/pmiller/",
    ".fi",
};

static const char *so_o__rules[] =
{
#include <man/man1/o__rules.h>
};

static const char *so_o_help[] =
{
#include <man/man1/o_help.h>
};

static const char *so_z_cr[] =
{
    ".SH COPYRIGHT",
    ".so cr",
    ".SH AUTHOR",
    ".so au",
};

static const char *so_z_exit[] =
{
#include <man/man1/z_exit.h>
};


struct so_list_ty
{
    const char *name;
    const char **text;
    size_t length;
};

static so_list_ty so_list[] =
{
    { "man/man1/o__rules.so", so_o__rules, SIZEOF(so_o__rules) },
    { "man/man1/o_help.so", so_o_help, SIZEOF(so_o_help) },
    { "man/man1/z_cr.so", so_z_cr, SIZEOF(so_z_cr) },
    { "man/man1/z_exit.so", so_z_exit, SIZEOF(so_z_exit) },
    { "man/man1/z_name.so", 0, 0 },
    { "etc/version.so", 0, 0 },
    { "cr", cr, SIZEOF(cr), },
    { "au", au, SIZEOF(au), },
};


static inline bool
safe_isprint(char c)
{
    return isprint((unsigned char)c);
}


void
roff::emit(char c)
{
    switch (c)
    {
    case ' ':
        icol++;
        break;

    case '\t':
        icol = ((icol / 8) + 1) * 8;
        break;

    case '\n':
        putchar('\n');
        icol = 0;
        ocol = 0;
        break;

    default:
        if (!safe_isprint(c))
            break;
        while (((ocol / 8) + 1) * 8 <= icol && ocol + 1 < icol)
        {
            putchar('\t');
            ocol = ((ocol / 8) + 1) * 8;
        }
        while (ocol < icol)
        {
            putchar(' ');
            ++ocol;
        }
        putchar(c);
        ++icol;
        ++ocol;
        break;
    }
    explain_ferror_or_die(stdout);
}


void
roff::emit_word(const char *buf, size_t len)
{
    if (len <= 0)
        return;

    //
    // if this line is not yet indented, indent it
    //
    if (!ocol && !icol)
        icol = in;

    //
    // if there is already something on this line
    // and we are in "fill" mode
    // and this word would cause it to overflow
    // then wrap the line
    //
    if (ocol && fill && icol + len >= ll)
    {
        emit('\n');
        icol = in;
    }
    if (ocol)
        emit(' ');
    while (len-- > 0)
        emit(*buf++);
}


void
roff::br(void)
{
    if (ocol)
        emit('\n');
}


void
roff::sp(void)
{
    br();
    emit('\n');
}


void
roff::interpret_line_of_words(const char *line)
{
    //
    // if not filling,
    // pump the line out literrally.
    //
    if (!fill)
    {
        if (!ocol && !icol)
            icol = in;
        while (*line)
            emit(*line++);
        emit('\n');
        return;
    }

    //
    // in fill mode, a blank line means
    // finish the paragraph and emit a blank line
    //
    if (!*line)
    {
        sp();
        return;
    }

    //
    // break the line into space-separated words
    // and emit each individually
    //
    while (*line)
    {
        while (isspace(*line))
            ++line;
        if (!*line)
            break;
        const char *start = line;
        while (*line && !isspace(*line))
            ++line;
        emit_word(start, line - start);

        //
        // extra space at end of sentences
        //
        if
        (
            (line[-1] == '.' || line[-1] == '?')
        &&
            (
                !line[0]
            ||
                (
                    line[0] == ' '
                &&
                    (!line[1] || line[1] == ' ')
                )
            )
        )
            emit(' ');
    }
}


void
roff::error(const char *s, ...)
{
    va_list ap;
    va_start(ap, s);
    char buffer[1000];
    vsnprintf(buffer, sizeof(buffer), s, ap);
    va_end(ap);

#if 0
    br();
    if (roff_file)
        emit_word(roff_file, strlen(roff_file));
    if (roff_line)
    {
        char line[20];
        snprintf(line, sizeof(line), "%ld", roff_line);
        emit_word(line, strlen(line));
    }
    interpret_line_of_words(buffer);
    br();
#else
    explain_output_error_and_die
    (
        "%s: %ld: %s",
        (roff_file ? roff_file : "(noname)"),
        roff_line,
        buffer
    );
#endif
}


void
roff::get_name(const char **lp, char *name)
{
    const char *line = *lp;
    if (*line == '(')
    {
        ++line;
        if (*line)
        {
            name[0] = *line++;
            if (*line)
            {
                name[1] = *line++;
                name[2] = 0;
            }
            else
                name[1] = 0;
        }
        else
            name[0] = 0;
    }
    else if (*line == '[')
    {
        ++line;
        int n = 0;
        for (;;)
        {
            char c = *line;
            if (!c)
                break;
            ++line;
            if (c == ']')
                break;
            if (n < 3)
                name[n++] = c;
        }
        name[n] = 0;
    }
    else if (*line)
    {
        name[0] = *line++;
        name[1] = 0;
    }
    else
        name[0] = 0;
    *lp = line;
}


const char *
roff::string_find(const char *name)
{
    string_registers_t::iterator it = string_registers.find(rcstring(name));
    if (it == string_registers.end())
        return 0;
    return (*it).second.c_str();
}


const char *
roff::numreg_find(const char *)
{
    return 0;
}


void
roff::prepro(char *buffer, const char *line)
{
    const char *value;
    char    name[4];

    char *bp = buffer;
    while (*line)
    {
        int c = *line++;
        if (c != '\\')
        {
            *bp++ = c;
            continue;
        }
        c = *line++;
        if (!c)
        {
            error("can't do escaped end-of-line");
            break;
        }
        switch (c)
        {
        default:
            error("unknown \\%c inline directive", c);
            break;

        case '%':
            // word break info
            break;

        case '*':
            // inline string
            get_name(&line, name);
            value = string_find(name);
            if (value)
            {
                while (*value)
                    *bp++ = *value++;
            }
            break;

        case 'n':
            // inline number register
            get_name(&line, name);
            value = numreg_find(name);
            if (value)
            {
                while (*value)
                    *bp++ = *value++;
            }
            break;

        case 'e':
        case '\\':
            *bp++ = '\\';
            break;

        case '-':
            *bp++ = '-';
            break;

        case 'f':
            // ignore font directives
            get_name(&line, name);
            break;

        case '&':
        case '|':
            // ignore weird space directives
            break;

        case '(':
        case '[':
            // special characters
            // (not really strings)
            --line;
            get_name(&line, name);
            value = string_find(name);
            if (value)
            {
                while (*value)
                    *bp++ = *value++;
            }
            break;
        }
    }
    *bp = 0;
}


void
roff::interpret_text(const char *line)
{
    char buffer[1000];
    prepro(buffer, line);
    interpret_line_of_words(buffer);
    if (TP_line)
    {
        if (icol >= 15)
            br();
        else
            icol = 15;
        TP_line = 0;
        in = in_base + 8;
    }
}


void
roff::sub(char *buffer, int argc, const char **argv)
{
    int     j;
    char    *bp;
    long    len;

    bp = buffer;
    for (j = 0; j < argc; ++j)
    {
        len = strlen(argv[j]);
        if (j)
            *bp++ = ' ';
        memcpy(bp, argv[j], len);
        bp += len;
    }
    *bp = 0;
}


void
roff::interpret_text_args(int argc, const char **argv)
{
    char buffer[1000];
    sub(buffer, argc, argv);
    interpret_text(buffer);
}


void
roff::concat_text_args(int argc, const char **argv)
{
    char buffer[1000];
    char *bp = buffer;
    for (int j = 0; j < argc; ++j)
    {
        size_t len = strlen(argv[j]);
        if ((bp - buffer) + len + 1 >= sizeof(buffer))
            break;
        memcpy(bp, argv[j], len);
        bp += len;
    }
    *bp = 0;
    interpret_text(buffer);
}


void
roff::so(int argc, const char **argv)
{
    so_list_ty      *sop;

    if (argc != 1)
    {
        error(".so requires one argument");
        return;
    }
    for (sop = so_list; sop < ENDOF(so_list); ++sop)
    {
        if (!strcmp(sop->name, argv[0]))
        {
            interpret(sop->text, sop->length);
            return;
        }
    }
    error("\".so %s\" not known", argv[0]);
}


void
roff::lf(int argc, const char **argv)
{
    if (roff_file)
        free(roff_file);
    if (argc >= 1)
        roff_line = atol(argv[0]) - 1;
    else
        roff_line = 0;
    if (argc >= 2)
        roff_file = explain_strdup_or_die(argv[1]);
    else
        roff_file = 0;
}


void
roff::ds_guts(const rcstring &name, const rcstring &value)
{
    string_registers_t::iterator it = string_registers.find(name);
    if (it == string_registers.end())
        string_registers.insert(string_registers_t::value_type(name, value));
    else
        (*it).second = rcstring(value);
}


void
roff::ds(int argc, const char **argv)
{
    char    buf1[1000];
    char    buf2[1000];

    if (!argc)
        return;
    sub(buf1, argc - 1, argv + 1);
    prepro(buf2, buf1);
    ds_guts(argv[0], buf2);
}


void
roff::dot_in(int argc, const char **argv)
{
    if (argc < 1)
        return;
    switch (argv[0][0])
    {
    case '-':
        in -= atoi(argv[0] + 1);
        break;

    case '+':
        in += atoi(argv[0] + 1);
        break;

    default:
        in = atoi(argv[0] + 1);
        break;
    }
    if (in < 0)
        in = 0;
}


void
roff::dot_br(int, const char **)
{
    br();
}


void
roff::dot_ce(int argc, const char **argv)
{
    br();
    interpret_text_args(argc, argv);
    br();
}


void
roff::dot_fi(int, const char **)
{
    br();
    fill = 1;
}


void
roff::dot_if(int argc, const char **argv)
{
    if (argc > 1 && 0 == strcmp(argv[0], "n"))
        interpret_text_args(argc - 1, argv + 1);
}


void
roff::dot_ip(int, const char **)
{
    in = in_base;
    sp();
    emit(' ');
    emit(' ');
}


void
roff::dot_nf(int, const char **)
{
    br();
    fill = 0;
}


void
roff::dot_pp(int, const char **)
{
    in = in_base;
    sp();
}


void
roff::dot_r(int, const char **)
{
    const char *cp = string_find("R)");
    if (!cp)
        cp = "";
    if (strcmp(cp, "no") != 0)
    {
        static const char *macro[] =
        {
            ".PP",
            "See also",
            ".IR \\*(n) (1)",
            "for options common to all \\*(n) commands.",
        };

        interpret(macro, SIZEOF(macro));
    }
}


void
roff::dot_re(int, const char **)
{
    in_base = 8;
    in = 8;
    br();
}


void
roff::dot_rs(int, const char **)
{
    in_base = 16;
    in = 16;
    br();
}


void
roff::dot_sh(int argc, const char **argv)
{
    in = 0;
    sp();
    interpret_text_args(argc, argv);
    br();
    in_base = 8;
    in = 8;
}


void
roff::dot_sp(int, const char **)
{
    sp();
}


void
roff::dot_tp(int, const char **)
{
    in = in_base;
    sp();
    TP_line = 1;
}


void
roff::dot_ignore(int, const char **)
{
}


void
roff::interpret_control(const char *line)
{
    struct directive_t
    {
        const char *name;
        void (roff::*perform)(int argc, const char **argv);
    };

    static const directive_t directive[] =
    {
        { "\\\"", &roff::dot_ignore },
        { "\"", &roff::dot_ignore },
        { "B", &roff::concat_text_args },
        { "BI", &roff::concat_text_args },
        { "BR", &roff::concat_text_args },
        { "br", &roff::dot_br },
        { "ce", &roff::dot_ce },
        { "ds", &roff::ds },
        { "fi", &roff::dot_fi },
        { "I", &roff::concat_text_args },
        { "IB", &roff::concat_text_args },
        { "if", &roff::dot_if },
        { "in", &roff::dot_in },
        { "IP", &roff::dot_ip },
        { "IR", &roff::concat_text_args },
        { "lf", &roff::lf },
        { "ne", &roff::dot_ignore },
        { "nf", &roff::dot_nf },
        { "PP", &roff::dot_pp },
        { "R", &roff::concat_text_args },
        { "r)", &roff::dot_r },
        { "RB", &roff::concat_text_args },
        { "RE", &roff::dot_re },
        { "RI", &roff::concat_text_args },
        { "RS", &roff::dot_rs },
        { "SH", &roff::dot_sh },
        { "so", &roff::so },
        { "sp", &roff::dot_sp },
        { "ta", &roff::dot_ignore },
        { "TH", &roff::dot_ignore },
        { "TP", &roff::dot_tp },
        { "XX", &roff::dot_ignore },
    };

    //
    // find the directive name
    //
    line++;
    for (;;)
    {
        unsigned char c = *line;
        if (c == 0)
            return;
        if (!isspace(c))
            break;
        ++line;
    }
    char dot_name[10];
    char *dnp = dot_name;
    for (;;)
    {
        unsigned char c = *line;
        if (!c)
            break;
        ++line;
        if (isspace(c))
            break;
        if (dnp < dot_name + sizeof(dot_name) - 1)
            *dnp++ = c;
    }
    *dnp = 0;

    //
    // break the line into space-separated arguments
    //
    int argc = 0;
    char temp[1000];
    char *cp1 = temp;
    const char *argv[20];
    while (argc < (int)SIZEOF(argv))
    {
        while (isspace(*line))
            ++line;
        if (!*line)
            break;
        argv[argc++] = cp1;
        int quoting = 0;
        while (*line)
        {
            if (*line == '"')
            {
                quoting = !quoting;
                ++line;
                continue;
            }
            if (!quoting && isspace(*line))
                break;
            *cp1++ = *line++;
        }
        *cp1++ = 0;
        if (!*line)
            break;
    }

    //
    // now do something with it
    //
    for (const directive_t *dp = directive; dp < ENDOF(directive); ++dp)
    {
        if (0 == strcmp(dot_name, dp->name))
        {
            (this->*dp->perform)(argc, argv);
            return;
        }
    }
    error("formatting directive \".%s\" unknown", dot_name);
}


void
roff::interpret(const char **text, size_t text_size)
{
    //
    // save position
    //
    trace(("interpret()\n{\n"));
    long hold_line = roff_line;
    char *hold_file = roff_file ? explain_strdup_or_die(roff_file) : (char *)0;

    //
    // interpret the text
    //
    for (size_t j = 0; j < text_size; ++j)
    {
        const char *s = text[j];
        if (*s == '.' || *s == '\'')
            interpret_control(s);
        else
            interpret_text(s);
        ++roff_line;
        explain_ferror_or_die(stdout);
    }

    //
    // restore position
    //
    if (roff_file)
        free(roff_file);
    roff_line = hold_line;
    roff_file = hold_file;
    trace(("}\n"));
}
