/*
    Serial port support.
    Copyright (c) 2006 stefan kersten.

    ====================================================================

    SuperCollider real time audio synthesis system
    Copyright (c) 2002 James McCartney. All rights reserved.
    http://www.audiosynth.com

    This program is free software; you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation; either version 2 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program; if not, write to the Free Software
    Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
*/

#ifndef _WIN32
#    include <termios.h> // TIOCEXCL
#    include <sys/ioctl.h> // ioctl
#    include <errno.h> // errno
#    include <system_error> // system_error, error_code, error_category
#endif // _WIN32

#include <atomic>
#include <stdexcept>
#include <sstream>

#include "PyrKernel.h"
#include "PyrPrimitive.h"
#include "PyrSched.h"
#include "SCBase.h"
#include "SC_Lock.h"

#include <boost/asio/use_future.hpp>
#include <boost/asio.hpp>
#include <boost/bind/bind.hpp>
#include <boost/cstdint.hpp>
#include <boost/lockfree/spsc_queue.hpp>

using boost::uint8_t;
using boost::asio::serial_port;

extern boost::asio::io_context ioContext; // defined in SC_ComPort.cpp

/**
 * \brief Serial port abstraction
 *
 * Uses SerialPort::Options for configuration. The port is opened on construction, and destruction
 * releases all resourses by canceling outstanding TX/RX requests and closing the port.
 * This class polls the port on a separate thread and immediately reads available data into an
 * internal buffer.
 */
class SerialPort {
public:
    // 7, not 6 - the last two options passed in are condensed into flow_control
    static const int kNumOptions = 7;
    static const int kBufferSize = 8192;

    /// Type of the underlying FIFO buffer.
    using FIFO = boost::lockfree::spsc_queue<uint8_t, boost::lockfree::capacity<kBufferSize>>;

    /**
     * \brief Serial port configuration options.
     *
     * \note The two SuperCollider options \c xonxoff and \c crtscts are used together to set
     * Options::flow_control.
     */
    struct Options {
        /// Whether to request exclusive access to this port. Not implemented on Windows.
        bool exclusive = false;

        serial_port::baud_rate baudrate { 9600 };

        /// Corresponds to \c databits in SC code
        serial_port::character_size charsize { 8 };

        /// Number of stop bits to send. In SC code, true = 2, false = 1
        serial_port::stop_bits::type stop_bits { serial_port::stop_bits::two };

        serial_port::parity::type parity { serial_port::parity::none };

        /// Whether to use XON/XOFF signals (software), RTS/CTS signals (hardware), or neither.
        serial_port::flow_control::type flow_control { serial_port::flow_control::hardware };
    };

    static PyrSymbol* s_dataAvailable;
    static PyrSymbol* s_doneAction;

public:
    /**
     * \brief Opens port, sets options, and starts polling thread
     *
     * May throw \c std::system_error or \c boost::system::system_error during creation if opening
     * the port fails or if setting options fails.
     */
    SerialPort(PyrObject* obj, const char* serialport, const Options& options):
        m_obj(obj),
        m_port(ioContext, serialport),
        m_options(options),
        m_rxErrors(0) {
        using namespace boost::asio;

        m_port.set_option(options.baudrate);
        m_port.set_option(serial_port::parity(options.parity));
        m_port.set_option(options.charsize);
        m_port.set_option(serial_port::stop_bits(options.stop_bits));

        setFlowControl(options.flow_control);
        setExclusive(options.exclusive);

        m_readThread = SC_Thread { &SerialPort::doRead, this };
    }

    ~SerialPort() { m_readThread.join(); }

    const Options& options() const { return m_options; }

    /// Blocking single-byte write. Returns whether the write was successful.
    bool put(uint8_t byte) {
        boost::system::error_code ec {};
        m_port.write_some(boost::asio::buffer(&byte, sizeof(byte)), ec);
        return !ec;
    }

    /// Non-blocking single-byte read. Returns whether the read was successful.
    bool get(uint8_t* byte) {
        uint8_t ret;

        bool success = m_rxfifo.pop(ret);
        if (!success)
            return false;

        *byte = ret & 0xFF;
        return true;
    }

    /// The number of read errors since the last time this method was called.
    /**
     * A read error occurs when the FIFO is too full to receive data; the extra bytes are lost.
     */
    int rxErrorsSinceLastQuery() { return m_rxErrors.exchange(0); }

    /// Cancels all outstanding reads and writes and closes the port.
    void stop() {
        m_port.cancel();
        m_port.close();
    }

private:
    /// Sets this serial port to exclusive mode. Has no effect on Windows. Throws \c system_error
    /// on failure.
    void setExclusive(bool);

    /// Sets flow control behavior. Throws \c system_error if mode is not supported or if failure
    /// occurs during set.
    void setFlowControl(serial_port::flow_control::type control);

