/*
    SuperCollider Qt IDE
    Copyright (c) 2012 Jakob Leben & Tim Blechmann
    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
*/

#include <QDebug>
#include <QMessageBox>
#include <QInputDialog>
#include <QFile>

#include <fstream>

#include "sclang_page.hpp"
#include "ui_settings_sclang.h"
#include "../../core/settings/manager.hpp"
#include "../../core/util/standard_dirs.hpp"
#include "../../core/main.hpp"

#include <yaml-cpp/yaml.h>


namespace ScIDE { namespace Settings {

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

    ui->sclang_add_configfile->setIcon(QIcon::fromTheme("list-add"));
    ui->sclang_remove_configfile->setIcon(QIcon::fromTheme("list-remove"));

    ui->sclang_add_include->setIcon(QIcon::fromTheme("list-add"));
    ui->sclang_remove_include->setIcon(QIcon::fromTheme("list-remove"));

    ui->sclang_add_exclude->setIcon(QIcon::fromTheme("list-add"));
    ui->sclang_remove_exclude->setIcon(QIcon::fromTheme("list-remove"));

    ui->runtimeDir->setFileMode(QFileDialog::Directory);

    connect(ui->activeConfigFileComboBox, &QComboBox::currentTextChanged, this,
            &SclangPage::changeSelectedLanguageConfig);

    connect(ui->sclang_add_configfile, SIGNAL(clicked()), this, SLOT(dialogCreateNewConfigFile()));
    connect(ui->sclang_remove_configfile, SIGNAL(clicked()), this, SLOT(dialogDeleteCurrentConfigFile()));

    connect(ui->sclang_add_include, SIGNAL(clicked()), this, SLOT(addIncludePath()));
    connect(ui->sclang_add_exclude, SIGNAL(clicked()), this, SLOT(addExcludePath()));

    connect(ui->sclang_remove_include, SIGNAL(clicked()), this, SLOT(removeIncludePath()));
    connect(ui->sclang_remove_exclude, SIGNAL(clicked()), this, SLOT(removeExcludePath()));

