#include "GUI.h"

#include "CustomWidget/OptionCombo.h"
#include "CustomWidget/OptionEntry.h"
#include "CustomWidget/OptionSpin.h"
#include "CustomWidget/OptionSwitch.h"
#include "Utils/ConfigurationLoaderInterface.h"
#include "Utils/ConfigurationResolver.h"
#include "Utils/Writer.h"
#include "version.h"

#include <exception>
#include <fstream>
#include <memory>
#include <utility>

GUI::GUI(LoggerInterface* logger,
         TranslatorInterface* translator,
         ConfigurationLoaderInterface* configurationLoader,
         ConfigurationResolverInterface* resolver,
         WriterInterface* writer)
    : logger(logger)
    , translator(translator)
    , configLoader(configurationLoader)
    , resolver(resolver)
    , writer(writer)
    , currentApp(nullptr)
    , currentDriver(nullptr) {

    /* Load the configurations */
    this->driverConfiguration
        = this->configLoader->loadDriverSpecificConfiguration();
    for (auto& driver : this->driverConfiguration) {
        driver->sortSectionOptions();
    }

    if (this->driverConfiguration.empty()) {
        throw std::runtime_error("No driver configuration could be loaded");
    }

    this->systemWideConfiguration
        = this->configLoader->loadSystemWideConfiguration();
    this->userDefinedConfiguration
        = this->configLoader->loadUserDefinedConfiguration();
    this->availableGPUs = this->configLoader->loadAvailableGPUs();

    /* For each app setup their prime driver name */
    this->resolver->updatePrimeApplications(this->userDefinedConfiguration,
                                            this->availableGPUs);

    /* Merge all the options in a complete structure */
    this->resolver->mergeOptionsForDisplay(this->driverConfiguration,
                                           this->userDefinedConfiguration,
                                           this->availableGPUs);

    /* Filter invalid options */
    this->resolver->filterDriverUnsupportedOptions(
        this->driverConfiguration,
        this->userDefinedConfiguration,
        this->availableGPUs);

    this->logger->debug(this->translator->trns("Start building GTK gui"));

    /* Load the GUI file */
    this->gladeBuilder = Gtk::Builder::create();
    this->gladeBuilder->add_from_resource("/jlHertel/adriconf/DriConf.glade");

    /* Extract the main object */
    this->pWindow = this->gladeBuilder->get_widget<Gtk::Window>("mainwindow");
    if (!pWindow) {
        this->logger->error(
            this->translator->trns("Main window object is not in glade file!"));
        return;
    }

    auto cssProvider = Gtk::CssProvider::create();
    cssProvider->load_from_resource("/jlHertel/adriconf/DarkTheme.css");

    this->pWindow->get_style_context()->add_provider_for_display(
        this->pWindow->get_display(),
        cssProvider,
        GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);

    this->profileList
        = this->gladeBuilder->get_widget<Gtk::FlowBox>("profileList");
    this->screenSwitcher
        = this->gladeBuilder->get_widget<Gtk::Stack>("screenSwitcher");

    this->setupNewProfileScreen();
    this->setupEditProfileScreen();
    this->setupAboutScreen();

    this->drawProfileList();
}

GUI::~GUI() {
    delete this->pWindow;
}

void GUI::saveProfiles() {
    this->logger->debug(
        this->translator->trns("Generating final XML for saving..."));
    auto resolvedOptions
        = this->resolver->resolveOptionsForSave(this->systemWideConfiguration,
                                                this->driverConfiguration,
                                                this->userDefinedConfiguration,
                                                this->availableGPUs);
    auto rawXML = this->writer->generateRawXml(resolvedOptions);
    this->logger->debug(Glib::ustring::compose(
        this->translator->trns("Writing generated XML: %1"),
        rawXML));

    auto environmentUserHome = std::getenv("HOME");
    if (environmentUserHome == nullptr) {
        this->logger->error(
            this->translator->trns("Environment variable HOME is not set"));
        return;
    }

    std::string userHome(environmentUserHome);
    std::ofstream outFile(userHome + "/.drirc");
    outFile << rawXML;
    outFile.close();
}

Gtk::Window* GUI::getWindowPointer() {
    return this->pWindow;
}

