/*
 * Copyright (c) 2021, Technische Universität Kaiserslautern
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 * 1. Redistributions of source code must retain the above copyright notice,
 *    this list of conditions and the following disclaimer.
 *
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * 3. Neither the name of the copyright holder nor the names of its
 *    contributors may be used to endorse or promote products derived from
 *    this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
 * TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * Authors:
 *    Derek Christ
 */

#include "simulationdialog.h"
#include "DRAMSys/config/TraceSetup.h"

#include <QDateTime>
#include <QDebug>
#include <QFile>
#include <QFileDialog>
#include <QMessageBox>
#include <QRegularExpression>
#include <optional>

SimulationDialog::SimulationDialog(QWidget *parent) : QWidget(parent), ui(new Ui::SimulationDialog)
{
    ui->setupUi(this);

    showStopButton(false);

    // Try to find path to DRAMSys
    {
        QFileInfo fileInfo;

        fileInfo.setFile("../simulator/DRAMSys");
        if (fileInfo.isFile())
            ui->dramSysPath->setText(fileInfo.absoluteFilePath());

        fileInfo.setFile("../simulator/DRAMSys.exe");
        if (fileInfo.isFile())
            ui->dramSysPath->setText(fileInfo.absoluteFilePath());

        fileInfo.setFile("simulator/DRAMSys");
        if (fileInfo.isFile())
            ui->dramSysPath->setText(fileInfo.absoluteFilePath());

        fileInfo.setFile("simulator/DRAMSys.exe");
        if (fileInfo.isFile())
            ui->dramSysPath->setText(fileInfo.absoluteFilePath());
    }

    ui->outputDirLineEdit->setText(QDir::currentPath());
}

void SimulationDialog::on_browseDramSysButton_clicked()
{
    QString fileName =
        QFileDialog::getOpenFileName(this, ui->browseDramSysButton->text(), {}, "DRAMSys executable (*)");
    ui->dramSysPath->setText(fileName);
}

void SimulationDialog::on_browseConfigButton_clicked()
{
    QString fileName =
        QFileDialog::getOpenFileName(this, ui->browseConfigButton->text(), {}, "Configuration file (*.json)");
    ui->jsonPath->setText(fileName);

    loadConfigurationFromPath();
}

void SimulationDialog::on_browseOutputButton_clicked()
{
    QString fileName = QFileDialog::getExistingDirectory(this, ui->browseOutputButton->text(), {});
    ui->outputDirLineEdit->setText(fileName);

    loadConfigurationFromPath();
}

void SimulationDialog::on_browseResourceDirButton_clicked()
{
    QString fileName = QFileDialog::getExistingDirectory(this, ui->browseResourceDirButton->text(), {});
    ui->resourceDirLineEdit->setText(fileName);

    loadConfigurationFromPath();
}

void SimulationDialog::on_simulateButton_clicked()
{
    saveConfiguration(temporaryConfigurationFile);

    ui->tabWidget->setCurrentWidget(ui->outputTab);
    ui->progressBar->setEnabled(true);
    ui->progressBar->setValue(0);

    showStopButton(true);

    // Spawn the DRAMSys process
    simulatorProcess = new QProcess(this);

    QObject::connect(simulatorProcess, &QIODevice::readyRead, this,
                     [=]
                     {
                         QByteArray msg = simulatorProcess->read(4096);
                         msg = msg.trimmed();
                         processMessage(msg.toStdString());
                     });

    QObject::connect(simulatorProcess, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this,
                     [=](int exitCode, QProcess::ExitStatus exitStatus)
                     {
                         Q_UNUSED(exitStatus)

                         showStopButton(false);

                         // Clear all the contents so that the user is not asked
                         // next time to overwrite the temp file.
                         temporaryConfigurationFile.resize(0);

                         if (exitCode == 0)
                         {
                             ui->progressBar->setValue(100);

                             QMessageBox msgBox;
                             msgBox.setText("Simulation done.");
                             msgBox.setInformativeText("Do you want to open the results?");
                             msgBox.setStandardButtons(QMessageBox::Open | QMessageBox::Cancel);
                             msgBox.setDefaultButton(QMessageBox::Open);
                             int ret = msgBox.exec();

                             if (ret == QMessageBox::Open)
                             {
                                 auto results = getSimulationResults();
                                 openSimulationResults(results);
                             }
                         }
                     });

    QStringList argumentList;
    argumentList << temporaryConfigurationFile.fileName();

    if (!ui->resourceDirLineEdit->text().isEmpty())
        argumentList << ui->resourceDirLineEdit->text();

    simulatorProcess->setWorkingDirectory(ui->outputDirLineEdit->text());
    simulatorProcess->start(ui->dramSysPath->text(), argumentList);
}

void SimulationDialog::on_reloadButton_clicked()
{
    loadConfigurationFromPath();

    ui->outputPlainTextEdit->clear();
}

void SimulationDialog::on_saveButton_clicked()
{
    QFile file(ui->jsonPath->text());
    saveConfiguration(file);
}

void SimulationDialog::on_stopButton_clicked()
{
    if (simulatorProcess)
        simulatorProcess->terminate();
}

void SimulationDialog::showStopButton(bool val)
{
    ui->simulateButton->setVisible(!val);
    ui->stopButton->setVisible(val);
}

