/*
 * pyspig.cpp: a Python extension which exposes the more sensible end
 * of spigot's internal API.
 */

#include <string>
#include <sstream>
using std::string;
using std::ostringstream;

#include <Python.h>
#include "structmember.h"

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

#if PY_MAJOR_VERSION >= 3
#define int_from_string PyLong_FromString
#define int_from_long PyLong_FromLong
#define string_from_string PyUnicode_FromString
#define bytes_Check PyBytes_Check
#define bytes_GET_SIZE PyBytes_GET_SIZE
#define bytes_AS_STRING PyBytes_AS_STRING
#else
#define int_from_string PyInt_FromString
#define int_from_long PyInt_FromLong
#define string_from_string PyString_FromString
#define bytes_Check PyString_Check
#define bytes_GET_SIZE PyString_GET_SIZE
#define bytes_AS_STRING PyString_AS_STRING
#endif

enum SpigState {
    SS_EMPTY,
    SS_FILLED,
    SS_FORMATTING,
    SS_CFRAC,
    SS_STATIC,
};

struct SpigotPy {
    PyObject_HEAD
    Spigot spig;
    unique_ptr<CfracGenerator> cfg;
    unique_ptr<StaticGenerator> sg;
    unique_ptr<OutputGenerator> og;
    bool og_finished = false;
    SpigState state = SS_EMPTY;
};

static PyObject *Spigot_new(PyTypeObject *type, PyObject *args, PyObject *kwds)
{
    // First use Python's allocator to get a memory block of the right size
    void *memory = type->tp_alloc(type, 0);
    if (!memory)
        return nullptr;

    // Then use C++ placement-new to construct a C++ object inside that block
    return (PyObject *) new (memory) SpigotPy;
}

static void Spigot_dealloc(SpigotPy *self)
{
    auto ob_type = Py_TYPE(self); // save a copy before destroying class

    // Before freeing the memory, run the C++ class's destructor
    // (which will take care of cleaning up anything else that the
    // contained unique_ptrs refer to)
    self->~SpigotPy();

    // And now the memory is inert and we can safely free it.
    ob_type->tp_free((PyObject *)self);
}

struct python_error {};

void spigot_check_exception()
{
    if (PyErr_CheckSignals())
        throw python_error();
}

static void spigot_emplace(SpigotPy *self, Spigot spig)
{
    if (self) {
        self->spig = move(spig);
        self->cfg = nullptr;
        self->og = nullptr;
        self->state = (self->spig ? SS_FILLED : SS_EMPTY);
    }
}

// Small helper class which Py_DECREFs an object on its destruction
class PythonReferenceWrapper {
    PyObject *obj;
  public:
    PythonReferenceWrapper(PyObject *aobj, bool increment=true);
    PythonReferenceWrapper(const PythonReferenceWrapper &other);
    ~PythonReferenceWrapper();
};

PythonReferenceWrapper::PythonReferenceWrapper(PyObject *aobj, bool increment)
    : obj(aobj)
{
    if (increment)
        Py_INCREF(obj);
}

PythonReferenceWrapper::PythonReferenceWrapper(
    const PythonReferenceWrapper &other)
    : PythonReferenceWrapper(other.obj)
{
}

PythonReferenceWrapper::~PythonReferenceWrapper()
{
    Py_DECREF(obj);
}

class PythonNullFileAccessContext : 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> PythonNullFileAccessContext::file_opener(const string &)
{
    throw io_error("file keywords not permitted in Python spigot");
}

unique_ptr<FileOpener> PythonNullFileAccessContext::fd_opener(int fd)
{
    throw io_error("fd keywords not permitted in Python spigot");
}

unique_ptr<FileOpener> PythonNullFileAccessContext::stdin_opener()
{
    throw io_error("stdin keywords not permitted in Python spigot");
}