void GUI::drawProfileList() {
    Gtk::FlowBoxChild* child = this->profileList->get_child_at_index(0);
    while (child != nullptr) {
        this->profileList->remove(*child);
        child = this->profileList->get_child_at_index(0);
    }

    for (auto& driver : this->userDefinedConfiguration) {
        for (auto& possibleApp : driver->getApplications()) {
            auto itemBox = Gtk::manage(new Gtk::Button);
            itemBox->set_label(possibleApp->getName());
            itemBox->set_visible(true);
            itemBox->get_style_context()->add_class("profileItem");
            itemBox->signal_clicked().connect(
                sigc::bind(sigc::mem_fun(*this, &GUI::onApplicationSelected),
                           possibleApp,
                           driver));

            this->profileList->append(*itemBox);
        }
    }

    auto newButton = Gtk::manage(new Gtk::Button());
    newButton->set_image_from_icon_name("list-add");
    newButton->set_visible(true);
    newButton->get_style_context()->add_class("profileItem");
    newButton->signal_clicked().connect([this]() {
        this->screenSwitcher->set_visible_child("newProfileScreen");
    });
    this->profileList->append(*newButton);

    auto aboutButton = Gtk::manage(new Gtk::Button());
    aboutButton->set_image_from_icon_name("help-about");
    aboutButton->set_visible(true);
    aboutButton->get_style_context()->add_class("profileItem");
    aboutButton->signal_clicked().connect(
        [this]() { this->screenSwitcher->set_visible_child("aboutScreen"); });
    this->profileList->append(*aboutButton);
}

