#include "commhandler.h"
#include "logcategories.h"

#include <QDebug>

// Copyright 2017-2020 Elliott H. Liggett

commHandler::commHandler(QObject* parent) : QObject(parent)
{
    //constructor
    // TODO: The following should become arguments and/or functions
    // Add signal/slot everywhere for comm port setup.
    // Consider how to "re-setup" and how to save the state for next time.
    baudrate = 115200;
    stopbits = 1;
    portName = "/dev/ttyUSB0";
    this->pttType = pttCIV;
    this->halfDuplex=false;
    init();
}

commHandler::commHandler(QString portName, quint32 baudRate, quint8 wfFormat, QObject* parent) : QObject(parent)
{
    //constructor
    // grab baud rate and other comm port details
    // if they need to be changed later, please
    // destroy this and create a new one.

    if (wfFormat == 1) { // Single waterfall packet
        combineWf = true;
        qDebug(logSerial()) << "Combine Waterfall Mode Enabled!";
    }

    // TODO: The following should become arguments and/or functions
    // Add signal/slot everywhere for comm port setup.
    // Consider how to "re-setup" and how to save the state for next time.
    baudrate = baudRate;
    stopbits = 1;
    this->portName = portName;
    this->pttType = pttCIV;
    init();

}

void commHandler::init()
{
    if (port != Q_NULLPTR) {
        delete port;
        port = Q_NULLPTR;
        isConnected = false;
    }

    port = new QSerialPort();
    setupComm(); // basic parameters
    openPort();
    // qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize();
    //port->setReadBufferSize(1024); // manually. 256 never saw any return from the radio. why...
    //qInfo(logSerial()) << "Serial buffer size: " << port->readBufferSize();

    connect(port, SIGNAL(readyRead()), this, SLOT(receiveDataIn()));
#if (QT_VERSION < QT_VERSION_CHECK(6,0,0))
    connect(port, SIGNAL(error(QSerialPort::SerialPortError)), this, SLOT(handleError(QSerialPort::SerialPortError)));
#else
    connect(port, SIGNAL(errorOccurred(QSerialPort::SerialPortError)), this, SLOT(handleError(QSerialPort::SerialPortError)));
#endif

    connect(this, SIGNAL(sendDataOutToPort(const QByteArray&)),this,SLOT(sendDataOut(const QByteArray &)));
    lastDataReceived = QTime::currentTime();
}

commHandler::~commHandler()
{
    qInfo(logSerial()) << "Closing serial port: " << port->portName();
    if (isConnected) {
        this->closePort();
    }
    delete port;
}

void commHandler::setHalfDuplex(bool en)
{
    halfDuplex=en;
    qInfo(logSerial()) << "Setting half duplex comms: " << ((en) ? "Enabled":"Disabled");
}

void commHandler::setupComm()
{
    serialError = false;
    port->setPortName(portName);
    port->setBaudRate(baudrate);
    port->setStopBits(QSerialPort::OneStop);// OneStop is other option
}

void commHandler::receiveDataFromUserToRig(const QByteArray &data)
{
    sendDataOut(data);
}

void commHandler::sendDataOut(const QByteArray &writeData)
{
    if (halfDuplex)
    {
        QMutexLocker locker(&duplexMutex);
    }

    // Recycle port to attempt reconnection.
    if (lastDataReceived.msecsTo(QTime::currentTime()) > 10000) {
        qDebug(logSerial()) << "Serial port error? Attempting reconnect...";
        lastDataReceived = QTime::currentTime();
        QTimer::singleShot(100, this, SLOT(init()));
        QTimer::singleShot(500, this, [=]() { sendDataOut(writeData); });
        return;
    }

    mutex.lock();

    previousSent = writeData;

    qint64 bytesWritten;

    if(pttType != pttCIV)
    {
        // Size:    1  2  3    4    5    6    7    8
        //index:    0  1  2    3    4    5    6    7
        //Query:   FE FE TO FROM 0x1C 0x00 0xFD
        //PTT On:  FE FE TO FROM 0x1C 0x00 0x01 0xFD
        //PTT Off: FE FE TO FROM 0x1C 0x00 0x00 0xFD

        if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\xFD")))
        {
            // Query
            bool ptt = false;

            if (pttType == pttRTS)
                ptt = port->isRequestToSend();
            else if (pttType == pttDTR)
                ptt = port->isDataTerminalReady();

            //qDebug(logSerial()) << "Looks like PTT Query, ptt is:"<<ptt;

            QByteArray pttreturncmd = QByteArray("\xFE\xFE");
            pttreturncmd.append(writeData.at(3));
            pttreturncmd.append(writeData.at(2));
            pttreturncmd.append(QByteArray("\x1C\x00", 2));
            pttreturncmd.append((char)ptt);
            pttreturncmd.append("\xFD");

            emit haveDataFromPort(pttreturncmd);
            
            mutex.unlock();
            return;
        } else if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\x01\xFD")))
        {
            // PTT ON
            if (pttType == pttRTS) {
                port->setRequestToSend(true);
            }
            else if (pttType == pttDTR) {
                port->setDataTerminalReady(true);
            }
            mutex.unlock();
            return;
        } else if(writeData.endsWith(QByteArrayLiteral("\x1C\x00\x00\xFD")))
        {
            // PTT OFF
            if (pttType == pttRTS) {
                port->setRequestToSend(false);
            }
            else if (pttType == pttDTR) {
                port->setDataTerminalReady(false);
            }

            mutex.unlock();
            return;
        }
    }

    bytesWritten = port->write(writeData);
    
    if(bytesWritten != (qint64)writeData.size())
    {
    qDebug(logSerial()) << "bytesWritten: " << bytesWritten << " length of byte array: " << writeData.length()\
             << " size of byte array: " << writeData.size()\
             << " Wrote all bytes? " << (bool) (bytesWritten == (qint64)writeData.size());
    }

    mutex.unlock();
}

