#include <QFileDialog>
#include <QFile>
#include <QSerialPortInfo>
#include "qpsxserial.h"
#include "ui_qpsxserial.h"

#define LOST_PACKET_TIMEOUT 3000
#define CONNECT_TO_PSX_TIMEOUT 500

QPSXSerial::QPSXSerial(QWidget *parent, APP_INTERFACE interface) :
    QMainWindow(parent),
    ui(new Ui::QPSXSerial),
    stdout_ui(new Ui::Stdout_Console),
    stdout_dialog(new QDialog),
    app_interface(interface),
    ack(false),
    exe_sent(false),
    write_ready(false),
    byte_sent_received(false),
    first_entered(false),
    disable_psx_stdout(false)
{
    connect(&lost_packet_timer, SIGNAL(timeout()), this, SLOT(onPacketLost()));
    connect(&serial, SIGNAL(bytesWritten(qint64)), this, SLOT(onBytesWritten(qint64)));
    connect(&serial, SIGNAL(readyRead()), this, SLOT(onReadyRead(void)));

    init_timer.setSingleShot(true);
    init_timer.setInterval(CONNECT_TO_PSX_TIMEOUT);

    connect(&init_timer, SIGNAL(timeout(void)), this, SLOT(connectToPSXTimeout(void)));

    if (app_interface == GUI_APP)
    {
        ui->setupUi(this);

        connect(ui->stdout_Button, SIGNAL(released()), this, SLOT(onStdOutButtonReleased()));
        connect(ui->loadFile_Btn, SIGNAL(released()), this, SLOT(onLoadFileBtnReleased()));
        connect(ui->updatePorts_Btn, SIGNAL(released()), this, SLOT(onUpdatePortsBtnReleased()));
        connect(ui->send_Btn, SIGNAL(released()), this, SLOT(onSendBtnReleased()));
        connect(ui->ports_ComboBox, SIGNAL(currentIndexChanged(QString)), this, SLOT(onPortSelectedComboBox(QString)));

        ui->exeProgressBar->setVisible(false);
        ui->send_Btn->setEnabled(false);
        ui->inFileName->setVisible(false);

        stdout_ui->setupUi(stdout_dialog);

        connect(stdout_ui->clean_Btn, SIGNAL(released()), stdout_ui->stdout_Log, SLOT(clear()));
        connect(stdout_ui->close_Btn, SIGNAL(released()), stdout_dialog, SLOT(close()));
        connect(this, SIGNAL(debug_frame_received(QString)), stdout_ui->stdout_Log, SLOT(append(QString)));

        setWindowTitle( "QPSXSerial "
                        + QString(QPSXSERIAL_VERSION_STR) );
    }
}

QPSXSerial::~QPSXSerial()
{
    delete ui;
}

void QPSXSerial::onStdOutButtonReleased(void)
{
    stdout_dialog->show();
}

void QPSXSerial::showError(QString error)
{
    QMessageBox box(QMessageBox::Critical, "QPSXSerial serror", error);

    box.show();
}

void QPSXSerial::onLoadFileBtnReleased(void)
{
    selectedFolder = QFileDialog::getExistingDirectory( this,
                                                        "Load folder with PSX data",
                                                        "C:/",
                                                        QFileDialog::ShowDirsOnly   );

    if (selectedFolder.isEmpty())
    {
        return;
    }

    ui->loadedFile_LineEdit->setText(selectedFolder);

    inputExe = getInputExeFromFolder(&selectedFolder);

    if (selectedPort.isEmpty() == false)
    {
        ui->send_Btn->setEnabled(true);
    }
}

void QPSXSerial::onUpdatePortsBtnReleased(void)
{
    ui->ports_ComboBox->clear();

    foreach(QSerialPortInfo port, QSerialPortInfo::availablePorts())
    {
        ui->ports_ComboBox->addItem(port.portName());
    }
}