void GUI::drawApplicationOptions() {
    auto selectedAppOptions = this->currentApp->getOptions();

    /* Get the notebook itself */
    auto* pNotebook
        = this->gladeBuilder->get_widget<Gtk::Notebook>("editProfileContent");
    if (!pNotebook) {
        this->logger->error(
            this->translator->trns("Notebook object not found in glade file!"));
        return;
    }

    /* Remove any previous defined page */
    int numberOfPages = pNotebook->get_n_pages();

    for (int i = 0; i < numberOfPages; i++) {
        pNotebook->remove_page(-1);
    }

    /* Remove any previous defined spinButton */
    this->currentSpinButtons.clear();

    pNotebook->set_visible(true);

    std::list<Section_ptr> definedSections;
    if (this->currentApp->getIsUsingPrime()) {
        definedSections
            = this->availableGPUs[this->currentApp->getDevicePCIId()]
                  ->getSections();
    } else {
        auto driverConfigurationIter = std::find_if(
            this->driverConfiguration.begin(),
            this->driverConfiguration.end(),
            [this](const DriverConfiguration_ptr& configuration) {
                return this->currentDriver->getDriver()
                       == configuration->getDriverName();
            });

        if (driverConfigurationIter == this->driverConfiguration.end()) {
            this->logger->error(Glib::ustring::compose(
                this->translator->trns(
                    "User-defined configuration with driver %1 doesn't has "
                    "valid configuration loaded."),
                this->currentDriver->getDriver()));
            return;
        }

        definedSections = (*driverConfigurationIter)->getSections();
    }

    /* Draw each section as a tab */
    for (auto& section : definedSections) {

        Gtk::Box* tabBox = Gtk::manage(new Gtk::Box);
        tabBox->set_visible(true);
        tabBox->set_orientation(Gtk::Orientation::VERTICAL);
        tabBox->set_margin_start(8);
        tabBox->set_margin_end(8);
        tabBox->set_margin_top(10);

        /* Draw each field individually */
        for (auto& option : section->getOptions()) {
            auto optionValue
                = std::find_if(selectedAppOptions.begin(),
                               selectedAppOptions.end(),
                               [&option](ProfileOption_ptr o) {
                                   return option->getName() == o->getName();
                               });

            ProfileOption_ptr currentOption;

            if (optionValue == selectedAppOptions.end()) {
                currentOption = std::make_shared<ProfileOption>();
                currentOption->setName(option->getName());
                currentOption->setValue(option->getDefaultValue());
                this->currentApp->addOption(currentOption);
            } else {
                currentOption = *optionValue;
            }

            Gtk::Box* optionBox = Gtk::manage(new Gtk::Box);
            optionBox->set_visible(true);
            optionBox->set_orientation(Gtk::Orientation::HORIZONTAL);
            optionBox->set_margin_bottom(10);

            if (option->getType() == DriverOptionType::BOOL
                || option->getType() == DriverOptionType::FAKE_BOOL) {
                OptionSwitch* optionSwitch
                    = Gtk::manage(new OptionSwitch(currentOption));
                optionSwitch->set_halign(Gtk::Align::END);
                optionSwitch->set_hexpand(true);

                optionBox->append(*optionSwitch);
            }

            if (option->getType() == DriverOptionType::ENUM
                || option->getType() == DriverOptionType::FAKE_ENUM) {
                auto optionCombo = Gtk::manage(new OptionCombo(currentOption));
                optionCombo->set_halign(Gtk::Align::END);
                optionCombo->set_hexpand(true);

                int counter = 0;
                for (auto const& enumOption : option->getEnumValues()) {
                    optionCombo->append(enumOption.first, enumOption.second);

                    if (enumOption.second == currentOption->getValue()) {
                        optionCombo->set_active(counter);
                    }
                    counter++;
                }

                optionBox->append(*optionCombo);
            }

            if (option->getType() == DriverOptionType::INT) {
                auto optionEntry = Gtk::manage(
                    new OptionSpin<int>(currentOption,
                                        option->getValidValueStart(),
                                        option->getValidValueEnd(),
                                        1,
                                        10,
                                        0));
                optionEntry->set_halign(Gtk::Align::END);
                optionEntry->set_hexpand(true);

                optionBox->append(*optionEntry);
            }

            if (option->getType() == DriverOptionType::FLOAT) {
                auto optionEntry = Gtk::manage(
                    new OptionSpin<float>(currentOption,
                                          option->getValidValueStartAsFloat(),
                                          option->getValidValueEndAsFloat(),
                                          0.01,
                                          0.1,
                                          4));
                optionEntry->set_halign(Gtk::Align::END);
                optionEntry->set_hexpand(true);

                optionBox->append(*optionEntry);
            }

            if (option->getType() == DriverOptionType::STRING) {
                auto optionEntry = Gtk::manage(new OptionEntry(currentOption));
                optionEntry->set_halign(Gtk::Align::END);
                optionEntry->set_hexpand(true);

                optionBox->append(*optionEntry);
            }

            Gtk::Label* label = Gtk::manage(new Gtk::Label);
            label->set_label(option->getDescription());
            label->set_visible(true);
            label->set_justify(Gtk::Justification::LEFT);
            label->set_wrap(true);
            label->set_margin_start(10);
            optionBox->prepend(*label);

            tabBox->append(*optionBox);
        }

        Gtk::ScrolledWindow* scrolledWindow
            = Gtk::manage(new Gtk::ScrolledWindow);
        scrolledWindow->set_visible(true);
        scrolledWindow->set_child(*tabBox);

        pNotebook->append_page(*scrolledWindow, section->getDescription());
    }

    /* If we have more than one GPU then we are under PRIME */
    if (this->availableGPUs.size() > 1) {
        auto primeOptionPtr
            = std::find_if(selectedAppOptions.begin(),
                           selectedAppOptions.end(),
                           [](const ProfileOption_ptr& a) {
                               return a->getName() == "device_id";
                           });

        ProfileOption_ptr primeOption;
        if (primeOptionPtr == selectedAppOptions.end()) {
            primeOption = std::make_shared<ProfileOption>();
            primeOption->setName("device_id");
            primeOption->setValue("");
            this->currentApp->addOption(primeOption);
        } else {
            primeOption = *primeOptionPtr;
        }

        this->shouldIgnorePrimeComboBoxEvents = true;
        if (this->primeComboBox == nullptr) {
            this->primeComboBox = std::make_shared<
                ComboBoxExtra<std::pair<Glib::ustring, ProfileOption_ptr>>>();
            this->primeComboBox->signal_changed().connect(
                sigc::mem_fun(*this, &GUI::onPrimeComboBoxChanged));
        } else {
            this->primeComboBox->removeAllChildren();
        }

        this->primeComboBox->set_visible(true);

        this->primeComboBox->append(
            this->translator->trns("Use default GPU of screen"),
            std::make_pair("", primeOption));

        this->primeComboBox->set_active(0);

        int counter = 0;
        for (auto const& enumOption : this->availableGPUs) {
            counter++;
            this->primeComboBox->append(
                enumOption.second->getDeviceName(),
                std::make_pair(enumOption.second->getPciId(), primeOption));

            if (this->currentApp->getDevicePCIId()
                == enumOption.second->getPciId()) {
                this->primeComboBox->set_active(counter);
            }
        }

        this->shouldIgnorePrimeComboBoxEvents = false;

        auto* label = Gtk::manage(new Gtk::Label);
        label->set_label(
            this->translator->trns("Force Application to use GPU"));
        label->set_visible(true);
        label->set_justify(Gtk::Justification::LEFT);
        label->set_wrap(true);
        label->set_margin_start(10);

        auto* optionBox = Gtk::manage(new Gtk::Box);
        optionBox->set_visible(true);
        optionBox->set_orientation(Gtk::Orientation::HORIZONTAL);
        optionBox->set_margin_bottom(10);
        optionBox->append(*(this->primeComboBox));
        optionBox->prepend(*label);

        auto primeTabBox = Gtk::manage(new Gtk::Box);
        primeTabBox->set_visible(true);
        primeTabBox->set_orientation(Gtk::Orientation::VERTICAL);
        primeTabBox->set_margin_start(8);
        primeTabBox->set_margin_end(8);
        primeTabBox->set_margin_top(10);
        primeTabBox->append(*optionBox);

        Gtk::ScrolledWindow* scrolledWindow
            = Gtk::manage(new Gtk::ScrolledWindow);
        scrolledWindow->set_visible(true);
        scrolledWindow->set_child(*primeTabBox);

        pNotebook->append_page(*scrolledWindow,
                               this->translator->trns("PRIME Settings"));
    }
}

