#include "databasetree.h"

#include "database.h"
#include "databaseview.h"
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QPushButton>
#include <QSettings>
#include <QSqlError>
#include <QStackedWidget>
#include <QVBoxLayout>

DatabaseTreeSettings::DatabaseTreeSettings(const QStringList &data, QStringList keywords, QWidget *parent) : QDialog(parent)
{
    setWindowTitle(tr("Add tree grouping"));
    QVBoxLayout *vlayout = new QVBoxLayout(this);
    setLayout(vlayout);

    QStringList key = data[0].split('/');

    qsizetype dateobsindex = keywords.indexOf("DATE-OBS");
    if(dateobsindex != -1)
    {
        keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH-DAY");
        keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR-MONTH");
        keywords.insert(dateobsindex + 1, "DATE-OBS_YEAR");
    }

    for(int i = 0; i < 10; i++)
    {
        QComboBox *comboxBox = new QComboBox(this);
        comboxBox->addItem("");
        comboxBox->addItems(keywords);
        vlayout->addWidget(comboxBox);
        _keywordsSelect.append(comboxBox);
        if(i < key.size() && keywords.contains(key[i]))
            comboxBox->setCurrentText(key[i]);
    }

    vlayout->addWidget(new QLabel(tr("Aggregate function"), this));
    _aggregateFunction = new QComboBox(this);
    _aggregateFunction->addItems({"", "SUM", "COUNT", "AVG", "MIN", "MAX", "MEDIAN"});
    vlayout->addWidget(_aggregateFunction);
    _aggregateFunction->setToolTip(tr("This aggregate function will be applied to last level of grouping"));
    _aggregateFunction->setCurrentText(data[1]);

    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
    connect(buttonBox, &QDialogButtonBox::accepted, this, &DatabaseTreeSettings::acceptButton);
    connect(buttonBox, &QDialogButtonBox::rejected, this, &DatabaseTreeSettings::reject);
    vlayout->addWidget(buttonBox);
}

QString DatabaseTreeSettings::keywords() const
{
    QStringList keywords;
    for(QComboBox *box : _keywordsSelect)
    {
        if(box->currentIndex() > 0)
            keywords.append(box->currentText());
    }
    return keywords.join('/');
}

QString DatabaseTreeSettings::aggregrationFunc() const
{
    return _aggregateFunction->currentText();
}

void DatabaseTreeSettings::acceptButton()
{
    for(QComboBox *box : _keywordsSelect)
    {
        if(box->currentIndex() > 0)
        {
            QDialog::accept();
            return;
        }
    }
    QDialog::reject();
}

class TreeNode
{
public:
    TreeNode() = default;
    TreeNode(TreeNode *parent, const QVariant value, int level)
        :_parent(parent)
        ,_value(value)
        ,_level(level)
    {}
    const TreeNode* child(size_t idx) const
    {
        if(idx >= 0 && idx < _children.size())
            return _children[idx].get();
        return nullptr;
    }
    TreeNode* child(size_t idx)
    {
        if(idx >= 0 && idx < _children.size())
            return _children[idx].get();
        return nullptr;
    }
    TreeNode* parent() const
    {
        return _parent;
    }
    int row() const
    {
        if(_parent)
            return _parent->indexOf(this);
        return 0;
    }
    int childCount() const
    {
        if(!_init)return 1;
        return _children.size();
    }
    const QVariant& value() const
    {
        return _value;
    }
    void fill(const QVariantList &list)
    {
        _init = true;
        for(auto &item : list)
            _children.push_back(std::make_unique<TreeNode>(this, item, _level + 1));
    }
    bool filled() const
    {
        return _init;
    }
    int level() const
    {
        return _level;
    }
private:
    int indexOf(const TreeNode *child) const
    {
        auto f = [child](const std::unique_ptr<TreeNode> &i){ return i.get() == child; };
        auto it = std::find_if(_children.begin(), _children.end(), f);
        if(it != _children.end())return std::distance(_children.begin(), it);
        return -1;
    }
    TreeNode *_parent = nullptr;
    QVariant _value;
    std::vector<std::unique_ptr<TreeNode>> _children;
    bool _init = false;
    int _level = 0;
};

DatabaseTree::DatabaseTree(Database *database, QObject *parent) : QAbstractItemModel(parent)
    ,_database(database)
{
    _italicFont.setItalic(true);
}

