/*
 * Copyright 2009- ECMWF.
 *
 * This software is licensed under the terms of the Apache Licence version 2.0
 * which can be obtained at http://www.apache.org/licenses/LICENSE-2.0.
 * In applying this licence, ECMWF does not waive the privileges and immunities
 * granted to it by virtue of its status as an intergovernmental organisation
 * nor does it submit to any jurisdiction.
 */

#include "VConfig.hpp"

#include <QDebug>
#include <boost/property_tree/json_parser.hpp>

#include "DirectoryHandler.hpp"
#include "SessionHandler.hpp"
#include "UiLog.hpp"
#include "UserMessage.hpp"
#include "VConfigLoader.hpp"
#include "VProperty.hpp"
#include "VSettings.hpp"
#include "ecflow/core/Filesystem.hpp"
#include "ecflow/core/Str.hpp"
#include "ecflow/core/Version.hpp"

VConfig* VConfig::instance_ = nullptr;

// #define _UI_CONFIG_LOAD_DEBUG

VConfig::VConfig() {
    appLongName_ = appName_ + " (" + ecf::Version::raw() + ")";

    // detect if the ui is to run via proxichains
    if (char* variable = getenv("ECFLOWUI_USE_PROXYCHAINS"); variable) {
        proxychainsUsed_ = true;
    }
}

VConfig::~VConfig() {
    for (auto& group : groups_) {
        delete group;
    }
    groups_.clear();
}

VConfig* VConfig::instance() {
    if (!instance_)
        instance_ = new VConfig();

    return instance_;
}

void VConfig::init(const std::string& parDirPath) {
    fs::path parDir(parDirPath);

    if (fs::exists(parDir) && fs::is_directory(parDir)) {
        // fs::directory_iterator it(parDir);

        // The conf files have to be loaded in alphabetical order!! At least NotifyChange require it!
        // So we read the paths into a vector and sort it.
        std::vector<fs::path> vec;
        copy(fs::directory_iterator(parDir), fs::directory_iterator(), back_inserter(vec));
        std::sort(vec.begin(), vec.end());

        // The paths are now in alphabetical order
        for (auto it = vec.begin(); it != vec.end(); ++it) {
            if (fs::is_regular_file(*it)) {
                std::string name = it->filename().string();
                if (name.find("_conf.json") != std::string::npos) {
                    loadInit(it->string());
                }
            }
        }
    }

    // Read gui definition for the editable properties
    std::string guiFile = DirectoryHandler::concatenate(parDir.string(), "ecflowview_gui.json");
    loadInit(guiFile);

    // Read gui definition for the editable properties that can be cutomised per server
    std::string guiServerFile = DirectoryHandler::concatenate(parDir.string(), "ecflowview_gui_server.json");
    loadInit(guiServerFile);

    // Load existing user settings for the editable properties
    loadSettings();
}

void VConfig::loadInit(const std::string& parFile) {
    // Parse param file using the boost JSON property tree parser
    using boost::property_tree::ptree;
    ptree pt;

    try {
        read_json(parFile, pt);
    }
    catch (const boost::property_tree::json_parser::json_parser_error& e) {
        std::string errorMessage = e.what();
        UserMessage::message(UserMessage::ERROR,
                             true,
                             std::string("Fatal error!\nVConfig::load() unable to parse definition file: " + parFile +
                                         "\nMessage: " + errorMessage));
        exit(1);
        return;
    }

    // Loop over the groups
    for (ptree::const_iterator itGr = pt.begin(); itGr != pt.end(); ++itGr) {
        ptree ptGr = itGr->second;

        // Get the group name and create it
        std::string groupName = itGr->first;
        auto* grProp          = new VProperty(groupName);
        groups_.push_back(grProp);

        UiLog().dbg() << "VConfig::loadInit() read config group: " << groupName;

        // Load the property parameters. It will recursively add all the
        // children properties.
        loadProperty(ptGr, grProp);

        // Add the group we created to the registered configloader
        VConfigLoader::process(groupName, grProp);
    }
}

