/*
 * main.cpp: Main program for spigot.
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>

#include <string>

#include "spigot.h"
#include "funcs.h"
#include "expr.h"
#include "error.h"
#include "baseout.h"
#include "cfracout.h"
#include "version.h"

#ifdef _WIN32
#  include <windows.h>
#else
#  if HAVE_UNISTD_H
#    include <unistd.h>
#    if HAVE_NCURSES_H
#      include <ncurses.h>
#      include <term.h>
#    elif HAVE_CURSES_H
#      include <curses.h>
#      include <term.h>
#    else
#      define TENTATIVE_OUTPUT_DISABLED
#    endif
#  else
#    define TENTATIVE_OUTPUT_DISABLED
#  endif
#endif

void check_stdout()
{
    fflush(stdout);
    if (ferror(stdout))
        exit(1);
}

#ifndef TENTATIVE_OUTPUT_DISABLED

static std::string current_tentative_output;
static bool tentative_output_enabled = false;

#ifdef _WIN32

/*
 * Windows implementation of colour control for tentative output, via
 * SetConsoleTextAttribute.
 */

static HANDLE console_handle;
static WORD normal_attributes, tentative_attributes;

void setup_tentative_output(bool required)
{
    console_handle = GetStdHandle(STD_OUTPUT_HANDLE);
    if (console_handle == INVALID_HANDLE_VALUE) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output: GetStdHandle returned error code %d\n", (int)GetLastError());
        return;
    }

    CONSOLE_SCREEN_BUFFER_INFO info;
    if (!GetConsoleScreenBufferInfo(console_handle, &info)) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output: GetConsoleScreenBufferInfo returned error code %d\n", (int)GetLastError());
        return;
    }
    normal_attributes = info.wAttributes;
    tentative_attributes = normal_attributes;
    tentative_attributes &= ~(FOREGROUND_BLUE | FOREGROUND_GREEN |
                              FOREGROUND_RED  | FOREGROUND_INTENSITY);
    tentative_attributes |= FOREGROUND_RED;

    tentative_output_enabled = true;
}

void backspace()
{
    fputc('\b', stdout);
}

void print_in_tentative_colour(const char *s)
{
    SetConsoleTextAttribute(console_handle, tentative_attributes);
    fputs(s, stdout);
    fflush(stdout);
    SetConsoleTextAttribute(console_handle, normal_attributes);
}

#else /* _WIN32 */

/*
 * Unix implementation of colour control for tentative output, via
 * ncurses/terminfo to send control sequences to the tty.
 */

static std::string term_backspace, term_tentative_colour, term_normal_colour;

void setup_tentative_output(bool required)
{
    char opname[20];
    char *tstr;

    if (setupterm(NULL, 1, NULL) != OK)
        return;

    /*
     * OS X's clang gives warnings about passing a string literal to
     * tigetstr, because tigetstr is defined to take a char * rather
     * than the more sensible const char *.
     *
     * Of course I don't really believe that tigetstr would _modify_
     * that string, so I could just cast my string literals to char *
     * to prevent the warning. But casts are ugly anyway, and TBH I
     * don't trust C compilers not to sooner or later start giving the
     * same warning even with the cast. So let's do this with a real,
     * writable char string.
     */
    strcpy(opname, "cub1");
    tstr = tigetstr(opname);
    if (tstr == NULL || tstr == (char *)-1) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output: terminal description does not provide 'cub1' backspace operation\n");
        return;
    }
    term_backspace = tstr;
    term_backspace += " ";
    term_backspace += tstr;

    strcpy(opname, "sgr0");
    tstr = tigetstr(opname);
    if (tstr == NULL || tstr == (char *)-1) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output: terminal description does not provide 'sgr0' operation to reset colour settings\n");
        return;
    }
    term_normal_colour = tstr;

    strcpy(opname, "setaf");
    tstr = tigetstr(opname);
    if (tstr == NULL || tstr == (char *)-1) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output: terminal description does not provide 'setaf' operation to set text colour\n");
        return;
    }