class PythonFileObjectAccessContext
    : public FileAccessContext, public Immovable
{
    PyObject *pyfac;

    class ReaderRaw : public FileReaderRaw, public Immovable {
        PyObject *pyfile;

        virtual size_t read_inner(void *vbuf, size_t size) override;

      public:
        ReaderRaw(PyObject *apyfile);
        ~ReaderRaw();
    };

    class Opener : public FileOpener, public Immovable {
        PyObject *pyfile;

        virtual BufferingReaderCacheKey key() const override;
        virtual string name() const override;
        virtual unique_ptr<FileReaderRaw> open() const override;

      public:
        Opener(PyObject *apyfile);
        ~Opener();
    };

  public:
    PythonFileObjectAccessContext(PyObject *apyfac);
    ~PythonFileObjectAccessContext();

  private:
    virtual unique_ptr<FileOpener> file_opener(const string &) override;
    virtual unique_ptr<FileOpener> fd_opener(int) override;
    virtual unique_ptr<FileOpener> stdin_opener() override;
};

PythonFileObjectAccessContext::ReaderRaw::ReaderRaw(PyObject *apyfile)
    : pyfile(apyfile)
{
    Py_INCREF(pyfile);
}

PythonFileObjectAccessContext::ReaderRaw::~ReaderRaw()
{
    Py_DECREF(pyfile);
}

size_t PythonFileObjectAccessContext::ReaderRaw::read_inner(
    void *vbuf, size_t size)
{
    PyObject *pysize = PyLong_FromLong(size);
    PyObject *pydata = PyObject_CallMethod(
        pyfile, "read", "O", pysize);
    Py_DECREF(pysize);

    if (!pydata)
        throw python_error();      // propagate exception

    PythonReferenceWrapper wr(pydata, false);

    if (bytes_Check(pydata)) {
        size_t byteslen = bytes_GET_SIZE(pydata);
        if (byteslen > size)
            throw io_error("reading from file returned too many bytes");

        memcpy(vbuf, bytes_AS_STRING(pydata), byteslen);
        return byteslen;
    } else if (PyUnicode_Check(pydata)) {
#if PY_MAJOR_VERSION >= 3
        /*
         * Python 3 method of reading from a Unicode object
         */

        if (PyUnicode_READY(pydata) < 0)
            throw python_error();      // propagate exception

        size_t unilen = PyUnicode_GET_LENGTH(pydata);
        if (unilen > size)
            throw io_error("reading from file returned too many characters");

        int kind = PyUnicode_KIND(pydata);
        void *data = PyUnicode_DATA(pydata);
        unsigned char *buf = (unsigned char *)vbuf;

        for (size_t index = 0; index < unilen; index++) {
            Py_UCS4 ch = PyUnicode_READ(kind, data, index);
            // spigot's file input will not do anything useful with
            // characters outside ASCII, so we might as well normalise
            // any interesting Unicode to some nonsense value like
            // 0x7F.
            buf[index] = ch > 0x7F ? 0x7F : ch;
        }

        return unilen;
#else
        /*
         * Python 2 method of reading from a Unicode object
         */
        size_t unilen = PyUnicode_GET_SIZE(pydata);
        if (unilen > size)
            throw io_error("reading from file returned too many characters");

        Py_UNICODE *data = PyUnicode_AS_UNICODE(pydata);
        unsigned char *buf = (unsigned char *)vbuf;

        for (size_t index = 0; index < unilen; index++) {
            Py_UNICODE ch = data[index];
            // As above, normalise non-ASCII stuff. This also means we
            // don't have to worry about whether Py_UNICODE is 32- or
            // 16-bit (and deal with UTF-16 surrogates in the latter
            // case).
            buf[index] = ch > 0x7F ? 0x7F : ch;
        }

        return unilen;
#endif
    } else {
        throw io_error("reading from file returned bad type");
    }
}

PythonFileObjectAccessContext::Opener::Opener(PyObject *apyfile)
    : pyfile(apyfile)
{
    Py_INCREF(pyfile);
}

PythonFileObjectAccessContext::Opener::~Opener()
{
    Py_DECREF(pyfile);
}

BufferingReaderCacheKey PythonFileObjectAccessContext::Opener::key() const
{
    return BufferingReaderCacheKey(FileType::Uncached);
}

