/*
 * io.cpp: Spigot definitions which provide numbers read from input
 * files or fds.
 *
 * (The fds part of that is conditional on running on an OS that
 * supports fds at all, to ease porting.)
 */

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

#include <string>
#include <vector>
#include <memory>
#include <map>
#include <sstream>
using std::string;
using std::vector;
using std::shared_ptr;
using std::make_shared;
using std::map;
using std::ostringstream;

#include "spigot.h"
#include "error.h"
#include "io.h"

/*
 * The architecture of this module is a bit fiddly and involves more
 * similarly named classes than I'd like, so here's an overview of
 * what's going on.
 *
 * The key problem is that if the user tells us to read a number from
 * a file or an fd, we can't just open it within our Source subclass
 * and start reading, because what if the resulting spigot has to be
 * cloned, or the user specifies the same number-from-fd more than
 * once in the original input expression? So we have a fundamental
 * requirement that we must only physically _read_ from a given
 * file/fd once, and we must buffer the data and be prepared to return
 * it again from the start if another copy of the same number is
 * needed.
 *
 * 'BufferingReader' is the key class that does the buffering. So
 * there must be exactly one BufferingReader per actual input source.
 * It handles multiple clients with different file positions by giving
 * each client a BufferingReaderPos which specifies where that client
 * had got up to; any time a client tries to read off the end of the
 * current buffer, BufferingReader will respond by reading more actual
 * input data, but if a client just wants to read more data that's
 * already buffered, BufferingReader will return it from the existing
 * buffer.
 *
 * Below BufferingReader are some implementing classes:
 *
 *  - FileReaderRaw and its subclasses deal with fiddly details of the
 *    actual reading: handling EOF, closing the FILE * when finished,
 *    and (in the subclass dealing with Unix numbered fds, and if
 *    configured to do so) restoring the termios settings on exit.
 *
 *  - FileBlock is a simple struct containing a chunk of buffered
 *    data. BufferingReader's buffer consists of a std::vector of these.
 *
 * Above BufferingReader is the caching system that makes sure we don't
 * open more than one BufferingReader for the same input:
 *
 *  - buffering_reader_cache is a map that stores all the currently
 *    known BufferingReaders
 *
 *  - BufferingReaderCacheKey is the key type for that map, consisting
 *    of a top-level enum saying what kind of thing you are (file or
 *    fd) and a string identifier to distinguish things of the same
 *    kind (the filename, or a decimal representation of the fd
 *    number).
 *
 *  - make_buffering_reader is the function you call to actually get
 *    hold of a BufferingReader, and it will either return the
 *    existing one from the cache or make a new one as necessary. Its
 *    parameter is a descendant of FileOpener, which is just a way to
 *    wrap up a few related methods: key() to return the cache key,
 *    name() to return a user-facing name for the input source if an
 *    error message about it needs to be generated, and open() to
 *    actually set up a FileReaderRaw pointing at it.
 */

// Method implementations for BufferingReaderCacheKey from io.h
BufferingReaderCacheKey::BufferingReaderCacheKey(FileType type_, string id_)
    : type(type_), id(id_)
{
}

BufferingReaderCacheKey::BufferingReaderCacheKey(FileType type_)
    : type(type_), id("")
{
}

bool BufferingReaderCacheKey::operator<(
    const BufferingReaderCacheKey &rhs) const
{
    if (type != rhs.type) return type < rhs.type;
    if (id != rhs.id) return id < rhs.id;
    return false;
}

FileReaderRaw::FileReaderRaw() : state(Open) {}

void FileReaderRaw::cleanup()
{
    if (state < CleanedUp) {
        cleanup_inner();
        state = CleanedUp;
    }
}

void FileReaderRaw::close()
{
    cleanup();
    if (state < Closed) {
        close_inner();
        state = Closed;
    }
}

void FileReaderRaw::setup(SpecialSetup sstype)
{
    if (state == Open)
        setup_inner(sstype);
}

FileReaderRaw::~FileReaderRaw()
{
    close();
}

size_t FileReaderRaw::read(void *buf, size_t size)
{
    size_t nread = read_inner(buf, size);
    if (nread == 0)
        close();
    return nread;
}

struct FileBlock {
    unsigned char data[16384];
    unsigned data_used;
    FileBlock() : data_used(0) {}
    bool full() const { return data_used == sizeof(data); }
    bool read(FileReaderRaw &frr) {
        assert(!full());
        size_t nread = frr.read(data + data_used, sizeof(data) - data_used);
        data_used += nread;
        return nread != 0;
    }
};