#if HAVE_TIPARM
    // Use the nice new tiparm if possible
    tstr = tiparm(tstr, 1);
#else
    // Old versions of tparm were not variadic, so pass the right
    // number of args
    tstr = tparm(tstr, 1, 0, 0, 0, 0, 0, 0, 0, 0);
#endif
    term_tentative_colour = tstr;

    tentative_output_enabled = true;
}

void backspace()
{
    fputs(term_backspace.c_str(), stdout);
}

void print_in_tentative_colour(const char *s)
{
    fputs(term_tentative_colour.c_str(), stdout);
    fputs(s, stdout);
    fputs(term_normal_colour.c_str(), stdout);
}

#endif /* _WIN32 */

void unwrite_tentative_output()
{
    if (tentative_output_enabled) {
        for (size_t i = 0; i < current_tentative_output.size(); ++i)
            backspace();
        check_stdout();
        current_tentative_output = "";
    }
}

void write_tentative_output(const std::string &s)
{
    assert(s.size() > 0);
    if (tentative_output_enabled) {
        if (s == current_tentative_output)
            return;
        unwrite_tentative_output();
        print_in_tentative_colour(s.c_str());
        current_tentative_output = s;
        check_stdout();
    }
}

#endif /* TENTATIVE_OUTPUT_DISABLED */

void write_definite_output(const std::string &s)
{
    assert(s.size() > 0);
#ifndef TENTATIVE_OUTPUT_DISABLED
    unwrite_tentative_output();
#endif
    fputs(s.c_str(), stdout);
    check_stdout();
}

static bool tentative_test = false;
static int tentative_test_digits;

/*
 * We don't produce tentative output at all if the 'digits' parameter
 * returned from get_tentative_output is too small. Otherwise, we'd be
 * forever writing out 10 characters of red text and deleting it, more
 * or less between _any_ pair of digits of real output.
 */
#define MIN_TENTATIVE_DIGITS 3

void write_to_stdout(OutputGenerator *og, bool print_newline)
{
    while (1) {
        std::string out;
        int digits;

        if (og->get_definite_output(out)) {
            if (out.size() > 0) {
                write_definite_output(out);
            } else if (og->get_tentative_output(out, &digits) &&
                       digits >= MIN_TENTATIVE_DIGITS) {
                if (tentative_test && digits >= tentative_test_digits) {
                    write_definite_output("!");
                    write_definite_output(out);
                    break;
                }
#ifndef TENTATIVE_OUTPUT_DISABLED
                write_tentative_output(out);
#endif
            }
        } else {
            break;
        }
    }
    if (print_newline) {
        putchar('\n');
        check_stdout();
    }
}
extern bool set_term_modes; /* for instructing io.cpp */

static void print_help(FILE *fp)
{
    static const char *const help[] = {
"usage: spigot [options] <expression>",
"where: -b BASE             print output in specified number base (2..36)",
"       -B BASE             like -b, but digits > 9 in upper case",
"       -c                  print output as list of continued fraction terms",
"       -l                    print output without line breaks (-c only)",
"       -C                  print output as list of rational convergents",
"       -R                  print output as a rational, if it is rational",
"       -S, -D, -Q, -H      print output as hex bit pattern of IEEE float",
"                             in single, double, quad (128-bit), half (16-bit)",
"       -d PREC             limit precision to at most PREC digits after the",
"                             point, or at most PREC terms / convergents",
"       -w DIGITS           print at least DIGITS digits of the integer part",
"       --printf=FORMAT     format output according to a printf-style floating",
"                             point formatting directive",
"       --rz, --ri          (-d, --printf) round towards / away from zero",
"       --ru, --rd          (-d, --printf) round upwards / downwards",
"       --rn                (-d, --printf) round to nearest, tie-break to even",
"       --rno               (-d, --printf) round to nearest, tie-break to odd",
"       --rnz, --rni, --rnu, --rnd  (-d, --printf) round to nearest, tie-break",
"                             as if --rz, --ri, --ru, --rd",
#ifndef TENTATIVE_OUTPUT_DISABLED
"       --tentative=STATE   enable/disable tentative output (STATE can be 'on'",
"                             'off', or the default 'auto' i.e. only on ttys)",
#endif
#ifdef HAVE_FDS
"       -T                  set terminal devices into raw mode when reading",
"                             input numbers from them",
#endif
"       -n                  suppress trailing newline when output terminates",
" also: spigot --version    report version number and build options",
"       spigot --help       display this help text",
"       spigot --licence    display (MIT) licence text",
    };

    for (size_t i = 0; i < lenof(help); ++i)
        fprintf(fp, "%s\n", help[i]);
}