string PythonFileObjectAccessContext::Opener::name() const
{
    ostringstream os;

#if PY_MAJOR_VERSION >= 3
    PyObject *pyrepr = PyObject_ASCII(pyfile);
    PythonReferenceWrapper wr(pyrepr, false);
    size_t len = PyUnicode_GET_LENGTH(pyrepr);
    int kind = PyUnicode_KIND(pyrepr);
    void *data = PyUnicode_DATA(pyrepr);
    for (size_t index = 0; index < len; index++)
        os << (char)PyUnicode_READ(kind, data, index);
#else
    PyObject *pyrepr = PyObject_Repr(pyfile);
    PythonReferenceWrapper wr(pyrepr, false);
    os << PyString_AsString(pyrepr);
#endif
    return os.str();
}

unique_ptr<FileReaderRaw> PythonFileObjectAccessContext::Opener::open() const
{
    return make_unique<ReaderRaw>(pyfile);
}

PythonFileObjectAccessContext::PythonFileObjectAccessContext(PyObject *apyfac)
    : pyfac(apyfac)
{
    Py_INCREF(pyfac);
}

PythonFileObjectAccessContext::~PythonFileObjectAccessContext()
{
    Py_DECREF(pyfac);
}

unique_ptr<FileOpener> PythonFileObjectAccessContext::file_opener(
    const string &filename)
{
    if (filename != "dummy")
        throw io_error("file names other than 'dummy' not supported in"
                       " this file access context");
    return make_unique<Opener>(pyfac);
}

unique_ptr<FileOpener> PythonFileObjectAccessContext::fd_opener(int)
{
    throw io_error("fd keywords not supported in this file access context");
}

unique_ptr<FileOpener> PythonFileObjectAccessContext::stdin_opener()
{
    throw io_error("stdin keywords not supported in this file access context");
}

static PyObject *bigint_to_pylong(const bigint &n);
static bigint bigint_from_pylong(PyObject *n);

static PyObject *Spigot_parse(SpigotPy *self, PyObject *args);
static PyObject *Spigot_parse_literal(SpigotPy *self, PyObject *args);
static PyObject *Spigot_clone(SpigotPy *self, PyObject *args);
static PyObject *Spigot_cfracsource(SpigotPy *self, PyObject *args);
static PyObject *Spigot_intervalsource(SpigotPy *self, PyObject *args);
static PyObject *Spigot_base(SpigotPy *self, PyObject *args);
static PyObject *Spigot_ieee(SpigotPy *self, PyObject *args);
static PyObject *Spigot_printf(SpigotPy *self, PyObject *args);
static PyObject *Spigot_readfmt(SpigotPy *self, PyObject *args);
static PyObject *Spigot_cfracterm(SpigotPy *self, PyObject *args);
static PyObject *Spigot_static_floor(SpigotPy *self, PyObject *args);
static PyObject *Spigot_static_premultiply(SpigotPy *self, PyObject *args);
static PyObject *Spigot_sign(SpigotPy *self, PyObject *args);
static PyObject *Spigot_rationalval(SpigotPy *self, PyObject *args);
static PyObject *Spigot_to_int(SpigotPy *self, PyObject *args);

