/*
 * 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 "ModelColumn.hpp"

#include <map>

#include <QDebug>

#include "DiagData.hpp"
#include "VConfig.hpp"
#include "VConfigLoader.hpp"
#include "VProperty.hpp"
#include "VSettingsLoader.hpp"

class ColumnDefinitions {
public:
    using storage_t = std::map<std::string, ModelColumn*>;

    ColumnDefinitions(): storage_{} {}
    ~ColumnDefinitions() {
        for(auto item: storage_) {
            delete item.second;
        }
    }

    void insert(const std::string& k, ModelColumn* v) {
        storage_[k] = v;
    }

    auto find(const std::string& k) const {
        return storage_.find(k);
    }

    auto end() {
        return storage_.end();
    }

    template <typename F>
    void for_each(F&& f) {
        for(auto item: storage_) {
            f(item.first, item.second);
        }
    }

private:
    storage_t storage_;
};

static ColumnDefinitions defs;

ModelColumn::ModelColumn(const std::string& id) : id_(id), diagStart_(-1), diagEnd_(-1) {
    defs.insert(id_, this);
}

ModelColumn::~ModelColumn() {
    for(auto && item: items_) {
        delete item;
    }
}

ModelColumn* ModelColumn::def(const std::string& id) {
    if (auto it = defs.find(id); it != defs.end()) {
        return it->second;
    }
    return nullptr;
}

ModelColumn* ModelColumn::tableModelColumn() {
    return ModelColumn::def("table_columns");
}

// int ModelColumn::indexOf(const std::string& id) const
//{
//     return indexOf(QString::fromStdString(id));
// }

int ModelColumn::indexOf(QString id) const {
    for (int i = 0; i < items_.count(); i++) {
        if (items_.at(i)->id_ == id)
            return i;
    }
    return -1;
}

void ModelColumn::loadItem(VProperty* p) {
    auto* obj     = new ModelColumnItem(p->strName());
    obj->label_   = p->param("label");
    obj->tooltip_ = p->param("tooltip");
    obj->icon_    = p->param("icon");
    obj->index_   = items_.count();
    items_ << obj;
}

void ModelColumn::loadExtraItem(QString name, QString label) {
    auto* obj     = new ModelColumnItem(name.toStdString(), true);
    obj->label_   = label;
    obj->tooltip_ = obj->label_;
    // obj->icon_=p->param("icon");
    obj->index_    = items_.count();
    obj->editable_ = true;

    if (hasDiag()) {
        items_.insert(diagStart_, obj);
        diagStart_++;
        diagEnd_++;
    }
    else {
        items_ << obj;
    }
}

void ModelColumn::loadDiagItem(QString name, QString label) {
    auto* obj     = new ModelColumnItem(name.toStdString(), true);
    obj->label_   = label;
    obj->tooltip_ = obj->label_;
    // obj->icon_=p->param("icon");
    obj->index_    = items_.count();
    obj->editable_ = false;
    items_ << obj;
}

void ModelColumn::addExtraItem(QString name, QString label) {
    if (indexOf(name) != -1)
        return;

    // Editable extra items are always inserted in front of the diag items
    int pos = items_.count();
    if (hasDiag()) {
        pos = diagStart_;
        Q_ASSERT(pos >= 0);
    }

    Q_EMIT addItemsBegin(pos, pos);
    // Q_EMIT appendItemBegin();
    loadExtraItem(name, label);
    // Q_EMIT appendItemEnd();
    Q_EMIT addItemsEnd(pos, pos);

    save();
}

void ModelColumn::changeExtraItem(int idx, QString name, QString label) {
    if (indexOf(name) != -1)
        return;

    if (!isExtra(idx) || !isEditable(idx))
        return;

    Q_EMIT changeItemBegin(idx);

    items_[idx]->id_    = name;
    items_[idx]->label_ = label;

    Q_EMIT changeItemEnd(idx);

    save();
}

void ModelColumn::removeExtraItem(QString name) {
    int idx = indexOf(name);
    if (idx != -1 && items_[idx]->isExtra() && items_[idx]->isEditable()) {
        Q_EMIT removeItemsBegin(idx, idx);

        ModelColumnItem* obj = items_[idx];
        items_.removeAt(idx);
        delete obj;

        if (hasDiag()) {
            diagStart_--;
            diagEnd_--;
            Q_ASSERT(diagStart_ >= 0);
        }

        Q_EMIT removeItemsEnd(idx, idx);

        save();
    }
}

bool ModelColumn::isSameDiag(DiagData* diag) const {
    if (diagStart_ >= 0 && diagEnd_ >= 0 && diag->count() == diagEnd_ - diagStart_ + 1) {
        for (int i = diagStart_; i <= diagEnd_; i++) {
            if (items_[i]->id_ != QString::fromStdString(diag->columnName(i - diagStart_))) {
                return false;
            }
        }
        return true;
    }
    return false;
}

void ModelColumn::setDiagData(DiagData* diag) {
    if (isSameDiag(diag))
        return;

    // Remove the current diag items
    if (diagStart_ >= 0 && diagEnd_ >= 0) {
        Q_EMIT removeItemsBegin(diagStart_, diagEnd_);
        for (int i = diagStart_; i <= diagEnd_; i++) {
            ModelColumnItem* obj = items_[diagStart_];
            items_.removeAt(diagStart_);
            delete obj;
        }
        Q_EMIT removeItemsEnd(diagStart_, diagEnd_);
        diagStart_ = -1;
        diagEnd_   = -1;
    }

    // Add the current diag items to the back of the items
    if (diag->count() <= 0)
        return;

    diagStart_ = items_.count();
    diagEnd_   = items_.count() + diag->count() - 1;
    Q_ASSERT(diagStart_ >= 0);
    Q_ASSERT(diagStart_ <= diagEnd_);
    Q_EMIT addItemsBegin(diagStart_, diagEnd_);

    for (int i = 0; i < diag->count(); i++) {
        QString n = QString::fromStdString(diag->columnName(i));
        loadDiagItem(n, n); // these are not editable items!!!
    }
    Q_EMIT addItemsEnd(diagStart_, diagEnd_);
}

void ModelColumn::save() {
    if (!configPath_.empty()) {
        if (VProperty* prop = VConfig::instance()->find(configPath_)) {
            QStringList lst;
            for (int i = 0; i < items_.count(); i++) {
                if (items_[i]->isExtra() && items_[i]->isEditable())
                    lst << items_[i]->id_;
            }
            if (lst.isEmpty())
                prop->setValue(prop->defaultValue());
            else
                prop->setValue(lst.join("/"));
        }
    }
}

void ModelColumn::load(VProperty* group) {
    Q_ASSERT(group);

    auto* m = new ModelColumn(group->strName());
    // Attention: Each ModelColumn object self-registers in `defs`!
    //            This means that the newly created object will live until `defs` (with static allocation) is released.

    for (int i = 0; i < group->children().size(); i++) {
        VProperty* p = group->children().at(i);
        m->loadItem(p);
    }

    // Define extra config property
    m->configPath_ = group->param("__config__").toStdString();
}

// Called via VSettingsLoader after the users settings are read
void ModelColumn::loadSettings() {
    defs.for_each([](auto&& k, auto&& v){v->loadUserSettings();});
}

// Load user defined settings
void ModelColumn::loadUserSettings() {
    // Load extra config
    if (!configPath_.empty()) {
        if (VProperty* p = VConfig::instance()->find(configPath_)) {
            QString pval = p->valueAsString();
            if (!pval.isEmpty() && pval != "__none__") {
                Q_FOREACH (QString s, pval.split("/")) {
                    loadExtraItem(s, s);
                }
            }
        }
    }
}

ModelColumnItem::ModelColumnItem(const std::string& id, bool extra)
    : id_(QString::fromStdString(id)),
      index_(-1),
      extra_(extra),
      editable_(extra) {
}

static SimpleLoader<ModelColumn> loaderQuery("query_columns");
static SimpleLoader<ModelColumn> loaderTable("table_columns");
static SimpleLoader<ModelColumn> loaderZombie("zombie_columns");
static SimpleLoader<ModelColumn> loaderTriggerGraph("trigger_graph_columns");
static SimpleLoader<ModelColumn> loaderOutput("output_columns");

static SimpleSettingsLoader<ModelColumn> settingsLoader;