void DatabaseTree::setKeys(const QStringList &keys)
{
    _keys = keys;
    if(!_loaded)return;
    beginResetModel();
    prepareQueries();
    _rootNode = std::make_unique<TreeNode>();
    fillNode(_rootNode.get());
    endResetModel();
}

QStringList DatabaseTree::keys() const
{
    return _keys;
}

QModelIndex DatabaseTree::index(int row, int column, const QModelIndex &parent) const
{
    if(!hasIndex(row, column, parent))
        return QModelIndex();

    TreeNode *node;

    if(!parent.isValid())
        node = _rootNode.get();
    else
        node = static_cast<TreeNode*>(parent.internalPointer());

    if(node)
    {
        TreeNode *child = node->child(row);
        if(child)return createIndex(row, column, child);
    }

    return QModelIndex();
}

QModelIndex DatabaseTree::parent(const QModelIndex &index) const
{
    if(!index.isValid())
        return QModelIndex();

    TreeNode *childNode = static_cast<TreeNode*>(index.internalPointer());
    const TreeNode *parentNode = childNode->parent();

    if (parentNode == _rootNode.get())
        return QModelIndex();

    return createIndex(parentNode->row(), 0, parentNode);
}

int DatabaseTree::rowCount(const QModelIndex &index) const
{
    if(index.column() > 0)return 0;

    TreeNode *node;
    if(!index.isValid())
        node = _rootNode.get();
    else
        node = static_cast<TreeNode*>(index.internalPointer());

    if(node && node->level() <= _keys.size())
        return node->childCount();

    return 0;
}

int DatabaseTree::columnCount(const QModelIndex &index) const
{
    Q_UNUSED(index);
    return 1;
}

QVariant DatabaseTree::data(const QModelIndex &index, int role) const
{
    if(!index.isValid())
        return QVariant();

    TreeNode *node = static_cast<TreeNode*>(index.internalPointer());
    if(node == nullptr)
        return QVariant();

    switch(role)
    {
    case Qt::FontRole:
    {
        if(node->value().toString().isNull())
            return _italicFont;
        return QVariant();
    }
    case Qt::DisplayRole:
    {
        QString str = node->value().toString();
        if(str.isNull())return "NULL";
        else return str;
    }
    default:
        return QVariant();
    }
}

bool DatabaseTree::canFetchMore(const QModelIndex &parent) const
{
    if(!parent.isValid())
        return false;

    TreeNode *node = static_cast<TreeNode*>(parent.internalPointer());
    //qDebug() << "Can Fetch more" << node->value();
    if(node)
        return !node->filled();
    return false;
}

void DatabaseTree::fetchMore(const QModelIndex &parent)
{
    if(!parent.isValid())
        return;

    TreeNode *node = static_cast<TreeNode*>(parent.internalPointer());
    //qDebug() << "Fetch more" << node->value();
    if(node)
    {
        fillNode(node);

        if(node->childCount() > 0)
        {
            beginInsertRows(parent, 0, node->childCount() - 1);
            endInsertRows();
        }
    }
}

QVariant DatabaseTree::headerData(int section, Qt::Orientation orientation, int role) const
{
    if(orientation == Qt::Horizontal && role == Qt::DisplayRole && section == 0)
        return _keys.join('/');
    return QVariant();
}

void DatabaseTree::load()
{
    if(!_loaded)
    {
        _loaded = true;
        setKeys(_keys);
    }
}

QSqlQuery DatabaseTree::getGroupQuery(const QString &aggregateFunc) const
{
    QStringList cols;
    QString join;
    QString sum;

    for(int i = 0; i < _keys.size(); i++)
    {
        join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id = h%1.id_file AND h%1.key = ?").arg(i);
        if(_keys[i] == "DATE-OBS_YEAR")
            cols.append(QString("STRFTIME('%Y', h%1.value)").arg(i));
        else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
            cols.append(QString("STRFTIME('%Y-%m', h%1.value)").arg(i));
        else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
            cols.append(QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i));
        else
            cols.append(QString("h%1.value").arg(i));

        if(i == _keys.size() - 1)
        {
            QString tmp = aggregateFunc + "(" + cols.last() + ")";
            if(aggregateFunc == "COUNT")
                cols.append("COUNT(*)");
            else
                cols.last() = tmp;
        }
    }

    QStringList group = cols;
    group.removeLast();
    QString sql = "SELECT " + cols.join(',') + " FROM fits_files AS f" + join + " GROUP BY " + group.join(',') + " ORDER BY " + cols.join(" NULLS LAST,") + " NULLS LAST";

    QSqlQuery query(sql, _database->db());
    for(auto &val : _keys)
    {
        if(val.startsWith("DATE-OBS_"))
            query.addBindValue("DATE-OBS");
        else
            query.addBindValue(val);
    }

    qDebug() << "Group query" << sql;
    if(!query.exec())
        qWarning() << "Group query failed" << query.lastError();

    return query;
}