static void print_licence(FILE *fp)
{
    static const char *const licence[] = {
/* To regenerate the below:
perl -l12 -pe 's/"/\\"/g; s/^/"/; s/$/",/' LICENCE
 */
"spigot is copyright 2007-2014 Simon Tatham. All rights reserved.",
"",
"Permission is hereby granted, free of charge, to any person",
"obtaining a copy of this software and associated documentation files",
"(the \"Software\"), to deal in the Software without restriction,",
"including without limitation the rights to use, copy, modify, merge,",
"publish, distribute, sublicense, and/or sell copies of the Software,",
"and to permit persons to whom the Software is furnished to do so,",
"subject to the following conditions:",
"",
"The above copyright notice and this permission notice shall be",
"included in all copies or substantial portions of the Software.",
"",
"THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,",
"EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF",
"MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND",
"NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS",
"BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN",
"ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN",
"CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE",
"SOFTWARE.",
    };

    for (size_t i = 0; i < lenof(licence); ++i)
        fprintf(fp, "%s\n", licence[i]);
}

static void print_version(FILE *fp)
{
    fprintf(fp, "spigot, %s\n", VER);
    fprintf(fp, "  big integer provider: %s\n", BIGINT_PROVIDER);
    fprintf(fp, "  fd literals (cfracfd:F, baseNfd:F): %s\n",
#ifdef HAVE_FDS
            "supported"
#else
            "not supported"
#endif
        );
}