void QPSXSerial::onSendBtnReleased(void)
{
    if (serial.isOpen() == false)
    {
        if (selectedPort.isEmpty())
        {
            showGUICLIerror("No selected port!");
            return;
        }

        serial.setPortName(selectedPort);
        serial.setBaudRate(QSerialPort::Baud115200);
        serial.setParity(QSerialPort::NoParity);
        serial.setDataBits(QSerialPort::Data8);
        serial.setStopBits(QSerialPort::OneStop);

        if (serial.open(QIODevice::ReadWrite) == false)
        {
            showGUICLIerror("Could not open port " + selectedPort);
            return;
        }

        init_timer.start();

        if (app_interface == GUI_APP)
        {
            ui->exeProgressBar->setVisible(true);
            ui->exeProgressBar->setMinimum(0);
            ui->exeProgressBar->setMaximum(0);
            ui->inFileName->setText("Waiting for response from the device...");
            ui->inFileName->setVisible(true);
            ui->send_Btn->setText("Disconnect from PSX");
        }
        else
        {
            printf("Connected to port successfully.\n");
            printf("Waiting for response from the device...\n");
        }
    }
    else
    {
        if (app_interface == GUI_APP)
        {
            ui->send_Btn->setText("Send to PSX!");
            ui->exeProgressBar->setVisible(false);
            ui->inFileName->clear();
        }

        serial.close();
        first_entered = false;
        exe_sent = false;
        lost_packet_timer.stop();
        init_timer.stop();
    }
}

void QPSXSerial::connectToPSXTimeout(void)
{
    QByteArray ba;

    if (ack == false)
    {
        ba.append(99);

        serial.write(ba);

        init_timer.start(CONNECT_TO_PSX_TIMEOUT);
    }
}

void QPSXSerial::onPortSelectedComboBox(QString port)
{
    selectedPort = port;

    if (inputExe.isEmpty() == false)
    {
        ui->send_Btn->setEnabled(true);
    }
}

void QPSXSerial::onBytesWritten(qint64)
{
    write_ready = true;
}

void QPSXSerial::onReadyRead(void)
{
    const QByteArray data = serial.readAll();
    static QTime time = QTime::currentTime();
    static bool cdrom_petition;

    if (exe_sent)
    {
        static QByteArray fileName;

        if (data.contains("#"))
        {
            cdrom_petition = true;
            fileName.clear();
        }

        if (data.count() == 1)
        {
            if (data.at(0) == 'b')
            {
                ack = true;
                return;
            }
        }

        qDebug() << data;

        if (cdrom_petition)
        {
            if (data.contains("#"))
            {
                int initial_i = data.indexOf("#");

                qDebug() << initial_i;

                fileName.append(data.mid(initial_i, data.count() - initial_i));
            }
            else
            {
                fileName.append(data);
            }

            if (fileName.contains("@"))
            {
                int terminator_i = fileName.indexOf("@");

                if (terminator_i >= fileName.count())
                {
                    fileName.chop(terminator_i - fileName.count() - 1);
                }

                cdrom_petition = false;

                qDebug() << "INPUT FRAME: " + fileName;
                QString filePath = QString(fileName);

                filePath.remove("@");
                filePath.remove("#");

                filePath.remove("cdrom:\\");

                qDebug() << "selectedFolder = " + selectedFolder;

                filePath.prepend(selectedFolder);

                filePath.replace("\\", "/");

                filePath.chop(2); // Remove ending ";1"

                qDebug() << "filePath = " + filePath;

                QFile f(filePath);

                if (f.open(QFile::ReadOnly) == false)
                {
                    qDebug() << "Error while reading input file!";
                    return;
                }

                QByteArray file_data = f.readAll();
                quint32 sz = file_data.count();

                sendDataSize(sz);

                if (serial.waitForReadyRead() == false)
                {
                    qDebug() << "Did not receive any ACK!";
                }
                else
                {
                    qDebug() << "sendData for file...";
                    onReadyRead();

                    sendData(file_data, filePath);

                    f.close();
                }
            }
        }
        else
        {
            if (app_interface == GUI_APP)
            {
                emit debug_frame_received(QString(data));
            }
            else if (app_interface == CLI_APP)
            {
                if (disable_psx_stdout == false)
                {
                    const char* string_received = data.toStdString().c_str();

                    printf("%s\n", string_received);
                }
            }
        }
    }

    if (data.isEmpty() == false)
    {
        quint8 data_byte = static_cast<quint8>(data.at(0));

        if (data_byte == 'b')
        {
            // Received handshaking byte. Start transmission!

            ack = true;

            init_timer.stop();

            if (first_entered == false)
            {
                first_entered = true;

                if (sendExe() == false)
                {
                    qDebug() << "An error happened when sending EXE file!";
                }

                exe_sent = true;
            }
        }
    }

    time = QTime::currentTime();
}