struct BufferingReaderPos {
    size_t blockidx = 0, blockpos = 0;
};

struct BufferingReader {
    string filename;
    unique_ptr<FileReaderRaw> frr;
    vector<unique_ptr<FileBlock>> blocks;

    BufferingReader(const string &afilename, unique_ptr<FileReaderRaw> frr_)
        : filename(afilename), frr(move(frr_)) {}

    void setup(SpecialSetup sstype) {
        if (frr)
            frr->setup(sstype);
    }
    void cleanup() {
        if (frr)
            frr->cleanup();
    }

    int getch(BufferingReaderPos &pos) {
        /*
         * The simple cases of just returning another character from
         * the current block, or stepping to the next existing block
         * and doing the same thing.
         */
        if (pos.blockidx < blocks.size() &&
            pos.blockpos == blocks[pos.blockidx]->data_used &&
            blocks[pos.blockidx]->full()) {
            ++pos.blockidx;
            pos.blockpos = 0;
        }

        if (pos.blockidx < blocks.size() &&
            pos.blockpos < blocks[pos.blockidx]->data_used)
            return blocks[pos.blockidx]->data[pos.blockpos++];

        /*
         * If we get here, then we know that pos.current points to
         * a block from which we've already returned all the data,
         * and that there is no pointer to a next block. So now we
         * must read from the file.
         */
        if (!frr) {
            /*
             * ... that is, if it's still even open (i.e. we haven't
             * already tried this once and got an eof).
             */
            return EOF;
        }

        if (pos.blockidx == blocks.size()) {
            assert(pos.blockpos == 0);
            blocks.push_back(make_unique<FileBlock>());
        }
        assert(pos.blockidx < blocks.size());
        assert(pos.blockpos == blocks[pos.blockidx]->data_used);
        if (!blocks[pos.blockidx]->read(*frr)) {
            /*
             * On end-of-file or error, destroy our FileReaderRaw;
             * we'll never try to read from it again.
             */
            frr = nullptr;
            return EOF;
        }
        /*
         * Otherwise, we should be able to just return a character
         * from the current block.
         */
        assert(pos.blockpos < blocks[pos.blockidx]->data_used);
        return blocks[pos.blockidx]->data[pos.blockpos++];
    }
};

static map<BufferingReaderCacheKey, shared_ptr<BufferingReader>>
    buffering_reader_cache;

shared_ptr<BufferingReader> make_buffering_reader(FileOpener &opener)
{
    auto key = opener.key();

    if (key.type != FileType::Uncached) {
        auto it = buffering_reader_cache.find(key);
        if (it != buffering_reader_cache.end())
            return it->second;
    }

    auto fr = make_shared<BufferingReader>(opener.name(), opener.open());

    if (key.type != FileType::Uncached) {
        buffering_reader_cache[key] = fr;
    }

    return fr;
}

class CfracFile : public CfracSource {
    shared_ptr<BufferingReader> file;
    BufferingReaderPos pos;
    bool first_term;
    bool exact;

    virtual Spigot clone() override;
    virtual bool gen_term(bigint *term) override;

  public:
    CfracFile(shared_ptr<BufferingReader> afile, bool aexact);
};

MAKE_CLASS_GREETER(CfracFile);

CfracFile::CfracFile(shared_ptr<BufferingReader> afile, bool aexact)
{
    file = afile;
    first_term = true;
    exact = aexact;
    dgreet("'", file->filename, "' exact=", exact);
}

Spigot CfracFile::clone()
{
    return spigot_clone(this, file, exact);
}

bool CfracFile::gen_term(bigint *term)
{
    int c, prevc;

    /*
     * We keep track of the character we saw just _before_ the
     * first digit, in case it was a minus sign (and we're in a
     * mood to recognise one - they're only supported in the
     * very first term of the continued fraction).
     */

    c = EOF;
    do {
        prevc = c;
        c = file->getch(pos);
    } while (c != EOF && !isdigit(c));

    if (c == EOF) {
        if (exact) {
            /*
             * In exact mode, EOF is interpreted as the
             * termination of the continued fraction expansion,
             * i.e. we actually have a rational.
             *
             * However, this only applies if we've already output at
             * least one term. Not even rationals (not even zero!)
             * have a continued fraction with _no_ terms.
             */
            if (first_term)
                throw io_error("continued fraction input from '" +
                               file->filename + "' ended before first term");
            return false;
        } else {
            /*
             * If we're in non-exact mode, throw the
             * out-of-precision exception.
             */
            throw eof_event(file->filename.c_str());
        }
    }

    *term = 0;
    while (c != EOF && isdigit(c)) {
        *term = *term * 10 + (c - '0');
        c = file->getch(pos);
    }

    if (prevc == '-' && first_term)
        *term = -*term;

    first_term = false;
    return true;
}