void GUI::setupAboutScreen() {
    Gtk::Label *versionLabel, *gitRevisionLabel;

    versionLabel = this->gladeBuilder->get_widget<Gtk::Label>("versionLabel");
    gitRevisionLabel
        = this->gladeBuilder->get_widget<Gtk::Label>("gitRevisionLabel");
    versionLabel->set_label(
        Glib::ustring::compose(this->translator->trns("version: %1"),
                               BUILD_VERSION_NUMBER));
    gitRevisionLabel->set_label(
        Glib::ustring::compose(this->translator->trns("git-revision: %1"),
                               GIT_COMMIT_HASH));

    Gtk::Button* aboutScreenBackButton;
    aboutScreenBackButton
        = this->gladeBuilder->get_widget<Gtk::Button>("aboutScreenBackButton");
    aboutScreenBackButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onBackToProfileListPressed));
}

void GUI::onBackToProfileListPressed() {
    this->screenSwitcher->set_visible_child("listProfilesScreen");
}

void GUI::setupNewProfileScreen() {
    this->newProfileBackButton
        = this->gladeBuilder->get_widget<Gtk::Button>("newProfileBackButton");
    this->newProfileSaveButton
        = this->gladeBuilder->get_widget<Gtk::Button>("newProfileSaveButton");

    this->newProfileBackButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onBackToProfileListPressed));
    this->newProfileSaveButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onNewProfileSaveClicked));

    // Setup the driver comboBox
    this->newProfileDriver = Gtk::Builder::get_widget_derived<
        ComboBoxExtra<DriverConfiguration_ptr>>(this->gladeBuilder,
                                                "newAppDriver");
    for (const auto& driverConfig : this->driverConfiguration) {
        this->newProfileDriver->append(driverConfig->getDriverName(),
                                       driverConfig);
    }

    this->newProfileDriver->set_active(0);
    if (this->driverConfiguration.size() == 1) {
        auto* newAppDriverBox
            = this->gladeBuilder->get_widget<Gtk::Box>("newAppDriverBox");
        newAppDriverBox->set_visible(false);
    }

    // Setup input fields
    this->newProfileSaveButton->set_sensitive(false);
    this->newProfileName
        = this->gladeBuilder->get_widget<Gtk::Entry>("newAppName");
    this->newProfileExecutable
        = this->gladeBuilder->get_widget<Gtk::Entry>("newAppExecutable");
    this->newProfileUseRegexCheckButton
        = this->gladeBuilder->get_widget<Gtk::CheckButton>(
            "newAppExecutableUseRegex");
    this->newProfileSHA1
        = this->gladeBuilder->get_widget<Gtk::Entry>("newProfileSHA1");

    this->newProfileName->signal_changed().connect(
        sigc::mem_fun(*this, &GUI::onNewApplicationTextFieldChanged));
    this->newProfileExecutable->signal_changed().connect(
        sigc::mem_fun(*this, &GUI::onNewApplicationTextFieldChanged));
    this->newProfileSHA1->signal_changed().connect(
        sigc::mem_fun(*this, &GUI::onNewApplicationTextFieldChanged));
}

void GUI::onNewApplicationTextFieldChanged() {
    this->newProfileSaveButton->set_sensitive(
        this->newProfileName->get_text_length() > 0
        && this->newProfileExecutable->get_text_length() > 0
        && (this->newProfileSHA1->get_text_length() == 0
            || this->newProfileSHA1->get_text_length() == 40));
}