    /// Calls a one-arg SuperCollider command on this object.
    void runCommand(PyrSymbol* cmd) {
        gLangMutex.lock();
        if (m_obj) {
            VMGlobals* g = gMainVMGlobals;
            g->canCallOS = true;
            ++g->sp;
            SetObject(g->sp, m_obj);
            runInterpreter(g, cmd, 1);
            g->canCallOS = false;
        }
        gLangMutex.unlock();
    }

    /// Runs the doneAction callback function on the SuperCollider \c SerialPort object.
    void doneAction() { runCommand(s_doneAction); }

    /// Notifies the object that data is available, unblocking any blocking reads.
    void dataAvailable() { runCommand(s_dataAvailable); }

    /// Reads endlessly from port until an error is encountered, at which point the SerialPort
    /// object's `doneAction` runs.
    void doRead() {
        auto const&& buf = boost::asio::buffer(m_rxbuffer, kBufferSize);
        while (true) {
            auto txFuture = m_port.async_read_some(buf, boost::asio::use_future);
            try {
                // blocks until read completes
                auto bytesTransferred = txFuture.get();
                for (std::size_t index = 0; index != bytesTransferred; ++index) {
                    uint8_t byte = m_rxbuffer[index];
                    bool putSuccessful = m_rxfifo.push(byte);
                    if (!putSuccessful)
                        m_rxErrors++;
                }

                if (bytesTransferred > 0) {
                    dataAvailable();
                }
            } catch (const boost::system::system_error& e) {
                // TODO: pass info on error to sclang? ignore spurious errors?
                doneAction();
                return;
            }
        }
    }

    PyrObject* m_obj; ///< Language object representing this port.
    boost::asio::serial_port m_port; ///< Port object.
    Options m_options; ///< Serial interface options.
    SC_Thread m_readThread; ///< Polling thread.

    /// Count of read errors since last query
    std::atomic<int> m_rxErrors;

    /// I/O buffer
    FIFO m_rxfifo;

    /// Temp buffer for reads
    uint8_t m_rxbuffer[kBufferSize];
};

PyrSymbol* SerialPort::s_dataAvailable = nullptr;
PyrSymbol* SerialPort::s_doneAction = nullptr;

void SerialPort::setFlowControl(serial_port::flow_control::type control) {
    /* FIXME backported from 3.9 impl -- boost.asio doesn't correctly detect support for these features
     * on macOS and possibly Linux (https://github.com/boostorg/asio/issues/65). Remove this if that issue
     * is ever solved and we upgrade to that as minimum version. */
#if !defined(_WIN32) && (!defined(_POSIX_C_SOURCE) || defined(__USE_MISC))
    int fd = m_port.native_handle();
    termios toptions;
    if (::tcgetattr(fd, &toptions) < 0) {
        throw std::system_error(std::error_code(errno, std::system_category()));
    }

    if (control == serial_port::flow_control::hardware) {
        toptions.c_cflag &= ~CRTSCTS;
    } else {
        toptions.c_cflag |= CRTSCTS;
    }

    if (control == serial_port::flow_control::hardware) {
        toptions.c_iflag |= (IXON | IXOFF | IXANY);
    } else {
        toptions.c_iflag &= ~(IXON | IXOFF | IXANY);
    }

    if (::tcsetattr(fd, TCSAFLUSH, &toptions) < 0) {
        throw std::system_error(std::error_code(errno, std::system_category()));
    }
#else // !_WIN32 && ( !_POSIX_C_SOURCE || __USE_MISC )
    m_port.set_option(serial_port::flow_control(control));
#endif // !_WIN32 && ( !_POSIX_C_SOURCE || __USE_MISC )
}

void SerialPort::setExclusive(bool b) {
#ifndef _WIN32
    if (!b) {
        return;
    }

    int fd = m_port.native_handle();
    if (ioctl(fd, TIOCEXCL) == -1) {
        throw std::system_error(std::error_code(errno, std::system_category()));
    }
#endif // _WIN32
}

// ================================================================================================
// helpers

static SerialPort* getSerialPort(PyrSlot* slot) {
    if (NotPtr(&slotRawObject(slot)->slots[0]))
        return nullptr;
    return (SerialPort*)slotRawPtr(&slotRawObject(slot)->slots[0]);
}

/// Translates between SC parity IDs and boost::asio::serial_port parity types.
static serial_port::parity::type asParityType(int i) {
    using parity = serial_port::parity;
    switch (i) {
    case 0:
        return parity::none;
    case 1:
        return parity::even;
    case 2:
        return parity::odd;
    default:
        printf("*** WARNING: SerialPort: unknown parity: %d. Defaulting to none.\n", i);
        return parity::none;
    }
}

/// Translates between SC xonxoff/crtscts args and boost::asio::serial_port flow_control types.
/**
 * \note Throws std::runtime_error if both arguments are true.
 */
static serial_port::flow_control::type asFlowControlType(bool hardware, bool software) {
    using flow_control = serial_port::flow_control;

    if (hardware && software) {
        throw std::runtime_error("SerialPort: xonxoff and crtscts cannot both be true");
    } else if (hardware) {
        return flow_control::hardware;
    } else if (software) {
        return flow_control::software;
    } else {
        return flow_control::none;
    }
}