class BaseFile : public BaseSource {
    shared_ptr<BufferingReader> file;
    BufferingReaderPos pos;
    bool first_digit;
    int base;
    bool exact;

    int digitval(int c);

    virtual Spigot clone() override;
    virtual bool gen_digit(bigint *digit) override;

  public:
    BaseFile(int abase, shared_ptr<BufferingReader> afile, bool aexact);
};

MAKE_CLASS_GREETER(BaseFile);

BaseFile::BaseFile(int abase, shared_ptr<BufferingReader> afile, bool aexact)
    : BaseSource(abase, true), base(abase)
{
    file = afile;
    first_digit = true;
    exact = aexact;
    assert(base > 0);

    dgreet("'", file->filename, "' base=", base, " exact=", exact);
}

Spigot BaseFile::clone()
{
    return spigot_clone(this, base, file, exact);
}

int BaseFile::digitval(int c)
{
    if (isdigit(c) && (c - '0') < base) {
        return c - '0';
    } else if (c >= 'A' && c <= 'Z' && (c - 'A' + 10) < base) {
        return c - 'A' + 10;
    } else if (c >= 'a' && c <= 'z' && (c - 'a' + 10) < base) {
        return c - 'a' + 10;
    } else
        return -1;
}

bool BaseFile::gen_digit(bigint *digit)
{
    int c, dv;

    if (first_digit) {
        /*
         * Read the integer part of the number, complete with a
         * leading minus sign if desired.
         */
        bool negate = false, seen_digit = false;

        *digit = 0;

        do {
            c = file->getch(pos);
            if (c == '-' && !seen_digit) {
                negate = true;
            } else if ((dv = digitval(c)) >= 0) {
                seen_digit = true;
                *digit *= base;
                *digit += dv;
            }
        } while (c != '.' && c != EOF);

        if (negate)
            *digit = -1-*digit;

        first_digit = false;

        return true;
    } else {
        /*
         * Read a single digit from the fraction part of the number.
         */
        do {
            c = file->getch(pos);
            if (c == EOF) {
                if (exact) {
                    /*
                     * In exact mode, EOF is interpreted as the
                     * termination of the base expansion, i.e. we
                     * actually have a rational.
                     */
                    return false;
                } else {
                    /*
                     * If we're in non-exact mode, throw the
                     * out-of-precision exception.
                     */
                    throw eof_event(file->filename.c_str());
                }
            }
        } while ((dv = digitval(c)) < 0);
        *digit = dv;
        return true;
    }
}

void io_cleanup()
{
    for (auto kv: buffering_reader_cache)
        kv.second->cleanup();
}

void io_setup(SpecialSetup sstype)
{
    for (auto kv: buffering_reader_cache)
        kv.second->setup(sstype);
}

Spigot spigot_basefile(FileAccessContext &fac,
                       int base, const char *filename, bool exact)
{
    auto opener = fac.file_opener(filename);
    return make_unique<BaseFile>(base, make_buffering_reader(*opener), exact);
}

Spigot spigot_cfracfile(FileAccessContext &fac,
                        const char *filename, bool exact)
{
    auto opener = fac.file_opener(filename);
    return make_unique<CfracFile>(make_buffering_reader(*opener), exact);
}

Spigot spigot_basefd(FileAccessContext &fac,
                     int base, int fd)
{
    auto opener = fac.fd_opener(fd);
    return make_unique<BaseFile>(base, make_buffering_reader(*opener), true);
}

Spigot spigot_cfracfd(FileAccessContext &fac,
                      int fd)
{
    auto opener = fac.fd_opener(fd);
    return make_unique<CfracFile>(make_buffering_reader(*opener), true);
}

Spigot spigot_basestdin(FileAccessContext &fac,
                        int base)
{
    auto opener = fac.stdin_opener();
    return make_unique<BaseFile>(base, make_buffering_reader(*opener), true);
}

Spigot spigot_cfracstdin(FileAccessContext &fac)
{
    auto opener = fac.stdin_opener();
    return make_unique<CfracFile>(make_buffering_reader(*opener), true);
}