void GUI::onNewProfileSaveClicked() {
    auto driver = this->newProfileDriver->getActiveExtraData();

    Profile_ptr newProfile = driver->generateApplication();
    newProfile->setName(this->newProfileName->get_text());

    if (this->newProfileUseRegexCheckButton->get_active()) {
        newProfile->setExecutableRegex(this->newProfileExecutable->get_text());
    } else {
        newProfile->setExecutable(this->newProfileExecutable->get_text());
    }

    newProfile->setSha1(this->newProfileSHA1->get_text());

    this->resolver->setSystemWideDefaults(newProfile,
                                          this->systemWideConfiguration,
                                          driver->getDriverName());

    for (auto& userConfig : this->userDefinedConfiguration) {
        if (userConfig->getDriver() == driver->getDriverName()) {
            userConfig->addApplication(newProfile);
            userConfig->sortApplications();
        }
    }

    this->saveProfiles();
    this->drawProfileList();
    this->onBackToProfileListPressed();
    this->newProfileName->set_text("");
    this->newProfileExecutable->set_text("");
    this->newProfileUseRegexCheckButton->set_active(false);
    this->newProfileSHA1->set_text("");
}

void GUI::onApplicationSelected(const Profile_ptr& selectedProfile,
                                Device_ptr selectedProfileDriver) {
    this->currentApp    = std::make_shared<ShadowProfile>(selectedProfile);
    this->currentDriver = std::move(selectedProfileDriver);

    this->drawApplicationOptions();
    this->screenSwitcher->set_visible_child("editProfileScreen");
}

void GUI::setupEditProfileScreen() {
    this->editProfileBackButton
        = this->gladeBuilder->get_widget<Gtk::Button>("editProfileBackButton");
    this->editProfileSaveButton
        = this->gladeBuilder->get_widget<Gtk::Button>("editProfileSaveButton");
    this->editProfileRemoveButton = this->gladeBuilder->get_widget<Gtk::Button>(
        "editProfileRemoveButton");

    this->editProfileBackButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onBackToProfileListPressed));
    this->editProfileSaveButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onEditProfileSaveClicked));
    this->editProfileRemoveButton->signal_clicked().connect(
        sigc::mem_fun(*this, &GUI::onEditProfileRemoveClicked));
}

void GUI::onEditProfileSaveClicked() {
    this->currentApp->applyChangesToOriginalData();
    this->saveProfiles();
    this->onBackToProfileListPressed();
}

void GUI::onEditProfileRemoveClicked() {
    this->confirmDeleteProfileDialog = std::make_unique<Gtk::MessageDialog>(
        *(this->pWindow),
        this->translator->trns("Are you sure?"),
        false,
        Gtk::MessageType::QUESTION,
        Gtk::ButtonsType::OK_CANCEL,
        true);
    this->confirmDeleteProfileDialog->set_title(
        this->translator->trns("Confirm"));
    this->confirmDeleteProfileDialog->set_name("confirmProfileRemoveDialog");
    this->confirmDeleteProfileDialog->set_hide_on_close(true);
    this->confirmDeleteProfileDialog->signal_response().connect(
        sigc::mem_fun(*this, &GUI::onConfirmDeleteProfileDialogResponse));

    this->confirmDeleteProfileDialog->show();
}

void GUI::onConfirmDeleteProfileDialogResponse(int responseId) {
    if (responseId == Gtk::ResponseType::OK) {
        this->currentDriver->getApplications().remove_if(
            [this](const Profile_ptr& app) {
                return app->isSameExecutable(this->currentApp);
            });

        this->saveProfiles();
        this->drawProfileList();
        this->onBackToProfileListPressed();
    }

    this->confirmDeleteProfileDialog->hide();
}

void GUI::onPrimeComboBoxChanged() {
    if (this->shouldIgnorePrimeComboBoxEvents) {
        return;
    }

    auto extraData        = this->primeComboBox->getActiveExtraData();
    auto selectedGpuPciId = extraData.first;
    auto profileOption    = extraData.second;

    profileOption->setValue(selectedGpuPciId);

    /* Reset to default */
    if (selectedGpuPciId.empty()) {
        this->currentApp->setIsUsingPrime(false);
        this->currentApp->setDevicePCIId("");
        this->currentApp->setPrimeDriverName("");
    } else {
        /* Set this app as a prime-enabled app */
        this->currentApp->setPrimeDriverName(
            this->availableGPUs[selectedGpuPciId]->getDriverName());
        this->currentApp->setIsUsingPrime(true);
        this->currentApp->setDevicePCIId(selectedGpuPciId);

        /* Add the missing options of the new driver */
        auto newDriverOptions
            = this->availableGPUs[selectedGpuPciId]->getOptionsMap();
        this->resolver->addMissingDriverOptions(this->currentApp,
                                                newDriverOptions);
    }

    this->drawApplicationOptions();
}