int main(int argc, char **argv)
{
    bool doing_args = true;
    enum {
        MODE_BASE, MODE_CFRAC, MODE_IEEE, MODE_CONVERGENTS,
        MODE_PRINTF, MODE_RATIONAL
    } outmode = MODE_BASE;
#ifndef TENTATIVE_OUTPUT_DISABLED
    enum {
        ON, OFF, AUTO
    } tentative_output_option = AUTO;
#endif
    RoundingMode rmode = ROUND_TOWARD_ZERO;
    int base = 10;
    int digitlimit = -1, minintdigits = 0;
    bool got_digitlimit = false;
    int ieee_bits = -1;
    bool base_uppercase = false;
    bool oneline = false;
    bool print_newline = true;
    bool printf_nibble_mode = false;
    Spigot *sp = NULL;
    int printf_width = 0, printf_precision = 0;
    int printf_flags = 0, printf_specifier = 0;

    while (--argc > 0) {
        const char *p = *++argv;

        if (doing_args && p[0] == '-') {
            if (!strcmp(p, "--")) {
                doing_args = false;
            } else if (p[1] == '-') {
                /*
                 * GNU-style long option.
                 */
                p += 2;
                if (!strcmp(p, "rz"))
                    rmode = ROUND_TOWARD_ZERO;
                else if (!strcmp(p, "ri"))
                    rmode = ROUND_AWAY_FROM_ZERO;
                else if (!strcmp(p, "rn") || !strcmp(p, "rne"))
                    rmode = ROUND_TO_NEAREST_EVEN;
                else if (!strcmp(p, "rno"))
                    rmode = ROUND_TO_NEAREST_ODD;
                else if (!strcmp(p, "rnz"))
                    rmode = ROUND_TO_NEAREST_TOWARD_ZERO;
                else if (!strcmp(p, "rni"))
                    rmode = ROUND_TO_NEAREST_AWAY_FROM_ZERO;
                else if (!strcmp(p, "ru") || !strcmp(p, "rp"))
                    rmode = ROUND_UP;
                else if (!strcmp(p, "rd") || !strcmp(p, "rm"))
                    rmode = ROUND_DOWN;
                else if (!strcmp(p, "rnu") || !strcmp(p, "rnp"))
                    rmode = ROUND_TO_NEAREST_UP;
                else if (!strcmp(p, "rnd") || !strcmp(p, "rnm"))
                    rmode = ROUND_TO_NEAREST_DOWN;
                else if (!strcmp(p, "nibble"))
                    printf_nibble_mode = true;
                else if (!strcmp(p, "printf") ||
                         !strncmp(p, "printf=", 7)) {
                    static const char flags[] = PRINTF_FLAGSTR;
                    const char *fmt, *q;
                    if (p[6]) {
                        fmt = p+7;
                    } else if (argc > 1) {
                        --argc;
                        fmt = *++argv;
                    } else {
                        fprintf(stderr, "option '--printf' expects a"
                                " parameter\n");
                        return 1;
                    }

                    q = fmt;
                    if (*q++ != '%') {
                        fprintf(stderr, "expected %% at start of '--printf'"
                                " parameter '%s'\n", fmt);
                        return 1;
                    }
                    printf_flags = 0;
                    while (*q && strchr(flags, *q)) {
                        printf_flags |= 1 << (strchr(flags, *q) - flags);
                        q++;
                    }
                    if (*q == '*') {
                        fprintf(stderr, "'*' width specifier not supported in"
                                " '--printf' parameter '%s'\n", fmt);
                        return 1;
                    } else if (isdigit((unsigned char)*q)) {
                        char *ret;
                        printf_width = strtol(q, &ret, 10);
                        q = ret;
                    } else {
                        printf_width = -1;
                    }
                    if (*q == '.') {
                        q++;
                        if (*q == '*') {
                            fprintf(stderr, "'*' precision specifier not "
                                    "supported in '--printf' parameter"
                                    " '%s'\n", fmt);
                            return 1;
                        } else if (isdigit((unsigned char)*q)) {
                            char *ret;
                            printf_precision = strtol(q, &ret, 10);
                            q = ret;
                        } else {
                            fprintf(stderr, "expected number after '.' in"
                                    " '--printf' parameter '%s'\n", fmt);
                            return 1;
                        }
                    } else {
                        printf_precision = -1;
                    }
                    if (*q && strchr("hljzt", *q)) {
                        fprintf(stderr, "integer length modifiers not "
                                "supported in '--printf' parameter '%s'\n",
                                fmt);
                        return 1;
                    } else if (*q && strchr("L", *q)) {
                        // Quietly ignore a floating-point length
                        // modifier, so that users can paste a
                        // formatting directive out of their actual
                        // program and have it Just Work as often as
                        // possible.
                        q++;
                    }
                    if (!*q || !strchr("eEfFgGaA", *q)) {
                        fprintf(stderr, "expected floating-point conversion"
                                " specifier in '--printf' parameter '%s'\n",
                                fmt);
                        return 1;
                    }
                    printf_specifier = *q++;
                    if (*q) {
                        fprintf(stderr, "expected nothing after conversion"
                                " specifier in '--printf' parameter '%s'\n",
                                fmt);
                        return 1;
                    }
                    outmode = MODE_PRINTF;
                    rmode = ROUND_TO_NEAREST_EVEN;
#ifndef TENTATIVE_OUTPUT_DISABLED
                } else if (!strcmp(p, "tentative") ||
                         !strncmp(p, "tentative=", 10)) {
                    const char *val;
                    if (p[9]) {
                        val = p+10;
                    } else if (argc > 1) {
                        --argc;
                        val = *++argv;
                    } else {
                        fprintf(stderr, "option '--tentative' expects a"
                                " parameter\n");
                        return 1;
                    }
                    if (!strcmp(val, "on") ||
                        !strcmp(val, "yes") ||
                        !strcmp(val, "true")) {
                        tentative_output_option = ON;
                    } else if (!strcmp(val, "off") ||
                               !strcmp(val, "no") ||
                               !strcmp(val, "false")) {
                        tentative_output_option = OFF;
                    } else if (!strcmp(val, "auto")) {
                        tentative_output_option = AUTO;
                    }
#endif /* TENTATIVE_OUTPUT_DISABLED */
                } else if (!strcmp(p, "tentative-test") ||
                           !strncmp(p, "tentative-test=", 15)) {
                    const char *val;
                    if (p[14]) {
                        val = p+15;
                    } else if (argc > 1) {
                        --argc;
                        val = *++argv;
                    } else {
                        fprintf(stderr, "option '--tentative-test' expects a"
                                " parameter\n");
                        return 1;
                    }

                    tentative_test = true;
                    tentative_test_digits = atoi(val);

#ifndef TENTATIVE_OUTPUT_DISABLED
                    /*
                     * In --tentative-test mode, disable the usual
                     * tentative output.
                     *
                     * (The --tentative-test option itself can remain
                     * enabled even when we're compiling spigot
                     * without the main tentative output feature,
                     * because the test mode is purely computational.
                     * The reasons why we can't always enable the
                     * proper tentative output feature have to do with
                     * ncurses and terminal devices and printing the
                     * output sensibly, not with the underlying
                     * mechanism for deciding what the tentative
                     * output _should be_.)
                     */
                    tentative_output_option = OFF;
#endif
                } else if (!strcmp(p, "help")) {
                    print_help(stdout);
                    return 0;
                } else if (!strcmp(p, "licence") || !strcmp(p, "license")) {
                    print_licence(stdout);
                    return 0;
                } else if (!strcmp(p, "version")) {
                    print_version(stdout);
                    return 0;
                } else {
                    fprintf(stderr, "unrecognised option '--%s'\n", p);
                    return 1;
                }
            } else {
                /*
                 * Short option(s).
                 */
                const char *val;
                p++;
                while (*p) {
                    char c = *p++;
                    switch (c) {
                      case 'b':
                      case 'B':
                      case 'd':
                      case 'w':
                        /*
                         * Options requiring an argument.
                         */
                        val = p;
                        p = "";
                        if (!*val) {
                            if (--argc > 0) {
                                val = *++argv;
                            } else {
                                fprintf(stderr, "option '-%c' expects a "
                                        "value\n", c);
                                return 1;
                            }
                        }
                        switch (c) {
                          case 'b':
                          case 'B':
                            outmode = MODE_BASE;
                            base = atoi(val);
                            if (base < 2 || base > 36) {
                                fprintf(stderr, "bases not in [2,...,36] are"
                                        " unsupported\n");
                                return 1;
                            }
                            base_uppercase = (c == 'B');
                            break;
                          case 'd':
                            digitlimit = atoi(val);
                            got_digitlimit = true;
                            break;
                          case 'w':
                            minintdigits = atoi(val);
                            break;
                        }
                        break;
                      case 'c':
                        outmode = MODE_CFRAC;
                        break;
                      case 'C':
                        outmode = MODE_CONVERGENTS;
                        break;
                      case 'R':
                        outmode = MODE_RATIONAL;
                        break;
                      case 'F':
                      case 'S':
                        outmode = MODE_IEEE;
                        ieee_bits = 32;
                        break;
                      case 'D':
                        outmode = MODE_IEEE;
                        ieee_bits = 64;
                        break;
                      case 'Q':
                        outmode = MODE_IEEE;
                        ieee_bits = 128;
                        break;
                      case 'H':
                        outmode = MODE_IEEE;
                        ieee_bits = 16;
                        break;
                      case 'l':
                        oneline = true;
                        break;
                      case 'T':
#ifdef HAVE_FDS
                        set_term_modes = true;
                        break;
#else
                        fprintf(stderr, "option '-T' not supported in this"
                                " build of spigot\n");
                        return 1;
#endif
                      case 'n':
                        print_newline = false;
                        break;
                      default:
                        fprintf(stderr, "unrecognised option '-%c'\n", c);
                        return 1;
                    }
                }
            }
        } else {
            if (sp) {
                fprintf(stderr, "only one expression argument expected\n");
                return 1;
            } else {
                try {
                    sp = expr_parse(p, NULL);
                } catch (spigot_error err) {
                    fprintf(stderr, "%s\n", err.errmsg);
                    return 1;
                }
            }
        }
    }

    if (!sp) {
        fprintf(stderr, "expected an expression argument\n");
        return 1;
    }

#ifndef TENTATIVE_OUTPUT_DISABLED
    if (tentative_output_option == ON ||
        (tentative_output_option == AUTO
#ifndef _WIN32
         && isatty(fileno(stdout))
#endif
            )) {
        /*
         * If the user has attempted to force tentative output *on*,
         * then we pass required=true to the setup function, which
         * will cause an error message if any of the necessary control
         * sequences can't be retrieved from terminfo. In auto mode,
         * however, we just silently disable tentative output in that
         * situation, the same as we do if the output channel isn't a
         * terminal.
         *
         * (On Windows, we just try to turn on tentative output
         * regardless, because it can only work on consoles anyway, so
         * setup_tentative_output will fail in the auto case if
         * standard output is not a console.)
         */
        setup_tentative_output(tentative_output_option == ON);
    }
#endif

#ifdef HAVE_FDS
    {
        extern void begin_fd_input();
        begin_fd_input();
    }
#endif

    try {
        OutputGenerator *og = NULL;
        switch (outmode) {
          case MODE_BASE:
            og = base_format
                (sp, base, base_uppercase, got_digitlimit, digitlimit,
                 rmode, minintdigits);
            break;
          case MODE_PRINTF:
            og = printf_format
                (sp, rmode, printf_width, printf_precision,
                 printf_flags, printf_specifier, printf_nibble_mode);
            break;
          case MODE_IEEE:
            og = ieee_format
                (sp, ieee_bits, got_digitlimit, digitlimit, rmode);
            break;
          case MODE_CFRAC:
            og = cfrac_output(sp, oneline, got_digitlimit, digitlimit);
            if (!oneline)
                print_newline = false; // we printed a newline already
            break;
          case MODE_CONVERGENTS:
            og = convergents_output(sp, got_digitlimit, digitlimit);
            print_newline = false; // we printed a newline already
            break;
          case MODE_RATIONAL:
            og = rational_output(sp, got_digitlimit);
            break;
          default:
            /*
             * Should never get here, since no other value of
             * outmode would have come to this branch of the outer
             * switch. But clang will give a warning anyway about
             * not all enum values appearing in this switch, so
             * here's a pointless default clause to placate it.
             */
            og = NULL;
        }
        assert(og);
        write_to_stdout(og, print_newline);
        return 0;
    } catch (spigot_eof err) {
        if (print_newline) {
            putchar('\n');
            check_stdout();
        }
        fprintf(stderr, "end of file '%s'\n", err.errmsg);
        return 2;
    } catch (spigot_error err) {
        fprintf(stderr, "%s\n", err.errmsg);
        return 1;
    }

    return 0;
}