static PyMethodDef Spigot_methods[] = {
    {"parse", (PyCFunction)Spigot_parse, METH_VARARGS,
     "Initialise a spigot by parsing an expression."
    },
    {"parse_literal", (PyCFunction)Spigot_parse_literal, METH_VARARGS,
     "Initialise a spigot by parsing a numeric literal only."
    },
    {"clone", (PyCFunction)Spigot_clone, METH_VARARGS,
     "Initialise a spigot by cloning from another spigot."
    },
    {"cfracsource", (PyCFunction)Spigot_cfracsource, METH_VARARGS,
     "Initialise a spigot from a callable giving continued fraction terms."
    },
    {"intervalsource", (PyCFunction)Spigot_intervalsource, METH_VARARGS,
     "Initialise a spigot from a callable giving subinterval tuples."
    },
    {"base", (PyCFunction)Spigot_base, METH_VARARGS,
     "Prepare a spigot for formatting data in a positional base."
    },
    {"ieee", (PyCFunction)Spigot_ieee, METH_VARARGS,
     "Prepare a spigot for formatting data as an (optionally-extended)"
     " IEEE hex representation."
    },
    {"printf", (PyCFunction)Spigot_printf, METH_VARARGS,
     "Prepare a spigot for formatting data in printf style."
    },
    {"readfmt", (PyCFunction)Spigot_readfmt, METH_NOARGS,
     "Read some data from a spigot that is in formatting mode."
    },
    {"cfracterm", (PyCFunction)Spigot_cfracterm, METH_NOARGS,
     "Read a continued fraction term from a spigot."
    },
    {"static_floor", (PyCFunction)Spigot_static_floor, METH_NOARGS,
     "Get the floor of a spigot's contents."
    },
    {"static_premultiply", (PyCFunction)Spigot_static_premultiply, METH_VARARGS,
     "Premultiply a matrix into a spigot in static-generator mode."
    },
    {"sign", (PyCFunction)Spigot_sign, METH_NOARGS,
     "Return the sign (-1, +1, or possibly 0) of the number in a spigot."
    },
    {"rationalval", (PyCFunction)Spigot_rationalval, METH_NOARGS,
     "Return a spigot's known rational value as an (n,d) tuple, or None."
    },
    {"to_int", (PyCFunction)Spigot_to_int, METH_VARARGS,
     "Round to an integer value with a given rounding mode."
    },
    {nullptr}  /* Sentinel */
};

static PyMemberDef Spigot_members[] = {
    {nullptr}  /* Sentinel */
};

static PyTypeObject spigot_SpigotType;

static PyObject *spigot_EndOfFile;

struct PythonGlobalScope : GlobalScope {
    PyObject *scope;

    PythonGlobalScope(PyObject *ascope) : scope(ascope) {
        Py_INCREF(scope);
    }
    ~PythonGlobalScope() {
        Py_DECREF(scope);
    }
    PythonGlobalScope(const PythonGlobalScope &) = delete;
    PythonGlobalScope(PythonGlobalScope &&) = delete;

    static Spigot lookup_recursive(PyObject *scope, const char *varname) {
        if (!PyMapping_Check(scope)) {
            PyErr_SetString(PyExc_TypeError,
                            "scope parameter must be a mapping type");
            throw python_error();
        }

        PyObject *found = PyMapping_GetItemString(scope, (char *)varname);
        if (!found)
            return nullptr;

        PythonReferenceWrapper wr(found, false);

        if (found == Py_None)
            return nullptr;

        if (!PyObject_IsInstance(found, (PyObject *)&spigot_SpigotType)) {
            string msg = "name '" + string(varname) +
                "' in scope returned a non-spigot";
            PyErr_SetString(PyExc_TypeError, msg.c_str());
            throw python_error();
        }

        return ((SpigotPy *)found)->spig->clone();
    }

    virtual Spigot lookup(const char *varname) {
        return lookup_recursive(scope, varname);
    }
};

static PyObject *Spigot_parse(SpigotPy *self, PyObject *args)
{
    const char *expr = nullptr;
    PyObject *pyscope = nullptr, *pyfile = nullptr;

    if (!PyArg_ParseTuple(args, "sOO", &expr, &pyscope, &pyfile))
        return nullptr;

    try {
        PythonGlobalScope scope(pyscope);
        unique_ptr<FileAccessContext> fac;
        if (!pyfile || pyfile == Py_None) {
            fac = make_unique<PythonNullFileAccessContext>();
        } else {
            fac = make_unique<PythonFileObjectAccessContext>(pyfile);
        }
        spigot_emplace(self, expr_parse(expr, &scope, *fac));
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    } catch (python_error) {
        return nullptr;
    }

    Py_RETURN_NONE;
}