void DatabaseTree::prepareQueries()
{
    if(!_loaded)return;

    _queries.clear();

    QString join;
    QString where;

    for(int i = 0; i < _keys.size(); i++)
        join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id = h%1.id_file AND h%1.key = ?").arg(i);

    for(int i = 0; i < _keys.size(); i++)
    {
        QString sql;
        QString col = QString("h%1.value").arg(i);
        if(_keys[i] == "DATE-OBS_YEAR")
            col = QString("STRFTIME('%Y', h%1.value)").arg(i);
        else if(_keys[i] == "DATE-OBS_YEAR-MONTH")
            col = QString("STRFTIME('%Y-%m', h%1.value)").arg(i);
        else if(_keys[i] == "DATE-OBS_YEAR-MONTH-DAY")
            col = QString("STRFTIME('%Y-%m-%d', h%1.value)").arg(i);

        sql = QString("SELECT %1 FROM fits_files AS f").arg(col) + join + where + QString(" GROUP BY %1 ORDER BY %1 NULLS LAST").arg(col);

        qDebug() << "Tree query for" << _keys[i] << sql;
        QSqlQuery query(sql, _database->db());
        for(auto &val : _keys)
        {
            if(val.startsWith("DATE-OBS_"))
                query.addBindValue("DATE-OBS");
            else
                query.addBindValue(val);
        }

        if(where.isEmpty())
            where += QString(" WHERE %1 IS ?").arg(col);
        else
            where += QString(" AND %1 IS ?").arg(col);
        _queries.append(std::move(query));
    }

    QSqlQuery files("SELECT f.file FROM fits_files AS f" + join + where + " GROUP BY f.id ORDER BY f.file", _database->db());
    for(auto &val : _keys)
    {
        if(val.startsWith("DATE-OBS_"))
            files.addBindValue("DATE-OBS");
        else
            files.addBindValue(val);
    }
    qDebug() << files.lastQuery();
    _queries.append(std::move(files));
}

void DatabaseTree::fillNode(TreeNode *node)
{
    if(node->filled())
        return;

    TreeNode *n = node;
    QVariantList vals;
    while(n->parent())
    {
        vals.prepend(n->value());
        n = n->parent();
    }

    int level = vals.size();
    if(level >= _queries.size())
    {
        qWarning() << "Level is too deep";
        node->fill({});
        return;
    }
    QSqlQuery &q = _queries[level];

    for(int i = 0; i < level; i++)
        q.bindValue(i + _keys.size(), vals[i]);
    if(!q.exec())
    {
        qWarning() << "Failed to execute query" << q.lastError() << q.lastQuery() << q.boundValues();
        node->fill({});
        return;
    }

    QVariantList list;
    while(q.next())
        list.append(q.value(0));

    node->fill(list);
}