void VConfig::loadProperty(const boost::property_tree::ptree& pt, VProperty* prop) {
    using boost::property_tree::ptree;

    ptree::const_assoc_iterator itProp;

    // Loop over the possible properties
    for (ptree::const_iterator it = pt.begin(); it != pt.end(); ++it) {
        std::string name = it->first;
        ptree ptProp     = it->second;

#ifdef _UI_CONFIG_LOAD_DEBUG
        UiLog().dbg() << "   VConfig::loadProperty() read item: " << name;
#endif
        // Default value
        if (name == "default") {
            auto val = ptProp.get_value<std::string>();
            prop->setDefaultValue(val);
        }

        // If it is just a key/value pair "line"
        else if (name == "line" && ptProp.empty()) {
            auto* chProp = new VProperty(name);
            prop->addChild(chProp);
            auto val = ptProp.get_value<std::string>();

            QString prefix = prop->param("prefix");
            if (!prefix.isEmpty())
                val = prefix.toStdString() + "." + val;

#ifdef _UI_CONFIG_LOAD_DEBUG
            UiLog().dbg() << "   VConfig::loadProperty() line: " << val;
#endif
            if (VProperty* lineEditProp = find(val)) {
#ifdef _UI_CONFIG_LOAD_DEBUG
                UiLog().dbg() << "     --> link found";
#endif
                chProp->setLink(lineEditProp);
            }
            else {
#ifdef _UI_CONFIG_LOAD_DEBUG
                UiLog().dbg() << "     --> link NOT found";
#endif
            }
        }
        // If the property is a "line" (i.e. a line with additional parameters)
        else if (prop->name() == "line" && name == "link") {
            auto val = ptProp.get_value<std::string>();

#ifdef _UI_CONFIG_LOAD_DEBUG
            UiLog().dbg() << "   VConfig::loadProperty() line link: " << val;
#endif
            if (VProperty* lineEditProp = find(val)) {
#ifdef _UI_CONFIG_LOAD_DEBUG
                UiLog().dbg() << "     --> link found";
#endif
                prop->setLink(lineEditProp);
            }
            else {
#ifdef _UI_CONFIG_LOAD_DEBUG
                UiLog().dbg() << "     --> link NOT found";
#endif
            }
        }

        // Here we only load the properties with
        // children (i.e. key/value pairs (like "line" etc above)
        // are ignored.
        else if (!ptProp.empty()) {
            auto* chProp = new VProperty(name);
            prop->addChild(chProp);
            loadProperty(ptProp, chProp);
            chProp->adjustAfterLoad();
        }
        else {
            QString val = QString::fromStdString(ptProp.get_value<std::string>());
            prop->setParam(QString::fromStdString(name), val);
        }
    }
}

VProperty* VConfig::find(const std::string& path) {
    VProperty* res = nullptr;

    for (auto it = groups_.begin(); it != groups_.end(); ++it) {
        VProperty* vGroup = *it;
        res               = vGroup->find(path);
        if (res) {
            return res;
        }
    }

    return res;
}

VProperty* VConfig::group(const std::string& name) {
    for (auto it = groups_.begin(); it != groups_.end(); ++it) {
        if ((*it)->strName() == name)
            return *it;
    }

    return nullptr;
}

VProperty* VConfig::cloneServerGui(VProperty* linkTarget) {
    VProperty* gr = find("gui_server.server");

    assert(gr);

    VProperty* cGr = gr->clone(true, false);

    std::vector<VProperty*> chVec;
    cGr->collectChildren(chVec);
    for (auto p : chVec) {
        if (p->link()) {
            p->setLink(linkTarget->find(p->link()->path()));
        }
    }

    return cGr;
}

// Saves the global settings that can be edited through the gui
void VConfig::saveSettings() {
    SessionItem* cs   = SessionHandler::instance()->current();
    std::string fName = cs->settingsFile();

    VProperty* guiProp = group("gui");

    saveSettings(fName, guiProp, nullptr, true);
}

// Saves the settings per server that can be edited through the servers option gui
void VConfig::saveSettings(const std::string& parFile, VProperty* guiProp, VSettings* vs, bool global) {
    using boost::property_tree::ptree;
    ptree pt;

    // Get editable properties. We will operate on the links.
    std::vector<VProperty*> linkVec;
    guiProp->collectLinks(linkVec);

    for (auto it = linkVec.begin(); it != linkVec.end(); ++it) {
        if (global) {
            if ((*it)->changed()) {
                pt.put((*it)->path(), (*it)->valueAsStdString());
            }
        }

        else {
            if (!(*it)->useMaster()) {
                pt.put((*it)->path(), (*it)->valueAsStdString());
            }
        }
    }

    // Add settings stored in VSettings
    if (vs) {
        // Loop over the possible properties
        for (ptree::const_iterator it = vs->propertyTree().begin(); it != vs->propertyTree().end(); ++it) {
            pt.add_child(it->first, it->second);
        }
    }

    write_json(parFile, pt);
}

// Loads the global settings that can be edited through the gui
void VConfig::loadSettings() {
    SessionItem* cs     = SessionHandler::instance()->current();
    std::string parFile = cs->settingsFile();

    VProperty* guiProp = group("gui");

    loadSettings(parFile, guiProp, true);
}