static PyObject *Spigot_parse_literal(SpigotPy *self, PyObject *args)
{
    const char *expr = nullptr;

    if (!PyArg_ParseTuple(args, "s", &expr))
        return nullptr;

    try {
        spigot_emplace(self, literal_parse(expr));
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (python_error) {
        return nullptr;
    }

    Py_RETURN_NONE;
}

static PyObject *Spigot_clone(SpigotPy *self, PyObject *args)
{
    SpigotPy *from;

    if (!PyArg_ParseTuple(args, "O!", &spigot_SpigotType, (PyObject **)&from))
        return nullptr;

    spigot_emplace(self, from->spig ? from->spig->clone() : nullptr);

    Py_RETURN_NONE;
}

static PyObject *Spigot_to_int(SpigotPy *self, PyObject *args)
{
    int rmode;

    if (!PyArg_ParseTuple(args, "i", &rmode))
        return nullptr;

    try {
        return bigint_to_pylong(
            spigot_to_integer(self->spig->clone(),
                              static_cast<RoundingMode>(rmode)));
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    }
}

class PythonCfracSource : public CfracSource {
    PyObject *src;
    bigint index;

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

  public:
    PythonCfracSource(PyObject *asrc);
    virtual ~PythonCfracSource() override;
};

PythonCfracSource::PythonCfracSource(PyObject *asrc)
    : src(asrc), index(0)
{
    Py_INCREF(src);
}

PythonCfracSource::~PythonCfracSource()
{
    Py_DECREF(src);
}

Spigot PythonCfracSource::clone()
{
    return spigot_clone(this, src);
}

bool PythonCfracSource::gen_term(bigint *term)
{
    PyObject *pyindex = bigint_to_pylong(index);

    PyObject *pyterm = PyObject_CallFunction(src, "O", pyindex);
    Py_DECREF(pyindex);

    if (!pyterm)
        throw python_error();      // propagate exception

    *term = bigint_from_pylong(pyterm);
    Py_DECREF(pyterm);

    index += 1;

    return true;
}

static PyObject *Spigot_cfracsource(SpigotPy *self, PyObject *args)
{
    PyObject *src;

    if (!PyArg_ParseTuple(args, "O", (PyObject **)&src))
        return nullptr;
    if (!PyCallable_Check(src)) {
        PyErr_SetString(PyExc_TypeError, "cfracsource requires a callable");
        return nullptr;
    }

    spigot_emplace(self, make_unique<PythonCfracSource>(src));

    Py_RETURN_NONE;
}

class PythonIntervalSource : public Source {
    PyObject *src;
    bigint index;

    void get(bigint *a, bigint *b, bigint *c = nullptr);

    virtual Spigot clone() override;
    virtual bool gen_interval(bigint *low, bigint_or_inf *high) override;
    virtual bool gen_matrix(Matrix &matrix) override;

  public:
    PythonIntervalSource(PyObject *asrc);
    ~PythonIntervalSource();
};

PythonIntervalSource::PythonIntervalSource(PyObject *asrc)
    : src(asrc), index(0)
{
    Py_INCREF(src);
}

PythonIntervalSource::~PythonIntervalSource()
{
    Py_DECREF(src);
}

void PythonIntervalSource::get(bigint *a, bigint *b, bigint *c)
{
    PyObject *pyindex = bigint_to_pylong(index);

    PyObject *pytuple = PyObject_CallFunction(src, "O", pyindex);
    Py_DECREF(pyindex);

    if (!pytuple)
        throw python_error();      // propagate exception

    PythonReferenceWrapper wr(pytuple, false);

    PyObject *pya, *pyb, *pyc;

    if (!PyArg_ParseTuple(pytuple, c ? "OOO" : "OO",
                          (PyObject **)&pya, (PyObject **)&pyb,
                          (PyObject **)&pyc))
        throw python_error();      // propagate exception
    *a = bigint_from_pylong(pya);
    *b = bigint_from_pylong(pyb);
    if (c)
        *c = bigint_from_pylong(pyc);

    index += 1;
}

Spigot PythonIntervalSource::clone()
{
    return spigot_clone(this, src);
}

bool PythonIntervalSource::gen_interval(bigint *low, bigint_or_inf *high)
{
    *low = 0;
    *high = 1;
    return true;                   // force first call to gen_matrix
}

bool PythonIntervalSource::gen_matrix(Matrix &matrix)
{
    matrix[2] = 0;
    if (index == 0) {
        get(&matrix[1], &matrix[0]);
        matrix[3] = 1;
    } else {
        get(&matrix[1], &matrix[0], &matrix[3]);
    }
    matrix[0] -= matrix[1];
    return false;
}

static PyObject *Spigot_intervalsource(SpigotPy *self, PyObject *args)
{
    PyObject *src;

    if (!PyArg_ParseTuple(args, "O", (PyObject **)&src))
        return nullptr;
    if (!PyCallable_Check(src)) {
        PyErr_SetString(PyExc_TypeError, "intervalsource requires a callable");
        return nullptr;
    }

    spigot_emplace(self, make_unique<PythonIntervalSource>(src));

    Py_RETURN_NONE;
}

static PyObject *Spigot_base(SpigotPy *self, PyObject *args)
{
    int base, uppercase, digitlimit, rmode, minintdigits;

    if (!PyArg_ParseTuple(args, "iiiii", &base, &digitlimit,
                          &rmode, &minintdigits, &uppercase))
        return nullptr;

    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }

    assert(!self->og);
    self->og = base_format(self->spig->clone(), base, uppercase,
                           rmode != -1, digitlimit,
                           (RoundingMode)rmode, minintdigits);
    self->og_finished = false;
    self->state = SS_FORMATTING;

    Py_RETURN_NONE;
}