bool QPSXSerial::sendExe(void)
{
    QFile f(inputExe);

    if (f.open(QFile::ReadOnly) == false)
    {
        qDebug() << "Could not open input EXE file!";
        return false;
    }

    f.seek(0);

    ack = false;

    QByteArray data = f.read(2048 /* PSX-EXE header */);

    if (app_interface == GUI_APP)
    {
        ui->inFileName->setText("Sending PSX-EXE size...");
    }
    else if (app_interface == CLI_APP)
    {
        printf("Sending PSX-EXE header data...\n");
    }

    // PSX-EXE header is actually 2048 bytes long, but initial 32 bytes
    // contain all the information we need for this.
    for (int i = 0; i < 32; i+=4)
    {
        QByteArray send;
        write_ready = false;

        QThread::msleep(100);

        QApplication::processEvents();

        qDebug() << "Sending " + data.mid(i, 4).toHex();

        send.append(data.mid(i, 4));

        serial.write(send);
    }

    if (app_interface == GUI_APP)
    {
        ui->inFileName->setText("Sending PSX-EXE size...");
    }
    else if (app_interface == CLI_APP)
    {
        printf("Sending PSX-EXE size...\n");
    }

    if (serial.waitForReadyRead() == false)
    {
        qDebug() << "Did not receive any ACK!";
    }
    else
    {
        onReadyRead();
    }

    qDebug () << "Sending EXE size...";

    qint64 sz = f.size() - 2048;

    sendDataSize(static_cast<quint32>(sz));

    if (serial.waitForReadyRead() == false)
    {
        qDebug() << "Did not receive any ACK!";
    }
    else
    {
        onReadyRead();
    }

    // Send file size without header

    qDebug() << "Dump EXE data...";

    f.seek(2048);

    data = f.readAll();

    qDebug() << data.count();

    sendData(data, inputExe);

    f.close();

    qDebug() << "PSX-EXE sent successfully!";

    return true;
}

void QPSXSerial::sendDataSize(quint32 size)
{
    QByteArray ar;

    for (unsigned int i = 0; i < sizeof(quint32); i++)
    {
        char send = (char)( size >> (i << 3) );

        QByteArray send_arr;

        send_arr.append(send);

        ar.append(send);

        serial.write(send_arr);

        while (write_ready == false)
        {
            QApplication::processEvents();
        }
    }

    /*for (unsigned int i = 0; i < sizeof(quint32); i++)
    {
        qDebug() << "0x" + QString::number(ar.at(i), 16);
    }*/

    ack = false;
}

void QPSXSerial::onPacketLost(void)
{
    qDebug() << "Entering onPacketLost...";

    if (ack == false)
    {
        serial.write(last_packet_sent);
        lost_packet_timer.start(LOST_PACKET_TIMEOUT);
    }
}