// ================================================================================================
// primitives

static int prSerialPort_Open(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* args = g->sp - 1 - SerialPort::kNumOptions;

    int err;

    PyrSlot* self = args + 0;

    if (getSerialPort(self))
        return errFailed;

    char portName[PATH_MAX];
    err = slotStrVal(args + 1, portName, sizeof(portName));
    if (err)
        return err;

    SerialPort::Options options {};
    SerialPort* port = nullptr;

    options.exclusive = IsTrue(args + 2);

    int baudrate;
    err = slotIntVal(args + 3, &baudrate);
    if (err)
        return err;
    options.baudrate = serial_port::baud_rate { static_cast<unsigned int>(baudrate) };

    int charsize;
    err = slotIntVal(args + 4, &charsize);
    if (err)
        return err;
    options.charsize = serial_port::character_size { static_cast<unsigned int>(charsize) };

    options.stop_bits = IsTrue(args + 5) ? serial_port::stop_bits::two : serial_port::stop_bits::one;

    int parity;
    err = slotIntVal(args + 6, &parity);
    if (err)
        return err;
    options.parity = asParityType(parity);

    // crtscts = use hardware signals for control flow
    bool useHardware = IsTrue(args + 7);
    // xonxoff = use XON/XOFF message bytes for control flow
    bool useSoftware = IsTrue(args + 8);
    options.flow_control = asFlowControlType(useHardware, useSoftware);

    try {
        port = new SerialPort(slotRawObject(self), portName, options);
    } catch (boost::system::system_error& e) {
        delete port;
        if (e.code().value() == boost::system::errc::no_such_file_or_directory) {
            throw std::runtime_error(std::string("SerialPort: port '") + portName + "' does not exist");
        } else {
            throw;
        }
    } catch (std::exception& e) {
        delete port;
        // TODO: check error types to provide better messages, such as when port doesn't exist
        throw;
    }

    SetPtr(slotRawObject(self)->slots + 0, port);

    return errNone;
}

/** \brief Stop a SerialPort object.
 *
 * When `close` is triggered, a complex sequence of events happens:
 * 1. Port is closed and SerialPort object is marked as closed
 * 2. This SC thread eventually yields, allowing the SerialPort's doneAction to run via read thread
 * 3. That doneAction causes a deferred call to `prSerialPort_Cleanup` and then yields
 * 4. `Cleanup` sets the dataptr to nil and destroys the C++ object, causing the read thread to join
 */
static int prSerialPort_Close(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* self = g->sp;
    SerialPort* port = getSerialPort(self);
    if (!port)
        return errFailed;
    port->stop();

    return errNone;
}

static int prSerialPort_Cleanup(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* self = g->sp;
    SerialPort* port = getSerialPort(self);

    if (!port)
        return errFailed;

    delete port;
    SetNil(slotRawObject(self)->slots + 0);
    return errNone;
}

static int prSerialPort_Next(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* self = g->sp;
    SerialPort* port = getSerialPort(self);
    if (!port)
        return errFailed;

    uint8_t byte;
    if (port->get(&byte)) {
        SetInt(self, byte);
    } else {
        SetNil(self);
    }

    return errNone;
}

static int prSerialPort_Put(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* args = g->sp - 1;

    PyrSlot* self = args + 0;
    SerialPort* port = getSerialPort(self);
    if (!port)
        return errFailed;

    PyrSlot* src = args + 1;

    int val;
    if (IsChar(src)) {
        val = slotRawChar(src);
    } else {
        int err = slotIntVal(src, &val);
        if (err)
            return err;
    }

    bool res = port->put(val & 0xFF);
    SetBool(self, res);

    return errNone;
}

static int prSerialPort_RXErrors(struct VMGlobals* g, int numArgsPushed) {
    PyrSlot* self = g->sp;
    SerialPort* port = getSerialPort(self);
    if (!port)
        return errFailed;

    SetInt(self, port->rxErrorsSinceLastQuery());
    return errNone;
}

void initSerialPrimitives() {
    int base, index;

    base = nextPrimitiveIndex();
    index = 0;

    definePrimitive(base, index++, "_SerialPort_Open", prSerialPort_Open, 2 + SerialPort::kNumOptions, 0);
    definePrimitive(base, index++, "_SerialPort_Close", prSerialPort_Close, 1, 0);
    definePrimitive(base, index++, "_SerialPort_Next", prSerialPort_Next, 1, 0);
    definePrimitive(base, index++, "_SerialPort_Put", prSerialPort_Put, 2, 0);
    definePrimitive(base, index++, "_SerialPort_RXErrors", prSerialPort_RXErrors, 1, 0);
    definePrimitive(base, index++, "_SerialPort_Cleanup", prSerialPort_Cleanup, 1, 0);

    SerialPort::s_dataAvailable = getsym("prDataAvailable");
    SerialPort::s_doneAction = getsym("prDoneAction");
}