static PyObject *Spigot_ieee(SpigotPy *self, PyObject *args)
{
    int ieee_bits, digitlimit, rmode;

    if (!PyArg_ParseTuple(args, "iii", &ieee_bits, &digitlimit, &rmode))
        return nullptr;

    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }

    assert(!self->og);
    self->og = ieee_format(self->spig->clone(), ieee_bits,
                           rmode != -1, digitlimit, (RoundingMode)rmode);
    self->og_finished = false;
    self->state = SS_FORMATTING;

    Py_RETURN_NONE;
}

static PyObject *Spigot_printf(SpigotPy *self, PyObject *args)
{
    int rmode, width, prec, flags, pad, spec;
    PyObject *nibblemode_bool = nullptr;

    if (!PyArg_ParseTuple(args, "iiiiiiO",
                          &rmode, &width, &prec, &flags,
                          &pad, &spec, &nibblemode_bool))
        return nullptr;

    bool nibblemode = nibblemode_bool && PyObject_IsTrue(nibblemode_bool);

    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }

    assert(!self->og);
    self->og = printf_format(self->spig->clone(), (RoundingMode)rmode,
                             width, prec, flags, pad, spec, nibblemode);
    self->og_finished = false;
    self->state = SS_FORMATTING;

    Py_RETURN_NONE;
}

static PyObject *Spigot_readfmt(SpigotPy *self, PyObject *args)
{
    if (self->state != SS_FORMATTING) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not formatting");
        return nullptr;
    }

    string out;

    if (self->og_finished)
        return string_from_string("");

    try {
        while (true) {
            if (!self->og->get_definite_output(out)) {
                self->og_finished = true;
                return string_from_string("");
            }
            if (out.size() > 0)
                return string_from_string(out.c_str());
        }
    } catch (python_error) {
        return nullptr;
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    }
}

static PyObject *bigint_to_pylong(const bigint &n)
{
    /*
     * I didn't find anything in the Python embedding docs that
     * permits the construction of a Python Long object from raw
     * binary data. Hex-formatted ASCII is therefore the most
     * efficient interchange format I can find.
     */
    string hex = bigint_hexstring(n);
    // Evil casting-away of const, because PyLong_FromString requires
    // a char * rather than a const char * :-( But I trust it not to
    // _really_ modify its input string, so this should still work.
    PyObject *ret = int_from_string((char *)hex.c_str(), nullptr, 16);
    return ret;
}

static bigint bigint_from_pylong(PyObject *n)
{
    bigint toret = 0, negate = false;
    unsigned shift = 0, baseshift = 8 * sizeof(unsigned);
    unsigned mask = 1 + 2 * ((1UL << (baseshift-1)) - 1);
    vector<PythonReferenceWrapper> refs;
    PyObject *pyzero = PyLong_FromUnsignedLong(0);
    refs.emplace_back(pyzero, false);
    PyObject *pymask = PyLong_FromUnsignedLong(mask);
    refs.emplace_back(pymask, false);

    if (PyObject_RichCompareBool(n, pyzero, Py_LT)) {
        PyObject *n_new = PyNumber_Negative(n);
        refs.emplace_back(n_new);
        n = n_new;
        negate = true;
    }

    while (true) {
        PyObject *pyshift = int_from_long(shift);
        PyObject *shifted = PyNumber_Rshift(n, pyshift);
        Py_DECREF(pyshift);
        PythonReferenceWrapper wr(shifted, false);
        if (PyObject_RichCompareBool(shifted, pyzero, Py_LE))
            break;
        PyObject *dnum = PyNumber_And(shifted, pymask);
        PythonReferenceWrapper wr2(dnum, false);
        toret += bigint((unsigned)PyLong_AsUnsignedLong(dnum)) << shift;
        shift += baseshift;
    }

    if (negate)
        toret = -toret;

    return toret;
}