void QPSXSerial::sendData(QByteArray data, QString fileName)
{
    static int last_i;

    last_i = 0;

    if (app_interface == GUI_APP)
    {
        ui->exeProgressBar->setVisible(true);
        ui->exeProgressBar->setRange(0, data.count());
        ui->inFileName->setText(fileName);
        ui->inFileName->setVisible(true);
    }

    lost_packet_timer.setInterval(LOST_PACKET_TIMEOUT);
    lost_packet_timer.setSingleShot(true);

    for (int i = 0; i < data.count(); i+= 8)
    {
        QByteArray send;
        ack = false;

        send.append(data.mid(i, 8));

        //qDebug() << "Sent packet";

        QApplication::processEvents();

        serial.write(send);

        last_packet_sent = send;

        lost_packet_timer.start(LOST_PACKET_TIMEOUT);

        if (serial.waitForReadyRead() == false)
        {
            qDebug() << "Did not receive any ACK!";
        }
        else
        {
            onReadyRead();
        }

        if (app_interface == GUI_APP)
        {
            ui->exeProgressBar->setValue(i);
        }
        else if (app_interface == CLI_APP)
        {
            if ( ( (i - last_i) > (data.count() >> 7))
                            ||
                (i > (data.count() - (data.count() >> 7) ) ) )
            {
                int j;
                bool draw_arrow = true;

                // Fancy, CLI progress bar

                printf("\r");

                printf("|");

                for (j = 0; j < data.count(); j += data.count() >> 5)
                {
                    if (i > j)
                    {
                        printf("=");
                    }
                    else if (i < j)
                    {
                        if (draw_arrow)
                        {
                            draw_arrow = false;
                            printf(">");
                        }
                        printf(" ");
                    }
                    else
                    {
                        printf(">");
                    }
                }

                printf("|");

                printf("\t%d/%d bytes sent...", i, data.count());

                last_i = i;
            }
        }
    }

    printf("\n");

    lost_packet_timer.stop();

    if (app_interface == GUI_APP)
    {
        ui->exeProgressBar->setValue(data.count());
        ui->inFileName->setText("Transfer complete!");
    }
    else if (app_interface == CLI_APP)
    {
        printf("Transfer complete!\n");
    }

    ack = false;
}

void QPSXSerial::cli_run(void)
{
    QStringList allowed_flags;
    bool correct_flag_used = false;

    allowed_flags.append("--help");
    allowed_flags.append("--port");
    allowed_flags.append("--inpath");

    foreach(QString flag, allowed_flags)
    {
        if (_paramlist.contains(flag))
        {
            correct_flag_used = true;
            break;
        }
    }

    if ( (_paramlist.count() < 2)                ||
        (_paramlist.contains("--help")) ||
        (correct_flag_used == false)                )
    {
        showHelp();

        emit finished();

        return;
    }

    // Check specified port

    int port_i = _paramlist.indexOf("--port");

    if (port_i == -1)
    {
        printf("No port specified! Please include \"--port\" flag and port name e.g.: \"--port COM7\"\n");

        emit finished();
        return;
    }

    if (_paramlist.count() > (++port_i ) )
    {
        selectedPort = _paramlist.at(port_i);
    }
    else
    {
        printf("No port name specified! Please write port name e.g.: \"--port COM7\"\n");
        emit finished();
        return;
    }

    // Check specified folder

    int folder_i = _paramlist.indexOf("--input_folder");

    if (folder_i == -1)
    {
        printf("No input folder has been specified. Please include \"--help\" for further reference"
               " about QPSXSerial usage.\n");
        emit finished();
        return;
    }

    if (_paramlist.count() > (++folder_i ) )
    {
        selectedFolder = _paramlist.at(folder_i);
    }
    else
    {
        printf( "No input folder path! Please write valid folder path e.g.: "
                "--input-folder ~/MyGame/cdimg\n");
        emit finished();
        return;
    }

    inputExe = getInputExeFromFolder(&selectedFolder);

    if (inputExe.isEmpty())
    {
        printf("Could not find PSX-EXE from specified input folder.\n");
        emit finished();
        return;
    }

    // Check optional flags

    int psx_sdtout_flag = _paramlist.indexOf("--disable_psx_stdout");

    if (psx_sdtout_flag != -1)
    {
        disable_psx_stdout = true;
    }
    else
    {
        disable_psx_stdout = false;
    }

    onSendBtnReleased();
}

