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

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

#include <string>
using std::string;

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

#ifdef _WIN32
#  include <windows.h>
#else
#  if HAVE_UNISTD_H && HAVE_CURSES
#    include <unistd.h>
#    include CURSES_HEADER
#    include <term.h>
#  else
#    define TENTATIVE_OUTPUT_DISABLED
#  endif
#endif

void check_output(FILE *fp)
{
    fflush(fp);
    if (ferror(fp))
        exit(1);
}

#ifndef TENTATIVE_OUTPUT_DISABLED

static 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(FILE *fp, bool required)
{
    if (fp != stdout) {
        if (required)
            fprintf(stderr, "spigot: unable to produce tentative output for"
                    " any destination other than standard output\n");
        return;
    }

    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(FILE *fp)
{
    fputc('\b', fp);
}

void print_in_tentative_colour(FILE *fp, const char *s)
{
    SetConsoleTextAttribute(console_handle, tentative_attributes);
    fputs(s, fp);
    fflush(fp);
    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 string term_backspace, term_tentative_colour, term_normal_colour;

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

    if (setupterm(nullptr, 1, nullptr) != 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 == nullptr || 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 == nullptr || 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 == nullptr || 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(FILE *fp)
{
    fputs(term_backspace.c_str(), fp);
}

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

#endif /* _WIN32 */

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

void write_tentative_output(FILE *fp, const string &s)
{
    assert(s.size() > 0);
    if (tentative_output_enabled) {
        if (s == current_tentative_output)
            return;
        unwrite_tentative_output(fp);
        print_in_tentative_colour(fp, s.c_str());
        current_tentative_output = s;
        check_output(fp);
    }
}

#endif /* TENTATIVE_OUTPUT_DISABLED */

void write_definite_output(FILE *fp, const string &s)
{
    assert(s.size() > 0);
#ifndef TENTATIVE_OUTPUT_DISABLED
    unwrite_tentative_output(fp);
#endif
    fputs(s.c_str(), fp);
    check_output(fp);
}

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_output(FILE *fp, unique_ptr<OutputGenerator> &og,
                  bool print_newline)
{
    while (1) {
        string out;
        int digits;

        if (og->get_definite_output(out)) {
            if (out.size() > 0) {
                write_definite_output(fp, out);
            } else if (og->get_tentative_output(out, &digits) &&
                       digits >= MIN_TENTATIVE_DIGITS) {
                if (tentative_test && digits >= tentative_test_digits) {
                    write_definite_output(fp, "!");
                    write_definite_output(fp, out);
                    break;
                }
#ifndef TENTATIVE_OUTPUT_DISABLED
                write_tentative_output(fp, out);
#endif
            }
        } else {
            break;
        }
    }
    if (print_newline) {
        fputc('\n', fp);
        check_output(fp);
    }
}
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",
"       -o FILE             write output to FILE instead of standard output",
"       --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
"       --safe              forbid expressions asking to read from files"
#ifdef HAVE_FDS
" or fds"
#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-2019 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
        );
}

class CLINullFileAccessContext : public FileAccessContext {
    virtual unique_ptr<FileOpener> file_opener(const string &) override;
    virtual unique_ptr<FileOpener> fd_opener(int) override;
    virtual unique_ptr<FileOpener> stdin_opener() override;
};

unique_ptr<FileOpener> CLINullFileAccessContext::file_opener(const string &)
{
    throw io_error("file keywords not permitted with --safe option");
}

unique_ptr<FileOpener> CLINullFileAccessContext::fd_opener(int)
{
    throw io_error("fd keywords not permitted with --safe option");
}

unique_ptr<FileOpener> CLINullFileAccessContext::stdin_opener()
{
    throw io_error("stdin keywords not permitted with --safe option");
}

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;
    bool revsci = false;
    long revsci_expbase = 0;
    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 = nullptr;
    int printf_width = 0, printf_precision = 0;
    int printf_flags = 0, printf_specifier = 0;
    char printf_pad = ' ';
    const char *expr = nullptr;
    const char *outfile = nullptr;
    auto fac = make_cli_file_access_context();

    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;

#define RMODE_OPTION(opt, mode) \
                if (!strcmp(p, #opt)) { \
                    rmode = mode; \
                    continue; \
                }
                RoundingMode_ABBR_LIST(RMODE_OPTION);
#undef RMODE_OPTION

                if (!strcmp(p, "nibble"))
                    printf_nibble_mode = true;
                else if (!strcmp(p, "safe"))
                    fac = make_unique<CLINullFileAccessContext>();
                else if (!strcmp(p, "debug-fns"))
                    enable_debug_fns = true;
                else if (!strcmp(p, "printf") ||
                         !strncmp(p, "printf=", 7)) {
                    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;
                    }
                    bool seen_minus_flag = false;
                    while (true) {
                        switch (*q) {
                          case '-':
                            seen_minus_flag = true;
                            printf_pad = ' ';
                            printf_flags |= PRINTF_ALIGN_LEFT;
                            break;
                          case '#':
                            printf_flags |= PRINTF_FORCE_POINT;
                            break;
                          case '0':
                            if (!seen_minus_flag) { // '-' supersedes '0'
                                printf_pad = '0';
                                printf_flags |= PRINTF_ALIGN_MIDDLE;
                            }
                            break;
                          case '+':
                            printf_flags |= PRINTF_SIGN_PLUS;
                            break;
                          case ' ':
                            printf_flags |= PRINTF_SIGN_SPACE;
                            break;
                          default:
                            goto done_printf_flags; // 2-level break
                        }
                        q++;
                    }
                  done_printf_flags:

                    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, "debug") || !strncmp(p, "debug=", 6)) {
                    const char *val;
                    if (p[5]) {
                        val = p+6;
                    } else if (argc > 1) {
                        --argc;
                        val = *++argv;
                    } else {
                        fprintf(stderr, "option '--debug' expects a"
                                " parameter\n");
                        return 1;
                    }

                    const char *v = val;
                    char *vend;
                    Debuggable::Id id;

                    if (*v == 'D' || *v == 'd')
                        v++;
                    if (*v == '#')
                        v++;
                    if (*v && (id = static_cast<Debuggable::Id>(
                                   strtoul(v, &vend, 10)), !*vend)) {
                        Debuggable::add_debug_id(id);
                    } else if (!strcmp(val, "greetings")) {
                        Debuggable::debug_greetings();
                    } else if (!strcmp(val, "all")) {
                        Debuggable::debug_all();
                    } else if (!strcmp(val, "help")) {
                        fprintf(stderr, "valid parameters to '--debug' are:\n"
                                " - 'all'\n"
                                " - 'greetings'\n"
                                " - a numeric id like '123' or 'D#123'\n"
                                " - a class name from this list:\n");

                        // Display the class-name list wrapped as
                        // tightly as possible to fit on the screen
                        string prefix = "   ", line = prefix;
                        for (const string &name:
                                 Debuggable::known_class_names()) {
                            if (line.size() > prefix.size() &&
                                line.size() + 1 + name.size() > 79) {
                                fprintf(stderr, "%s\n", line.c_str());
                                line = prefix;
                            }
                            line += " " + name;
                        }
                        if (line.size() > prefix.size())
                            fprintf(stderr, "%s\n", line.c_str());

                        fprintf(stderr, " - 'help' to display this help\n");
                        return 0;
                    } else if (Debuggable::is_known_class_name(val)) {
                        Debuggable::add_debug_name(val);
                    } else {
                        fprintf(stderr, "unrecognised parameter '%s' to "
                                "option '--debug'; try '--debug=help'\n", val);
                        return 1;
                    }
                } 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 'o':
                      case 's':
                      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 'o':
                            outfile = val;
                            break;
                          case 's':
                            revsci = true;
                            if (!strcmp(val, "b") || !strcmp(val, "B")) {
                                /*
                                 * Special-case string, meaning 'use
                                 * the same base for the exponent as
                                 * for the main significand digits'.
                                 */
                                revsci_expbase = 0;
                            } else {
                                char *end;
                                revsci_expbase = strtol(val, &end, 0);
                                if (!*val || *end) {
                                    fprintf(stderr, "unable to parse exponent "
                                            "base '%s'\n", val);
                                    return 1;
                                }
                                if (revsci_expbase <= 1) {
                                    fprintf(stderr, "exponent base must be "
                                            "at least 2\n");
                                    return 1;
                                }
                            }
                          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 (expr) {
                fprintf(stderr, "only one expression argument expected\n");
                return 1;
            } else {
                expr = p;
            }
        }
    }

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

    try {
        sp = expr_parse(expr, nullptr, *fac);
    } catch (spigot_error &err) {
        fprintf(stderr, "%s\n", err.text());
        return 1;
    }

    FILE *outfp;
    if (outfile) {
        outfp = fopen(outfile, "w");
        if (!outfp) {
            fprintf(stderr, "unable to open '%s': %s\n",
                    outfile, strerror(errno));
            exit(1);
        }
    } else {
        outfp = stdout;
    }

