#include "thread.hpp"
#include "compat/sleep.hpp"
#include <utility>

#include <QTextStream>
#include <QTime>
#include <QDebug>

#include <cstring>

void hatire_thread::sendcmd_impl(const QByteArray &cmd)
{
#ifndef HATIRE_DEBUG_LOGFILE
    QByteArray Msg;

    if (cmd.length() > 0)
    {
        if (com_port.isOpen())
        {
            QString logMess;
            logMess.append("SEND '");
            logMess.append(cmd);
            logMess.append("'");
            Log(logMess);
            com_port.write(cmd);
            if (!com_port.waitForBytesWritten(1000)) {
                emit serial_debug_info_str(tr("Timeout during writing command"));
            } else
            {
                Msg.append("\r\n");
                Msg.append("SEND '");
                Msg.append(cmd);
                Msg.append("'\r\n");
            }
#if 0 // WaitForReadyRead isn't working well and there are some reports of it being a win32 issue. We can live without it anyway
            if  ( !com_port.waitForReadyRead(1000)) {
                emit serial_debug_info("TimeOut in response to CMD") ;
            } else {
                emit serial_debug_info(Msg);
            }
#else
            emit serial_debug_info(Msg);
#endif
        } else {
            emit serial_debug_info_str(tr("COM port not open"));
        }
    }
#endif
}

struct Diag final : public QFile
{
    Diag()
    {
        setFileName(QCoreApplication::applicationDirPath() + "/HATDiagnostics.txt");
    }
};

void hatire_thread::Log(const QString& message)
{
    // Drop out immediately if logging is off. Yes, there is still some overhead because of passing strings around for no reason.
    // that's unfortunate and I'll monitor the impact and see if it needs a more involved fix.
    if (!s.EnableLogging) return;

    Diag flDiagnostics;

    if (flDiagnostics.open(QIODevice::ReadWrite | QIODevice::Append))
    {
        QTextStream out(&flDiagnostics);
        QString milliSeconds;
        milliSeconds = QString("%1").arg(QTime::currentTime().msec(), 3, 10, QChar('0'));
        // We have a file
        out << QTime::currentTime().toString() << "." << milliSeconds << ": " << message << "\r\n";
        flDiagnostics.close();
    }
}

void hatire_thread::start()
{
    QThread::start();
}

hatire_thread::~hatire_thread()
{
    quit();
    wait();
}

hatire_thread::hatire_thread()
{
    data_read.reserve(65536);
    com_port.setReadBufferSize(2048);

    connect(this, &QThread::finished, this, &hatire_thread::teardown_serial, Qt::DirectConnection);
    connect(this, &hatire_thread::init_serial_port, this, &hatire_thread::init_serial_port_impl, Qt::QueuedConnection);
    connect(this, &hatire_thread::serial_info, this, &hatire_thread::serial_info_impl, Qt::QueuedConnection);
    connect(this, &hatire_thread::sendcmd, this, &hatire_thread::sendcmd_impl, Qt::QueuedConnection);
    connect(this, &hatire_thread::sendcmd_str, this, &hatire_thread::sendcmd_str_impl, Qt::QueuedConnection);

    //com_port.moveToThread(this);
#ifdef HATIRE_DEBUG_LOGFILE
    read_timer.moveToThread(this);
#endif

    connect(&com_port, &serial_t::readyRead, this, &hatire_thread::on_serial_read, Qt::DirectConnection);
}

void hatire_thread::teardown_serial()
{
    if (isRunning() && com_port.isOpen())
    {
        QByteArray msg;
        Log("Tracker shut down");
        com_port.write(to_latin1(s.CmdStop));
        if (!com_port.waitForBytesWritten(1000))
        {
            emit serial_debug_info("TimeOut in writing CMD");
        }
        else
        {
            msg.append("\r\n");
            msg.append("SEND '");
            msg.append(s.CmdStop);
            msg.append("'\r\n");
        }
        emit serial_debug_info(msg);

        disconnect(&com_port, SIGNAL(readyRead()), nullptr, nullptr);
        com_port.close();
    }
}

void hatire_thread::run()
{
#ifdef HATIRE_DEBUG_LOGFILE
    com_port.setFileName(HATIRE_DEBUG_LOGFILE);
    com_port.open(QIODevice::ReadOnly);

    read_timer.start(10);
    connect(&read_timer, &QTimer::timeout, this, &hatire_thread::on_serial_read, Qt::DirectConnection);
#endif
    (void) exec();

#ifdef HATIRE_DEBUG_LOGFILE
    read_timer.stop();
#endif
}

void hatire_thread::serial_debug_info_str(const QString& str)
{
    serial_debug_info(str.toUtf8());
}