DatabaseTreeView::DatabaseTreeView(Database *database, QWidget *parent) : QWidget(parent)
    ,_database(database)
{
    QVBoxLayout *vlayout = new QVBoxLayout(this);
    QHBoxLayout *hlayout = new QHBoxLayout;

    _model = new DatabaseTree(database, this);
    _treeView = new QTreeView(this);
    _treeView->setModel(_model);
    _treeView->setHeaderHidden(true);

    _tableView = new CopyTableView(this);
    _sqlModel = new QSqlQueryModel(this);
    _tableView->setModel(_sqlModel);

    QSettings settings;
    QStringList filters = settings.value("databasetreeview/filters", QStringList{"OBJECT", "OBJECT/IMAGETYP", "OBJECT/IMAGETYP/FILTER", "OBJECT/IMAGETYP/FILTER/EXPTIME",
                                                                                 "IMAGETYP/OBJECT/IMAGETYP/FILTER/EXPTIME", "IMAGETYP/DATE-OBS_YEAR/EXPTIME"}).toStringList();
    QStringList aggrFuncs = settings.value("databasetreeview/aggrFuncs", QStringList{"", "", "", "SUM", "SUM", "SUM"}).toStringList();
    int selectedFilter = settings.value("databasetreeview/selectedFilter", 2).toInt();

    _filters = new QComboBox(this);
    for(int i = 0; i < std::min(filters.size(), aggrFuncs.size()); i++)
    {
        _filters->addItem(filters[i] + " " + aggrFuncs[i], QStringList{filters[i], aggrFuncs[i]});
    }
    _filters->setCurrentIndex(selectedFilter);
    connect(_filters, &QComboBox::currentIndexChanged, this, &DatabaseTreeView::filterChanged);
    filterChanged(_filters->currentIndex());

    QStackedWidget *stackedWidget = new QStackedWidget(this);
    stackedWidget->addWidget(_treeView);
    stackedWidget->addWidget(_tableView);

    QPushButton *addButton = new QPushButton(tr("Add"), this);
    QPushButton *removeButton = new QPushButton(tr("Remove"), this);
    QPushButton *treeTableButton = new QPushButton(tr("Tree/Table"), this);
    treeTableButton->setCheckable(true);
    connect(treeTableButton, &QPushButton::clicked, [stackedWidget](bool checked){
        stackedWidget->setCurrentIndex(checked ? 1 : 0);
    });

    hlayout->addWidget(_filters, 1);
    hlayout->addWidget(addButton);
    hlayout->addWidget(removeButton);
    hlayout->addWidget(treeTableButton);

    vlayout->addLayout(hlayout);
    vlayout->addWidget(stackedWidget);

    connect(_treeView, &QTreeView::activated, [this](const QModelIndex &index){
        if(!_model->hasChildren(index))
        {
            QString path = _model->data(index).toString();
            emit loadFile(path);
        }
    });

    connect(addButton, &QPushButton::clicked, this, &DatabaseTreeView::addFilter);
    connect(removeButton, &QPushButton::clicked, this, &DatabaseTreeView::removeFilter);
}

DatabaseTreeView::~DatabaseTreeView()
{
    QStringList filters;
    QStringList aggrFuncs;
    for(int i = 0; i < _filters->count(); i++)
    {
        QStringList data = _filters->itemData(i).toStringList();
        filters.append(data[0]);
        aggrFuncs.append(data[1]);
    }

    QSettings settings;
    settings.setValue("databasetreeview/filters", filters);
    settings.setValue("databasetreeview/aggrFuncs", aggrFuncs);
    settings.setValue("databasetreeview/selectedFilter", _filters->currentIndex());
}

void DatabaseTreeView::addFilter()
{
    QStringList keywords = _database->getFitsKeywords();
    QStringList data = _filters->currentData().toStringList();
    DatabaseTreeSettings settings(data, keywords, this);
    int result = settings.exec();
    if(result == QDialog::Accepted)
    {
        QString keywords = settings.keywords();
        QString aggrFunc = settings.aggregrationFunc();
        QString text = keywords + " " + aggrFunc;
        int idx = _filters->findText(text);
        if(idx == -1)
        {
            _filters->addItem(text, QStringList{keywords, aggrFunc});
            _filters->setCurrentText(text);
        }
        else
        {
            _filters->setCurrentIndex(idx);
        }
    }
}

void DatabaseTreeView::removeFilter()
{
    if(_filters->count() > 1)
        _filters->removeItem(_filters->currentIndex());
}

void DatabaseTreeView::filterChanged(int index)
{
    QStringList data = _filters->itemData(index).toStringList();
    QStringList keys = data[0].split('/');
    _model->setKeys(keys);
    setQuery(data[1]);
}

void DatabaseTreeView::visible(bool visible)
{
    if(visible && !_loaded)
    {
        _loaded = true;
        _model->load();
        QStringList data = _filters->currentData().toStringList();
        setQuery(data[1]);
    }
}

void DatabaseTreeView::setQuery(const QString &func)
{
    if(!_loaded)return;

    QStringList keys = _model->keys();
    int i = 0;
    _sqlModel->setQuery(_model->getGroupQuery(func));
    if(!func.isEmpty())
    {
        QString tmp = func + "(" + keys.last() + ")";
        if(func == "COUNT")
            keys.append("COUNT");
        else
            keys.last() = tmp;
    }
    for(auto &key : keys)
        _sqlModel->setHeaderData(i++, Qt::Horizontal, key);

    _tableView->resizeColumnsToContents();
}