static PyObject *Spigot_cfracterm(SpigotPy *self, PyObject *args)
{
    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED && self->state != SS_CFRAC) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }
    if (self->state != SS_CFRAC) {
        assert(self->spig && !self->cfg);
        self->cfg = make_unique<CfracGenerator>(self->spig->clone());
        self->state = SS_CFRAC;
    }

    try {
        bigint term;
        if (!self->cfg->get_term(&term)) {
            Py_RETURN_NONE;
        }
        return bigint_to_pylong(term);
    } catch (python_error) {
        return nullptr;
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    }
}

static PyObject *Spigot_static_floor(SpigotPy *self, PyObject *args)
{
    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED && self->state != SS_STATIC) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }
    if (self->state != SS_STATIC) {
        assert(self->spig && !self->sg);
        self->sg = make_unique<StaticGenerator>(self->spig->clone());
        self->state = SS_STATIC;
    }

    try {
        bool constant;
        bigint digit = self->sg->get_floor(&constant);
        return PyTuple_Pack(2, bigint_to_pylong(digit),
                            PyBool_FromLong(constant));
    } catch (python_error) {
        return nullptr;
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    }
}

static PyObject *Spigot_static_premultiply(SpigotPy *self, PyObject *args)
{
    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED && self->state != SS_STATIC) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }
    if (self->state != SS_STATIC) {
        assert(self->spig && !self->sg);
        self->sg = make_unique<StaticGenerator>(self->spig->clone());
        self->state = SS_STATIC;
    }

    PyObject *pymatrix[4];
    if (!PyArg_ParseTuple(args, "(OOOO)",
                          &pymatrix[0], &pymatrix[1],
                          &pymatrix[2], &pymatrix[3]))
        return nullptr;

    try {
        Matrix matrix;
        for (int i = 0; i < 4; i++)
            matrix[i] = bigint_from_pylong(pymatrix[i]);
        self->sg->premultiply(matrix);

        Py_RETURN_NONE;
    } catch (python_error) {
        return nullptr;
    }
}

static PyObject *Spigot_sign(SpigotPy *self, PyObject *args)
{
    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    } else if (self->state != SS_FILLED) {
        PyErr_SetString(PyExc_RuntimeError, "spigot already formatting");
        return nullptr;
    }

    try {
        StaticGenerator test(self->spig->clone());
        return int_from_long((long)test.get_sign());
    } catch (python_error) {
        return nullptr;
    } catch (spigot_error &err) {
        PyErr_SetString(PyExc_ValueError, err.text());
        return nullptr;
    } catch (eof_event &err) {
        PyErr_SetString(spigot_EndOfFile, err.text());
        return nullptr;
    }
}

static PyObject *Spigot_rationalval(SpigotPy *self, PyObject *args)
{
    if (self->state == SS_EMPTY) {
        PyErr_SetString(PyExc_RuntimeError, "spigot not initialised");
        return nullptr;
    }

    bigint n, d;
    if (!self->spig->is_rational(&n, &d)) {
        Py_RETURN_NONE;
    }

    PyObject *pyn = bigint_to_pylong(n), *pyd = bigint_to_pylong(d);
    PythonReferenceWrapper wrn(pyn), wrd(pyd);
    return PyTuple_Pack(2, pyn, pyd);
}

static PyObject *Spigot_functions(SpigotPy *module, PyObject *args)
{
    PyObject *toret = PyDict_New();
    for (auto fn: expr_functions())
        PyDict_SetItemString(toret, fn.name.c_str(),
                             int_from_long(fn.arity));
    return toret;
}