serial_result hatire_thread::init_serial_port_impl()
{
#ifndef HATIRE_DEBUG_LOGFILE
    Log(tr("Setting serial port name"));
    com_port.setPortName(s.QSerialPortName);

    Log(tr("Opening serial port"));
    if (com_port.open(QIODevice::ReadWrite))
    {
        Log(tr("Port Open"));
        if (
            com_port.setBaudRate((QSerialPort::BaudRate)s.pBaudRate)
            && com_port.setDataBits((QSerialPort::DataBits)s.pDataBits)
            && com_port.setParity((QSerialPort::Parity)s.pParity)
            && com_port.setStopBits((QSerialPort::StopBits)s.pStopBits)
            && com_port.setFlowControl((QSerialPort::FlowControl)s.pFlowControl)
            && com_port.clear(QSerialPort::AllDirections)
            && com_port.setDataErrorPolicy(QSerialPort::IgnorePolicy)
           )
        {
            Log(tr("Port Parameters set"));
            qDebug()  << QTime::currentTime()
                      << " HAT OPEN on"
                      << com_port.portName()
                      << com_port.baudRate()
                      << com_port.dataBits()
                      << com_port.parity()
                      << com_port.stopBits()
                      << com_port.flowControl();

            if (com_port.flowControl() == QSerialPort::HardwareControl)
            {
                // Raise DTR
                Log(tr("Raising DTR"));
                if (!com_port.setDataTerminalReady(true))
                    Log("Couldn't set DTR");

                // Raise RTS/CTS
                Log(tr("Raising RTS"));
                if (!com_port.setRequestToSend(true))
                    Log("Couldn't set RTS");
            }
            // Wait init arduino sequence
            for (int i = 1; i <= s.DelayInit; i+=50)
            {
                if (com_port.waitForReadyRead(50)) break;
            }
            Log(tr("Waiting on init"));
            qDebug()  << QTime::currentTime()  << " HAT send INIT ";
            sendcmd_str(s.CmdInit);
            // Wait init MPU sequence
            for (int i = 1; i <= s.DelayStart; i+=50)
            {
                if (com_port.waitForReadyRead(50)) break;
            }
            // Send  START cmd to IMU
            qDebug()  << QTime::currentTime()  << " HAT send START ";
            sendcmd_str(s.CmdStart);

            // Wait start MPU sequence
            for (int i = 1; i <=s.DelaySeq; i+=50)
            {
                if (com_port.waitForReadyRead(50)) break;
            }
            Log(tr("Port setup, waiting for HAT frames to process"));
            qDebug()  << QTime::currentTime()  << " HAT wait MPU ";

            return serial_result();
        }
        else
        {
            return serial_result(result_error, com_port.errorString());
        }
    }
    else
        return serial_result(result_open_error, com_port.errorString());
#else
    return serial_result();
#endif
}

// Info SerialPort
void hatire_thread::serial_info_impl()
{
#ifndef HATIRE_DEBUG_LOGFILE
    QByteArray msg;

    if (com_port.isOpen())
    {
        msg.append("\r\n");
        msg.append(com_port.portName());
        msg.append("\r\n");
        msg.append("BAUDRATE :");
        msg.append(QString::number(com_port.baudRate()));
        msg.append("\r\n");
        msg.append("DataBits :");
        msg.append(QString::number(com_port.dataBits()));
        msg.append("\r\n");
        msg.append("Parity :");

        switch (com_port.parity())
        {
        case 0:  msg.append("No parity");
            break;
        case 2:  msg.append("Even parity");
            break;
        case 3:  msg.append("Odd parity");
            break;
        case 4:  msg.append("Space parity");
            break;
        case 5:  msg.append("Mark parity");
            break;
        default:  msg.append("Unknown parity");
            break;
        }

        msg.append("\r\n");
        msg.append("Stop Bits :");

        switch (com_port.stopBits())
        {
        case 1:  msg.append("1 stop bit.");
            break;
        case 2:  msg.append("2 stop bits.");
            break;
        case 3:  msg.append("1.5 stop bits.");
            break;
        default:  msg.append("Unknown number of stop bit.");
            break;
        }

        msg.append("\r\n");
        msg.append("Flow Control :");
        switch (com_port.flowControl())
        {
        case 0:  msg.append("No flow control");
            break;
        case 1:  msg.append("Hardware flow control (RTS/CTS)");
            break;
        case 2:  msg.append("Software flow control (XON/XOFF)");
            break;
        default:  msg.append("Unknown flow control");
            break;
        }

        emit serial_debug_info(msg);
    }
#endif
}

void hatire_thread::on_serial_read()
{
    static char buf[256];
    int sz;

#if !defined HATIRE_DEBUG_LOGFILE
    bool error = false, empty = false;
#endif

    {
        QMutexLocker lck(&data_mtx);

#ifndef HATIRE_DEBUG_LOGFILE
        sz = com_port.read(buf, sizeof(buf));
        error |= sz < 0;
        empty |= sz == 0;
#else
        const int sz = com_port.read(buf, sizeof(buf));

        if (sz <= 0 && read_timer.isActive())
        {
            if (sz < 0)
                qDebug() << "hatire: debug file read error" << com_port.errorString();
            qDebug() << "eof";
            read_timer.stop();
            return;
        }
#endif
    }

#if !defined HATIRE_DEBUG_LOGFILE
    if (error || com_port.error() != QSerialPort::NoError)
    {
        once_only(qDebug() << "hatire serial: error num" << com_port.error() << "num2" << error);
        com_port.clearError(); // XXX must test it
    }
    else if (empty)
        once_only(qDebug() << "hatire serial: empty");
    else
        goto ok;
#endif

    goto fail;

ok:
    data_read.append(buf, sz);

    using namespace time_units;

    stat.input(prog1(timer.elapsed<ms>().count(), timer.start()));

    if (throttle_timer.elapsed_seconds() >= 1)
    {
        throttle_timer.start();
        qDebug() << "hatire stat:" << "avg" << stat.avg() << "stddev" << stat.stddev();
    }

    if (!s.serial_bug_workaround)
        return;

fail:
    // qt can fire QSerialPort::readyRead() needlessly, causing a busy loop.
    // see https://github.com/opentrack/opentrack/issues/327#issuecomment-207941003

    once_only(qDebug() << "hatire: sleeping due to error, pinout:" << int(com_port.pinoutSignals()));

    {
        constexpr int hz = 90;
        constexpr int ms = 1000/hz;
        portable::sleep(ms);
    }
}

QByteArray& hatire_thread::send_data_read_nolock()
{
    return data_read;
}