void QPSXSerial::showHelp(void)
{
    printf("##########################\n");
    printf("QPSXSerial version %s\n", QPSXSERIAL_VERSION_STR);
    printf("##########################\n");
    printf("This application allows uploading PSX-EXE files to a PlayStation 1\n");
    printf("console (also known as PSX) using serial port interface.\n");
    printf("To be used together with OpenSend, available on the link below:\n");

    printf("\n%s\n\n", OPENSEND_URL);
    printf("Report any bugs, issues or suggestions on the official Github repository for QPSXSerial:\n");
    printf("\n%s\n\n", QPSXSERIAL_URL);

    printf("Usage:\n\n");

    printf("QPSXSerial --input_folder inpath --port PORTNAME [options]\n\n");

    printf("The following options are available:\n");
    printf("--disable_psx_stdout\t\tDisables stdout messages from console to PC\n\n");

    printf("Additionally to PSX-EXEs, QPSXSerial can also send external files e.g.:\n"
           "*.TIM or *.VAG files whenever the game requests such data.\n");
    printf("A working example of this bidirectional communication can be found on my open-source\n");
    printf("video game \"Airport\", available on the following Github repository:\n");
    printf("\nhttps://github.com/XaviDCR92/Airport/\n\n");
    printf("QPSXSerial, OpenSend and Airport written by Xavier Del Campo (aka Xavi92).\n");
    printf("All software released under the General Public License (GPL-3.0).\n");
    printf("This application has been created using Qt toolkit %s\n", QT_VERSION_STR);
    printf("\nINFO: In case you do not set any parameters, GUI is enabled by default.\n");
}

QString QPSXSerial::getInputExeFromFolder(QString* folder)
{
    QFile system_f(*folder + "\\SYSTEM.CNF");

    QString fileSelected;
    QString retExe;

    if ( (folder->endsWith("/") == false) && (folder->endsWith("\\") == false) )
    {
        folder->append("/");
    }

    if (system_f.exists() == false)
    {
        QDir d(*folder);
        QStringList filters;

        filters.append("*.EXE");
        filters.append("*.exe");

        QStringList exe_list = d.entryList(filters);

        if (exe_list.isEmpty())
        {
            retExe.clear();
            return retExe;
        }

        if (exe_list.contains("PSX.EXE"))
        {
            fileSelected = "PSX.EXE";
        }
        else
        {
            fileSelected = exe_list.first();
        }
    }
    else
    {
        if (system_f.open(QFile::ReadOnly) == false)
        {
            QString error = "Could not open " + *folder + "\\SYSTEM.CNF";
            showGUICLIerror(error);

            retExe.clear();
            return retExe;
        }

        QTextStream system_txt(&system_f);

        do
        {
            QString line = system_txt.readLine();

            QStringList tokens = line.split("=");

            if (tokens.isEmpty())
            {
                continue;
            }

            if (tokens.at(0).contains("BOOT") == false)
            {
                continue;
            }

            fileSelected = tokens.at(1).split("\\").at(1);

            fileSelected.chop(2); // Remove ";1" suffix.
            break;

        }while (system_txt.atEnd() == false);
    }


    retExe = *folder + fileSelected;

    return retExe;
}

void QPSXSerial::showGUICLIerror(QString error)
{
    if (app_interface == GUI_APP)
    {
        showError(error);
    }
    else if (app_interface == CLI_APP)
    {
        const char* c_str_error = error.toStdString().c_str();

        printf("%s\n", c_str_error);
    }
}