static PyObject *Spigot_constants(SpigotPy *module, PyObject *args)
{
    PyObject *toret = PyDict_New();

#define ENUM_SET_VALUE(name) \
    PyDict_SetItemString(toret, #name, int_from_long(name));
#define ENUM_SET_VALUE_PRIVATE(name) \
    PyDict_SetItemString(toret, "_" #name, int_from_long(name));

    RoundingMode_LIST(ENUM_SET_VALUE);
    PrintfFlag_LIST(ENUM_SET_VALUE_PRIVATE);

    PyObject *rmode_abbrs = PyDict_New();
#define ENUM_SET_RMODE_ABBR(abbr, rmode)                                \
    PyDict_SetItemString(rmode_abbrs, #abbr,                            \
                         string_from_string(#rmode));
    RoundingMode_ABBR_LIST(ENUM_SET_RMODE_ABBR);

    PyDict_SetItemString(toret, "_rmodeabbrs", rmode_abbrs);

    return toret;
}

static PyMethodDef spigot_module_methods[] = {
    {"functions", (PyCFunction)Spigot_functions, METH_NOARGS,
     "Return a dict mapping name to arity for spigot's supported functions."
    },
    {"constants", (PyCFunction)Spigot_constants, METH_NOARGS,
     "Return a dict of auxiliary constants with ordinary Python types."
    },
    {nullptr}  /* Sentinel */
};

static bool module_setup(PyObject *module)
{
    static const PyTypeObject inittype = { PyVarObject_HEAD_INIT(nullptr, 0) };
    spigot_SpigotType = inittype;

    spigot_SpigotType.tp_name = "spigot._internal.Spigot";
    spigot_SpigotType.tp_basicsize = sizeof(SpigotPy);
    spigot_SpigotType.tp_dealloc = (destructor)Spigot_dealloc;
    spigot_SpigotType.tp_flags = Py_TPFLAGS_DEFAULT;
    spigot_SpigotType.tp_doc = "An object containing a spigot representation"
        " of a real number";
    spigot_SpigotType.tp_methods = Spigot_methods;
    spigot_SpigotType.tp_members = Spigot_members;
    spigot_SpigotType.tp_new = Spigot_new;

    if (PyType_Ready(&spigot_SpigotType) < 0)
        return false;

    Py_INCREF(&spigot_SpigotType);
    PyModule_AddObject(module, "Spigot", (PyObject *)&spigot_SpigotType);

    spigot_EndOfFile = PyErr_NewExceptionWithDoc(
        "spigot.EndOfFile",
        "A file object being used as a spigot source ran out of data."
        "\n\n"
        "This exception is thrown when ``spigot`` encounters an end-of-file "
        "condition when reading from a file object that was passed to one "
        "of the functions :func:`spigot.from_file` and "
        ":func:`spigot.from_cfrac_file`, without the ``exact`` flag.",
        nullptr, nullptr);
    PyModule_AddObject(module, "EndOfFile", spigot_EndOfFile);

    return true;
}

#if PY_MAJOR_VERSION >= 3

PyMODINIT_FUNC PyInit__internal(void)
{
    static const struct PyModuleDef mod_init = { PyModuleDef_HEAD_INIT };
    struct PyModuleDef *moduledef = new PyModuleDef;

    *moduledef = mod_init;
    moduledef->m_name = "myextension";
    moduledef->m_doc = "Module providing Python bindings for spigot-based exact"
        " real calculation.";
    moduledef->m_methods = spigot_module_methods;

    PyObject *module = PyModule_Create(moduledef);

    if (!module) {
        delete moduledef;
        return nullptr;
    }

    if (!module_setup(module)) {
        Py_DECREF(module);
        delete moduledef;
        return nullptr;
    }

    return module;
}

#else

PyMODINIT_FUNC init_internal(void)
{
    PyObject *module = Py_InitModule3(
        "spigot._internal", spigot_module_methods,
        "Module providing Python bindings for spigot-based exact real"
        " calculation.");
    if (!module)
        return;

    if (!module_setup(module))
        Py_DECREF(module);
}

#endif