void commHandler::receiveDataIn()
{
    if (halfDuplex)
    {
        QMutexLocker locker(&duplexMutex);
    }
    // connected to comm port data signal

    // Here we get a little specific to CIV radios
    // because we know what constitutes a valid "frame" of data.

    // new code:
    lastDataReceived = QTime::currentTime();
    port->startTransaction();
    inPortData = port->readAll();

    if (inPortData.startsWith("\xFC\xFC\xFC\xFC\xFC"))
    {
        // Collision detected by remote end, re-send previous command.
        qInfo(logSerial()) << "COLLISION, resending:"<<previousSent.toHex(' ');
        port->commitTransaction();
        emit sendDataOutToPort(previousSent);
        return;
    }

    if(inPortData.size() == 1)
    {
        // Generally for baud <= 9600
        if (inPortData == "\xFE")
        {
            // This will get hit twice.
            // After the FE FE, we transition into
            // the normal .startsWith FE FE block
            // where the normal rollback code can handle things.
            port->rollbackTransaction();
            rolledBack = true;
            return;
        }
    }

    if (inPortData.startsWith("\xFE\xFE"))
    {
        if(inPortData.contains("\xFC"))
        {
            qInfo(logSerial()) << "Transaction contains collision data. Dumping.";
            //printHex(inPortData, false, true);
            port->commitTransaction();
            return;
        }
        if(inPortData.endsWith("\xFD"))
        {
            // good!
            port->commitTransaction();

            //payloadIn = data.right(data.length() - 4);

            // Do we need to combine waterfall into single packet?
            //int combined = 0;
            if (combineWf) {
                int pos = inPortData.indexOf(QByteArrayLiteral("\x27\x00\x00"));
                int fdPos = inPortData.mid(pos).indexOf(QByteArrayLiteral("\xfd"));
                //printHex(inPortData, false, true);
                while (pos > -1 && fdPos > -1) {

                    spectrumDivisionNumber = inPortData[pos + 3] & 0x0f;
                    spectrumDivisionNumber += ((inPortData[pos + 3] & 0xf0) >> 4) * 10;

                    if (spectrumDivisionNumber == 1) {
                        // This is the first waveform data.
                        spectrumDivisionMax = 0;
                        spectrumDivisionMax = inPortData[pos + 4] & 0x0f;
                        spectrumDivisionMax += ((inPortData[pos + 4] & 0xf0) >> 4) * 10;
                        spectrumData.clear();
                        spectrumData = inPortData.mid(pos - 4, fdPos+4); // Don't include terminating FD
                        spectrumData[8] = spectrumData[7]; // Make max = current;
                        //qDebug() << "New Spectrum seq:" << spectrumDivisionNumber << "pos = " << pos << "len" << fdPos;

                    }
                    else  if (spectrumDivisionNumber > lastSpectrum && spectrumDivisionNumber <= spectrumDivisionMax) {
                        spectrumData.insert(spectrumData.length(), inPortData.mid(pos + 5, fdPos-5));
                        //qInfo() << "Added spectrum seq:" << spectrumDivisionNumber << "len" << fdPos-5<< "Spec" << spectrumData.length();
                        //printHex(inPortData.mid((pos+4),fdPos - (pos+5)), false, true);
                    }
                    else {
                        qDebug() << "Invalid Spectrum Division received" << spectrumDivisionNumber << "last Spectrum" << lastSpectrum;
                    }

                    lastSpectrum = spectrumDivisionNumber;
                        
                    if (spectrumDivisionNumber == spectrumDivisionMax) {
                        //qDebug() << "Got Spectrum! length=" << spectrumData.length();
                        spectrumData.append("\xfd"); // Need to add FD on the end.
                        //printHex(spectrumData, false, true);
                        emit haveDataFromPort(spectrumData);
                        lastSpectrum = 0;
                    }
                    inPortData = inPortData.remove(pos-4, fdPos+5);
                    pos = inPortData.indexOf(QByteArrayLiteral("\x27\x00\x00"));
                    fdPos = inPortData.mid(pos).indexOf(QByteArrayLiteral("\xfd"));
                }
                // If we still have data left, let the main function deal with it, any spectrum data has been removed
                if (inPortData.length() == 0)
                {
                    return;
                }
               // qDebug() << "Got extra data!";
                //printHex(inPortData, false, true);
            }
            // 
            emit haveDataFromPort(inPortData);

            if(rolledBack)
            {
                // qInfo(logSerial()) << "Rolled back and was successful. Length: " << inPortData.length();
                //printHex(inPortData, false, true);
                rolledBack = false;
            }
        } else {
            // did not receive the entire thing so roll back:
            // qInfo(logSerial()) << "Rolling back transaction. End not detected. Length: " << inPortData.length();
            //printHex(inPortData, false, true);
            port->rollbackTransaction();
            rolledBack = true;
        }
    } else {
        port->commitTransaction(); // do not emit data, do not keep data.
        //qInfo(logSerial()) << "Warning: received data with invalid start. Dropping data.";
        //qInfo(logSerial()) << "THIS SHOULD ONLY HAPPEN ONCE!!";
        // THIS SHOULD ONLY HAPPEN ONCE!

        // unrecoverable. We did not receive the start and must
        // have missed it earlier because we did not roll back to
        // preserve the beginning.

        //printHex(inPortData, false, true);

    }
}