    connect(ui->sclang_post_inline_warnings, SIGNAL(stateChanged(int)), this, SLOT(markSclangConfigDirty()));
    connect(ui->sclang_exclude_default_paths, SIGNAL(stateChanged(int)), this, SLOT(markSclangConfigDirty()));
}

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

void SclangPage::load(Manager* s) {
    s->beginGroup("IDE/interpreter");

    ui->autoStart->setChecked(s->value("autoStart").toBool());
    ui->runtimeDir->setText(s->value("runtimeDir").toString());

    QStringList availConfigFiles = availableLanguageConfigFiles();
    QString configSelectedLanguageConfigFile = s->value("configFile").toString();

    ui->activeConfigFileComboBox->clear();
    ui->activeConfigFileComboBox->addItems(availConfigFiles);
    int index = availConfigFiles.indexOf(configSelectedLanguageConfigFile);
    if (index != -1)
        ui->activeConfigFileComboBox->setCurrentIndex(index);
    selectedLanguageConfigFile = configSelectedLanguageConfigFile; // Happens after setting the combobox entries, since
                                                                   // the code triggers stateChanged event.

    s->endGroup();

    sclangConfigChanged = false;

    readLanguageConfig();
}

void SclangPage::store(Manager* s) {
    s->beginGroup("IDE/interpreter");
    s->setValue("autoStart", ui->autoStart->isChecked());
    s->setValue("runtimeDir", ui->runtimeDir->text());
    s->setValue("configFile", ui->activeConfigFileComboBox->currentText());
    s->endGroup();

    writeLanguageConfig();
}

void SclangPage::addIncludePath() {
    QString path = QFileDialog::getExistingDirectory(this, tr("ScLang include directories"));
    if (path.size())
        ui->sclang_include_directories->addItem(path);
    sclangConfigDirty = true;
}

void SclangPage::removeIncludePath() {
    foreach (QListWidgetItem* item, ui->sclang_include_directories->selectedItems()) {
        ui->sclang_include_directories->removeItemWidget(item);
        delete item;
    }
    sclangConfigDirty = true;
}

void SclangPage::addExcludePath() {
    QString path = QFileDialog::getExistingDirectory(this, tr("ScLang exclude directories"));
    if (path.size())
        ui->sclang_exclude_directories->addItem(path);
    sclangConfigDirty = true;
}

void SclangPage::removeExcludePath() {
    foreach (QListWidgetItem* item, ui->sclang_exclude_directories->selectedItems()) {
        ui->sclang_exclude_directories->removeItemWidget(item);
        delete item;
    }
    sclangConfigDirty = true;
}

void SclangPage::changeSelectedLanguageConfig(const QString& configPath) {
    selectedLanguageConfigFile = configPath;
    sclangConfigChanged = true;
    readLanguageConfig();
}

void SclangPage::readLanguageConfig() {
    // LATER: watch for changes

    QString configFile = languageConfigFile();

    QFileInfo configFileInfo(configFile);
    const bool configFileExists = configFileInfo.exists();

    if (!configFileExists)
        return;

    using namespace YAML;
    try {
        std::ifstream fin(configFile.toStdString());
        Node doc = YAML::Load(fin);
        if (doc) {
            const Node& includePaths = doc["includePaths"];
            if (includePaths && includePaths.IsSequence()) {
                ui->sclang_include_directories->clear();
                for (Node const& pathNode : includePaths) {
                    if (!pathNode.IsScalar())
                        continue;
                    std::string path = pathNode.as<std::string>();
                    if (!path.empty())
                        ui->sclang_include_directories->addItem(QString(path.c_str()));
                }
            }

            const Node& excludePaths = doc["excludePaths"];
            if (excludePaths && excludePaths.IsSequence()) {
                ui->sclang_exclude_directories->clear();
                for (Node const& pathNode : excludePaths) {
                    if (!pathNode.IsScalar())
                        continue;
                    std::string path = pathNode.as<std::string>();
                    if (!path.empty())
                        ui->sclang_exclude_directories->addItem(QString(path.c_str()));
                }
            }

            const Node& inlineWarnings = doc["postInlineWarnings"];
            if (inlineWarnings) {
                try {
                    bool postInlineWarnings = inlineWarnings.as<bool>();
                    ui->sclang_post_inline_warnings->setChecked(postInlineWarnings);
                } catch (...) { qDebug() << "Warning: Cannot parse config file entry \"postInlineWarnings\""; }
            }

            const Node& excludeDefaultPaths = doc["excludeDefaultPaths"];
            if (excludeDefaultPaths) {
                try {
                    bool excludeDefaultPathsBool = excludeDefaultPaths.as<bool>();
                    ui->sclang_exclude_default_paths->setChecked(excludeDefaultPathsBool);
                } catch (...) { qDebug() << "Warning: Cannot parse config file entry \"excludeDefaultPaths\""; }
            }
        }
    } catch (std::exception&) {}

    sclangConfigDirty = false;
}

void SclangPage::writeLanguageConfig() {
    if (!sclangConfigDirty) {
        if (sclangConfigChanged) {
            dialogConfigFileUpdated();
            sclangConfigChanged = false; // avoid double invocation
        }
        return;
    }

    using namespace YAML;
    using std::ofstream;
    Emitter out;
    out.SetIndent(4);
    out.SetMapFormat(Block);
    out.SetSeqFormat(Block);
    out.SetBoolFormat(TrueFalseBool);

    out << BeginMap;

    out << Key << "includePaths";
    out << Value << BeginSeq;

    for (int i = 0; i != ui->sclang_include_directories->count(); ++i)
        out << ui->sclang_include_directories->item(i)->text().toStdString();
    out << EndSeq;

    out << Key << "excludePaths";
    out << Value << BeginSeq;
    for (int i = 0; i != ui->sclang_exclude_directories->count(); ++i)
        out << ui->sclang_exclude_directories->item(i)->text().toStdString();
    out << EndSeq;

    out << Key << "postInlineWarnings";
    out << Value << (ui->sclang_post_inline_warnings->checkState() == Qt::Checked);

    out << Key << "excludeDefaultPaths";
    out << Value << (ui->sclang_exclude_default_paths->checkState() == Qt::Checked);

    out << EndMap;
    ofstream fout(languageConfigFile().toStdString().c_str());
    fout << out.c_str();

    dialogConfigFileUpdated();

    sclangConfigDirty = false;
}

QString SclangPage::languageConfigFile() {
    if (selectedLanguageConfigFile.isEmpty()) {
        selectedLanguageConfigFile = standardDirectory(ScConfigUserDir) + "/" + QStringLiteral("sclang_conf.yaml");
    }
    return selectedLanguageConfigFile;
}

QStringList SclangPage::availableLanguageConfigFiles() {
    QDir qdir = QDir(standardDirectory(ScConfigUserDir));
    QStringList fileFilters;
    fileFilters << "sclang_conf*.yaml";
    QFileInfoList configFileList = qdir.entryInfoList(fileFilters);
    QStringList canonicalPaths;
    foreach (QFileInfo aFile, configFileList) {
        canonicalPaths.append(aFile.canonicalFilePath());
    }
    return canonicalPaths;
}

void SclangPage::dialogCreateNewConfigFile() {
    bool ok;
    QString text = QInputDialog::getText(this, tr("New Configuration File"),
                                         tr("Create configuration file 'sclang_conf_*.yaml' with '*' replaced by:"),
                                         QLineEdit::Normal, QDir::home().dirName(), &ok);

    if (ok && !text.isEmpty()) {
        QString proposedLanguageConfigFile = standardDirectory(ScConfigUserDir) + "/sclang_conf_" + text + ".yaml";
        if (QFile(proposedLanguageConfigFile).exists()) {
            QMessageBox::information(this, tr("File Already Exists"),
                                     tr("Configuration file already exists:\n%1").arg(proposedLanguageConfigFile));
        } else {
            selectedLanguageConfigFile = proposedLanguageConfigFile;
            sclangConfigDirty = true;
            writeLanguageConfig();

            int index = ui->activeConfigFileComboBox->count();
            ui->activeConfigFileComboBox->addItem(selectedLanguageConfigFile);
            ui->activeConfigFileComboBox->setCurrentIndex(index);
        }
    }
}

void SclangPage::dialogDeleteCurrentConfigFile() {
    int ret = QMessageBox::warning(this, tr("Delete Configuration File"),
                                   tr("Are you sure you want to delete the following configuration file?\nThis action "
                                      "is immediate and cannot be undone.\n")
                                       + selectedLanguageConfigFile,
                                   QMessageBox::Ok | QMessageBox::Cancel, QMessageBox::Cancel);

    if (ret == QMessageBox::Ok) {
        QString pathBeingRemoved = selectedLanguageConfigFile;
        QFile::remove(pathBeingRemoved);
        ui->activeConfigFileComboBox->removeItem(ui->activeConfigFileComboBox->findText(pathBeingRemoved));
        if (ui->activeConfigFileComboBox->count() != 0) {
            ui->activeConfigFileComboBox->setCurrentIndex(0);
        }
    }
}

void SclangPage::dialogConfigFileUpdated() {
    QMessageBox::StandardButton reply =
        QMessageBox::question(this, tr("Sclang configuration file updated"),
                              tr("The SuperCollider language configuration has been updated.\n"
                                 "The interpreter needs to reboot to apply the changes.\n\n"
                                 "Would you like to reboot now?\n"
                                 "WARNING: all sound will stop."),
                              QMessageBox::Yes | QMessageBox::No, QMessageBox::No);

    if (reply == QMessageBox::Yes) {
        if (auto process = Main::instance()->scProcess()) {
            process->restartLanguage();
        } else {
            qWarning() << "SclangPage::dialogConfigFileUpdated(): "
                          "Main->scProcess pointer is null!";
        }
    }
}
}} // namespace ScIDE::Settings