// Loads the settings per server that can be edited through the servers option gui
void VConfig::loadSettings(const std::string& parFile, VProperty* guiProp, bool global) {
    // We will operate on the links
    std::vector<VProperty*> linkVec;
    guiProp->collectLinks(linkVec);

    // Parse file using the boost JSON property tree parser
    using boost::property_tree::ptree;
    ptree pt;

    try {
        read_json(parFile, pt);
    }
    catch (const boost::property_tree::json_parser::json_parser_error& e) {
        if (fs::exists(parFile)) {
            std::string errorMessage = e.what();
            UserMessage::message(UserMessage::ERROR,
                                 true,
                                 std::string("Error! VConfig::loadSettings() unable to parse settings file: " +
                                             parFile + " Message: " + errorMessage));
        }
        return;
    }

    for (auto it = linkVec.begin(); it != linkVec.end(); ++it) {
        if (pt.get_child_optional((*it)->path()) != boost::none) {
            auto val = pt.get<std::string>((*it)->path());

            if (!global) {
                (*it)->setUseMaster(false);
            }

            (*it)->setValue(val);
        }
    }

    // nodeMenuMode was introduced in 4.7.0 but was renamed in 4.8.0. We need to make sure
    // it is read correctly when 4.8.0 started up for the first time.
    // TODO: In versions after 4.8.0 we can remove this code!!!
    if (global) {
        std::string prevPath = "menu.access.nodeMenuMode";
        std::string actPath  = "server.menu.nodeMenuMode";
        if (pt.get_child_optional(prevPath) != boost::none) {
            for (auto it = linkVec.begin(); it != linkVec.end(); ++it) {
                if ((*it)->path() == actPath) {
                    auto val = pt.get<std::string>(prevPath);
                    (*it)->setValue(val);
                    break;
                }
            }
        }
    }
}

void VConfig::loadImportedSettings(const boost::property_tree::ptree& pt, VProperty* guiProp) {
    std::vector<VProperty*> linkVec;
    guiProp->collectLinks(linkVec);

    for (auto it = linkVec.begin(); it != linkVec.end(); ++it) {
        if (pt.get_child_optional((*it)->path()) != boost::none) {
            auto val = pt.get<std::string>((*it)->path());
            (*it)->setValue(val);
        }
        else if ((*it)->master()) {
            (*it)->setUseMaster(true);
        }
    }
}

void VConfig::importSettings() {
    boost::property_tree::ptree pt;

    std::string globalRcFile(DirectoryHandler::concatenate(DirectoryHandler::rcDir(), "user.default.options"));
    if (readRcFile(globalRcFile, pt)) {
        VProperty* gr = VConfig::find("gui");
        loadImportedSettings(pt, gr);
        VConfig::saveSettings();
    }
}

bool VConfig::readRcFile(const std::string& rcFile, boost::property_tree::ptree& pt) {
    std::ifstream in(rcFile.c_str());

    if (!in.good())
        return false;
    ;

    bool hasValue = false;

    std::string line;
    while (getline(in, line)) {
        std::string buf;
        std::stringstream ssdata(line);
        std::vector<std::string> vec;

        while (ssdata >> buf) {
            vec.push_back(buf);
        }

        if (vec.size() >= 1) {
            std::vector<std::string> par;
            ecf::algorithm::split(par, vec[0], ":");

            if (par.size() == 2) {
                // Update
                if (par[0] == "timeout") {
                    pt.put("server.update.updateRateInSec", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "poll") {
                    pt.put("server.update.update", par[1]);
                    hasValue = true;
                }

                else if (par[0] == "drift") {
                    pt.put("server.update.adaptiveUpdate", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "maximum") {
                    pt.put("server.update.maxAdaptiveUpdateRateInMin", par[1]);
                    hasValue = true;
                }

                // Files
                else if (par[0] == "direct_read") {
                    pt.put("server.files.readFilesFromDisk", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "jobfile_length") {
                    pt.put("server.files.maxOutputFileLines", par[1]);
                    hasValue = true;
                }

                // Popup
                else if (par[0] == "aborted") {
                    pt.put("server.notification.aborted.enabled", par[1]);
                    pt.put("server.notification.aborted.popup", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "restarted") {
                    pt.put("server.notification.restarted.enabled", par[1]);
                    pt.put("server.notification.restarted.popup", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "late") {
                    pt.put("server.notification.late.enabled", par[1]);
                    pt.put("server.notification.late.popup", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "zombies") {
                    pt.put("server.notification.zombie.enabled", par[1]);
                    pt.put("server.notification.zombie.popup", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "aliases") {
                    pt.put("server.notification.alias.enabled", par[1]);
                    pt.put("server.notification.alias.popup", par[1]);
                    hasValue = true;
                }
                // Suites
                else if (par[0] == "new_suites") {
                    pt.put("suite_filter.autoAddNew", par[1]);
                    hasValue = true;
                }
                else if (par[0] == "suites") {
                    boost::property_tree::ptree suites;
                    suites.push_back(std::make_pair(std::string(""), boost::property_tree::ptree(par[1])));

                    for (unsigned int j = 1; j < vec.size(); j++) {
                        suites.push_back(std::make_pair(std::string(""), boost::property_tree::ptree(vec.at(j))));
                    }

                    pt.put_child("suite_filter.suites", suites);

                    pt.put("suite_filter.enabled", "true");

                    hasValue = true;
                }
            }
        }

    } // while(getline)

    in.close();

    return hasValue;
}