#ifndef TENTATIVE_OUTPUT_DISABLED
    if (tentative_output_option == ON ||
        (tentative_output_option == AUTO
#ifndef _WIN32
         && isatty(fileno(outfp))
#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(outfp, tentative_output_option == ON);
    }
#endif

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

    try {
        unique_ptr<OutputGenerator> og;
        switch (outmode) {
          case MODE_BASE:
            if (revsci) {
                og = revsci_format
                    (move(sp), revsci_expbase ? revsci_expbase : base,
                     base, base_uppercase, got_digitlimit, digitlimit, rmode);
                break;
            } else {
                og = base_format
                    (move(sp), base, base_uppercase,
                     got_digitlimit, digitlimit, rmode, minintdigits);
            }
            break;
          case MODE_PRINTF:
            og = printf_format
                (move(sp), rmode, printf_width, printf_precision,
                 printf_flags, printf_pad, printf_specifier,
                 printf_nibble_mode);
            break;
          case MODE_IEEE:
            og = ieee_format
                (move(sp), ieee_bits, got_digitlimit, digitlimit, rmode);
            break;
          case MODE_CFRAC:
            og = cfrac_output(move(sp), oneline, got_digitlimit, digitlimit);
            if (!oneline)
                print_newline = false; // we printed a newline already
            break;
          case MODE_CONVERGENTS:
            og = convergents_output(move(sp), got_digitlimit, digitlimit);
            print_newline = false; // we printed a newline already
            break;
          case MODE_RATIONAL:
            og = rational_output(move(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 = nullptr;
        }
        assert(og);
        write_output(outfp, og, print_newline);
        return 0;
    } catch (eof_event &err) {
        if (print_newline) {
            fputc('\n', outfp);
            check_output(outfp);
        }
        fprintf(stderr, "end of file '%s'\n", err.text());
        return 2;
    } catch (spigot_error &err) {
        fprintf(stderr, "%s\n", err.text());
        return 1;
    }

    return 0;
}