void SimulationDialog::saveConfiguration(QFile &file)
{
    if (!file.open(QIODevice::ReadWrite | QIODevice::Text))
        return;

    if (file.size() != 0)
    {
        QMessageBox msgBox;
        msgBox.setText("The configuration file will be overwritten.");
        msgBox.setInformativeText("Do you want to save your changes?");
        msgBox.setStandardButtons(QMessageBox::Save | QMessageBox::Cancel);
        msgBox.setDefaultButton(QMessageBox::Save);
        int ret = msgBox.exec();

        if (ret != QMessageBox::Save)
            return;
    }

    // Clear the file
    file.resize(0);

    QTextStream out(&file);

    loadConfigurationFromTextFields();

    std::string dump = DRAMSys::Config::dump(configuration, 4);
    out << dump.c_str();
}

void SimulationDialog::processMessage(const std::string &msg)
{
    // Get percentages
    QRegularExpression re("(\\d+(\\.\\d+)?|\\.\\d+) ?%");
    QRegularExpressionMatch match = re.match(msg.c_str());

    if (match.hasMatch())
    {
        unsigned int percentage = match.captured(1).toUInt();

        ui->progressBar->setValue(percentage);
    }

    ui->outputPlainTextEdit->appendPlainText(msg.c_str());
}

void SimulationDialog::loadConfigurationFromTextFields()
{
    using namespace DRAMSys::Config;

    AddressMapping addressMapping;
    McConfig mcConfig;
    MemSpec memSpec;
    SimConfig simConfig;
    std::string simulationId;
    TraceSetup traceSetup;

    simulationId = ui->simulationIdLineEdit->text().toStdString();

    try
    {
        from_dump(ui->addressMappingTextEdit->toPlainText().toStdString(), addressMapping);
        from_dump(ui->mcConfigTextEdit->toPlainText().toStdString(), mcConfig);
        from_dump(ui->memSpecTextEdit->toPlainText().toStdString(), memSpec);
        from_dump(ui->simConfigTextEdit->toPlainText().toStdString(), simConfig);

        if (!ui->traceSetupTextEdit->toPlainText().toStdString().empty())
            from_dump(ui->traceSetupTextEdit->toPlainText().toStdString(), traceSetup);
    }
    catch (const std::exception &e)
    {
        qWarning() << "Error while parsing json:" << e.what();
        return;
    }

    configuration = DRAMSys::Config::Configuration{addressMapping,
                                                        mcConfig,
                                                        memSpec,
                                                        simConfig,
                                                        simulationId,
                                                        std::make_optional<TraceSetup>(std::move(traceSetup))};

    loadConfiguration();
}

void SimulationDialog::loadConfigurationFromPath()
{
    QFileInfo fileInfo(ui->jsonPath->text());

    if (!fileInfo.isFile())
        return;

    try
    {
        configuration = DRAMSys::Config::from_path(ui->jsonPath->text().toStdString());
    }
    catch (const std::exception &e)
    {
        qWarning() << "Error while parsing json:" << e.what();
        return;
    }

    loadConfiguration();
}

void SimulationDialog::loadConfiguration()
{
    ui->simulationIdLabel->setEnabled(true);
    ui->simulationId->setEnabled(true);
    ui->simulationId->setText(configuration.simulationId.c_str());

    ui->simulationIdLineEdit->setText(configuration.simulationId.c_str());

    loadSimConfig();
    loadMcConfig();
    loadMemSpec();
    loadAddressMapping();
    loadTraceSetup();
    loadPreview();
}

void SimulationDialog::loadSimConfig()
{
    ui->simConfigTextEdit->clear();

    std::string dump = DRAMSys::Config::dump(configuration.simConfig, 4);
    ui->simConfigTextEdit->setText(dump.c_str());
}

void SimulationDialog::loadMcConfig()
{
    ui->mcConfigTextEdit->clear();

    std::string dump = DRAMSys::Config::dump(configuration.mcConfig, 4);
    ui->mcConfigTextEdit->setText(dump.c_str());
}

void SimulationDialog::loadMemSpec()
{
    ui->memSpecTextEdit->clear();

    std::string dump = DRAMSys::Config::dump(configuration.memSpec, 4);
    ui->memSpecTextEdit->setText(dump.c_str());
}

void SimulationDialog::loadAddressMapping()
{
    ui->addressMappingTextEdit->clear();

    std::string dump = DRAMSys::Config::dump(configuration.addressMapping, 4);
    ui->addressMappingTextEdit->setText(dump.c_str());
}

void SimulationDialog::loadTraceSetup()
{
    ui->traceSetupTextEdit->clear();

    if (const auto &traceSetup = configuration.traceSetup)
    {
        std::string dump = DRAMSys::Config::dump(*traceSetup, 4);
        ui->traceSetupTextEdit->setText(dump.c_str());
    }
}

void SimulationDialog::loadPreview()
{
    ui->previewTextEdit->clear();

    std::string dump = DRAMSys::Config::dump(configuration, 4);
    ui->previewTextEdit->setText(dump.c_str());
}

QFileInfoList SimulationDialog::getSimulationResults()
{
    QFileInfoList list;

    // Get the path where the tracefiles are located
    QDir baseDir(ui->outputDirLineEdit->text());

    for (const auto &fileInfo : baseDir.entryInfoList())
    {
        if (fileInfo.baseName().startsWith(configuration.simulationId.c_str()))
        {
            // Dont open tracefiles that are older than a few seconds
            if (fileInfo.metadataChangeTime().secsTo(QDateTime::currentDateTime()) > 30)
                continue;

            list << fileInfo;
        }
    }

    return list;
}

void SimulationDialog::openSimulationResults(const QFileInfoList &fileInfos)
{
    for (const auto &fileInfo : fileInfos)
        openFileRequested(fileInfo.absoluteFilePath());
}