void commHandler::setPTTType(pttType_t ptt)
{
    this->pttType = ptt;
}

void commHandler::openPort()
{
    bool success;
    success = port->open(QIODevice::ReadWrite);
    if(success)
    {
        port->setDataTerminalReady(false);
        port->setRequestToSend(false);
        isConnected = true;
        qInfo(logSerial()) << "Opened port: " << portName;
        return;
    } else {
        qInfo(logSerial()) << "Could not open serial port " << portName << " , please check Radio Access under Settings.";
        isConnected = false;
        serialError = true;
        emit havePortError(errorType(true, portName, "Could not open Serial or USB port.\nPlease check Radio Access under Settings."));
        return;
    }
}

void commHandler::closePort()
{
    if(port)
    {
        port->close();
        //delete port;
    }
    isConnected = false;
}

void commHandler::debugThis()
{
    // Do not use, function is for debug only and subject to change.
    qInfo(logSerial()) << "comm debug called.";

    inPortData = port->readAll();
    emit haveDataFromPort(inPortData);
}


void commHandler::printHex(const QByteArray &pdata, bool printVert, bool printHoriz)
{
    qDebug(logSerial()) << "---- Begin hex dump -----:";
    QString sdata("DATA:  ");
    QString index("INDEX: ");
    QStringList strings;

    for(int i=0; i < pdata.length(); i++)
    {
        strings << QString("[%1]: %2").arg(i,8,10,QChar('0')).arg((quint8)pdata[i], 2, 16, QChar('0'));
        sdata.append(QString("%1 ").arg((quint8)pdata[i], 2, 16, QChar('0')) );
        index.append(QString("%1 ").arg(i, 2, 10, QChar('0')));
    }

    if(printVert)
    {
        for(int i=0; i < strings.length(); i++)
        {
            qDebug(logSerial()) << strings.at(i);
        }
    }

    if(printHoriz)
    {
        qDebug(logSerial()) << index;
        qDebug(logSerial()) << sdata;
    }
    qDebug(logSerial()) << "----- End hex dump -----";
}


void commHandler::handleError(QSerialPort::SerialPortError err)
{
    switch (err) {
        case QSerialPort::NoError:
            break;
        default:
            if (lastDataReceived.msecsTo(QTime::currentTime()) > 2000) {
                qDebug(logSerial()) << "Serial port" << port->portName() << "Error, attempting disconnect/reconnect";
                lastDataReceived = QTime::currentTime();
                QTimer::singleShot(500, this, SLOT(init()));
            }
            break;
    }
}
