#include "batchprocessing.h"
#include "databaseview.h"
#include <QClipboard>
#include <QContextMenuEvent>
#include <QDebug>
#include <QDialogButtonBox>
#include <QGuiApplication>
#include <QHeaderView>
#include <QMenu>
#include <QMimeData>
#include <QPushButton>
#include <QRegularExpression>
#include <QSettings>
#include <QSqlError>
#include <QTimer>
#include <QVBoxLayout>
#include <iostream>

const QStringList DEFAULT_COLUMNS = {"EXPTIME", "OBJECT", "RA", "DEC"};

double RA(const QString &ra)
{
    QRegularExpression reg("(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
    QRegularExpressionMatch match = reg.match(ra);
    double h = match.captured(1).toDouble();
    double m = match.captured(2).toDouble();
    double s = match.captured(3).toDouble();
    qDebug() << "RA" << match.capturedTexts() << h << m << s;
    return h*15 + m*0.25 + s*15/3600;
}

double DEC(const QString &dec)
{
    QRegularExpression reg("([\\+\\-])?(\\d+)\\s*(\\d+)?\\s*(\\d+)?");
    QRegularExpressionMatch match = reg.match(dec);
    double sign = match.captured(1) == "-" ? -1 : 1;
    double d = match.captured(2).toDouble();
    double m = match.captured(3).toDouble();
    double s = match.captured(4).toDouble();
    qDebug() << "DEC" << match.capturedTexts() << sign << d << m << s;
    return sign * (d + m/60 + s/3600);
}

SelectColumnsDialog::SelectColumnsDialog(QWidget *parent) : QDialog(parent)
{
    m_listWidget = new QListWidget(this);
    m_listWidget->setSelectionMode(QAbstractItemView::MultiSelection);
    QVBoxLayout *layout = new QVBoxLayout(this);
    setLayout(layout);
    layout->addWidget(m_listWidget);

    QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
    connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
    connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
    layout->addWidget(buttonBox);

    setWindowTitle(tr("Select columns"));
}

void SelectColumnsDialog::setColumns(QStringList columns)
{
    QSettings settings;
    m_listWidget->addItems(columns);
    QStringList selected = settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList();
    for(auto &sel : selected)
    {
        int i = columns.indexOf(sel);
        if(i>=0)
            m_listWidget->item(i)->setSelected(true);
    }
}

QStringList SelectColumnsDialog::selectedColumns()
{
    QStringList ret;
    for(auto &sel : m_listWidget->selectedItems())
        ret.append(sel->text());
    return ret;
}

FITSFileModel::FITSFileModel(Database *database, QObject *parent) : QSqlQueryModel(parent)
  , m_database(database)
{
}

void FITSFileModel::sort(int column, Qt::SortOrder order)
{
    if(column < 0)
        m_sort.clear();
    else if(column <= m_columns.size())
    {
        if(column == 0)m_sort = " ORDER BY f.file ";
        else m_sort = QString(" ORDER BY \"h%1_value\" ").arg(column-1);
        m_sort += order == Qt::AscendingOrder ? "ASC" : "DESC";
        prepareQuery();
    }
}

void FITSFileModel::setColumns(const QStringList &columns)
{
    m_columns = columns;
    prepareQuery();
}

void FITSFileModel::setFilter(const QStringList &key, const QStringList &value, const QStringList &limit)
{
    if(value.isEmpty())
    {
        m_key.clear();
        m_value.clear();
        m_limit.clear();
    }
    else
    {
        m_key = key;
        m_value = value;
        m_limit = limit;
    }
    prepareQuery();
}

QVariant FITSFileModel::data(const QModelIndex &index, int role) const
{
    if(role == Qt::FontRole && index.column() == 0)
    {
        QFont font;
        QString file = index.data().toString();
        font.setBold(m_markedFiles.contains(file));
        return font;
    }
    if(role == Qt::ToolTipRole && index.column() == 0)
    {
        return QSqlQueryModel::data(index, Qt::DisplayRole);
    }
    return QSqlQueryModel::data(index, role);
}

void FITSFileModel::filesMarked(const QModelIndexList &indexes)
{
    for(auto &index : indexes)
    {
        QString file = index.data().toString();
        if(!m_markedFiles.contains(file))
        {
            m_markedFiles.insert(file);
            emit dataChanged(index, index, {Qt::FontRole});
        }
    }
}

void FITSFileModel::filesUnmarked(const QModelIndexList &indexes)
{
    for(auto &index : indexes)
    {
        QString file = index.data().toString();
        if(m_markedFiles.contains(file))
        {
            m_markedFiles.remove(file);
            emit dataChanged(index, index, {Qt::FontRole});
        }
    }
}

void FITSFileModel::load()
{
    if(!m_loaded)
    {
        m_loaded = true;
        prepareQuery();
    }
}

void FITSFileModel::prepareQuery()
{
    if(!m_loaded)return;

    QString cols;
    QString join;
    QStringList where;
    QString sql = m_columns.size() ? "SELECT f.file," : "SELECT f.file";
    QVariantList bindValues;
    QVariantList bindValuesJoin;
    for(int i=0; i<m_value.size(); i++)
    {
        if(m_key[i] == "file")
        {
            where.append(" f.file LIKE ? ");
            bindValues.append(m_value[i]);
        }
        else if(m_key[i] == "RA pos")
        {
            where.append(" ? BETWEEN f.minRa AND f.maxRa ");
            bindValues.append(RA(m_value[i]));
        }
        else if(m_key[i] == "DEC pos")
        {
            where.append(" ? BETWEEN f.minDec AND f.maxDec ");
            bindValues.append(DEC(m_value[i]));
        }
        else if(m_key[i] == "RA range")
        {
            where.append(" crVal1 BETWEEN ? AND ? ");
            bindValues.append(RA(m_value[i]));
            bindValues.append(RA(m_limit[i]));
        }
        else if(m_key[i] == "DEC range")
        {
            where.append(" crVal2 BETWEEN ? AND ? ");
            bindValues.append(DEC(m_value[i]));
            bindValues.append(DEC(m_limit[i]));
        }
        else
        {
            join += QString(" JOIN fits_headers AS s%1 ON f.id=s%1.id_file AND s%1.key=? AND s%1.value LIKE ? ").arg(i);
            bindValuesJoin.append(m_key[i]);
            bindValuesJoin.append(m_value[i]);
        }
    }
    int i=0;
    for(auto &column : m_columns)
    {
        cols += QString("GROUP_CONCAT(h%1.value) AS h%1_value,").arg(i);
        join += QString(" LEFT JOIN fits_headers AS h%1 ON f.id=h%1.id_file AND h%1.key=?").arg(i);
        bindValuesJoin.append(column);
        i++;
    }
    cols.chop(1);
    sql += cols;
    sql += " FROM fits_files AS f";
    sql += join;
    if(!where.isEmpty())sql += " WHERE " + where.join("AND");
    sql += " GROUP BY f.id" + m_sort;

    QSqlQuery query(m_database->db());
    query.prepare(sql);
    for(auto &val : bindValuesJoin)
        query.addBindValue(val);
    for(auto &val : bindValues)
        query.addBindValue(val);

    if(!query.exec())
        qWarning() << "Failed to exectute query" << query.lastQuery() << bindValuesJoin << bindValues;
    else
        setQuery(std::move(query));

    setHeaderData(0, Qt::Horizontal, tr("File name"));
    i = 1;
    for(auto &column : m_columns)
    {
        setHeaderData(i++, Qt::Horizontal, column);
    }
    qDebug() << "DB SQL" << sql;
    if(lastError().type() != QSqlError::NoError)
        qDebug() << "Database error" << lastError();

    QStringList list = m_database->getMarkedFiles();
    m_markedFiles = QSet<QString>(list.begin(), list.end());
}

DatabaseTableView::DatabaseTableView(QWidget *parent) : CopyTableView(parent)
{
}

void DatabaseTableView::contextMenuEvent(QContextMenuEvent *event)
{
    QMenu menu;
    QAction *mark = menu.addAction(tr("Mark"));
    QAction *unmark = menu.addAction(tr("Unmark"));
    QAction *open = menu.addAction(tr("Open"));
    QAction *openDirAction = menu.addAction(tr("Open file location"));
    QAction *copyPath = menu.addAction(tr("Copy files"));

    QAction *a = menu.exec(event->globalPos());
    if(a == nullptr)
        return;

    QModelIndexList indexes = selectionModel()->selectedRows();

    if(a == mark)
        emit filesMarked(indexes);
    else if(a == unmark)
        emit filesUnmarked(indexes);
    else if(a == open)
        emit openFile(indexes);
    else if(a == openDirAction)
        emit openDir(indexes);
    else if(a == copyPath)
    {
        QStringList paths;
        QList<QUrl> urls;
        for(auto &index : indexes)
        {
            QString path = index.siblingAtColumn(0).data().toString();
            paths.append(path);
            urls.append(QUrl::fromLocalFile(path));
        }
        QMimeData *data = new QMimeData;
        data->setUrls(urls);
        data->setText(paths.join('\n'));
        QClipboard *clipboard = QGuiApplication::clipboard();
        clipboard->setMimeData(data);
    }
}

DataBaseView::DataBaseView(Database *database, QWidget *parent) : QWidget(parent)
  ,m_database(database)
{
    QVBoxLayout *layout = new QVBoxLayout(this);
    setLayout(layout);

    m_tableView = new DatabaseTableView(this);
    m_tableView->verticalHeader()->setDefaultSectionSize(1);
    m_tableView->setSortingEnabled(true);
    m_tableView->horizontalHeader()->setSortIndicatorShown(true);
    m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows);
    layout->addWidget(m_tableView);
    connect(m_tableView, &QTableView::activated, this, &DataBaseView::itemActivated);

    m_model = new FITSFileModel(m_database, this);

    QSettings settings;
    m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
    m_tableView->setModel(m_model);
    QTimer::singleShot(200, [this](){
        QSettings settings;
        m_tableView->horizontalHeader()->restoreState(settings.value("databaseview/header").toByteArray());
    });

    QHBoxLayout *hlayout = new QHBoxLayout();
    layout->addLayout(hlayout);

    QPushButton *selectColumnsButton = new QPushButton(tr("Select columns"), this);
    hlayout->addWidget(selectColumnsButton);
    connect(selectColumnsButton, &QPushButton::pressed, this, &DataBaseView::selectColumns);

    connect(m_tableView, &DatabaseTableView::filesMarked, [this](QModelIndexList indexes){
        QStringList files;
        for(auto &index : indexes)
            files.append(index.data().toString());
        m_database->mark(files);
        m_model->filesMarked(indexes);
    });
    connect(m_tableView, &DatabaseTableView::filesUnmarked, [this](QModelIndexList indexes){
        QStringList files;
        for(auto &index : indexes)
            files.append(index.data().toString());
        m_database->unmark(files);
        m_model->filesUnmarked(indexes);
    });
    connect(m_tableView, &DatabaseTableView::openFile, [this](QModelIndexList indexes){
        if(indexes.size())
            emit loadFile(m_model->data(indexes.front().siblingAtColumn(0)).toString());
    });
    connect(m_tableView, &DatabaseTableView::openDir, [this](QModelIndexList indexes){
        if(indexes.size())
        {
            QFileInfo info(m_model->data(indexes.front().siblingAtColumn(0)).toString());
            openDir(info.absolutePath());
        }
    });

    auto addFilterItems = [](QComboBox *combobox, const QStringList &fitsKeywords)
    {
        combobox->clear();
        combobox->addItem("file");
        combobox->addItem("RA pos");
        combobox->addItem("DEC pos");
        combobox->addItem("RA range");
        combobox->addItem("DEC range");
        combobox->addItems(fitsKeywords);
    };

    QStringList fitsKeywords = m_database->getFitsKeywords();
    QStringList filterKey = settings.value("databaseview/filterKey", QStringList{"file", "file", "file"}).toStringList();
    for(int i=0; i<3; i++)
    {
        m_filterKeyword[i] = new QComboBox(this);
        m_filterKeyword[i]->setMaximumWidth(300);
        addFilterItems(m_filterKeyword[i], fitsKeywords);
        m_filterKeyword[i]->setCurrentText(filterKey[i]);

        m_search[i] = new QLineEdit(this);
        m_search[i]->setPlaceholderText(tr("Text to search, you can % as wildcard"));

        m_limit[i] = new QLineEdit(this);
        m_limit[i]->hide();
        connect(m_filterKeyword[i], static_cast<void(QComboBox::*)(int)>(&QComboBox::currentIndexChanged), [this, i](int index){
            if(index == 3 || index == 4)m_limit[i]->show();
            else m_limit[i]->hide();
        });

        connect(m_search[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
        connect(m_limit[i], &QLineEdit::returnPressed, this, &DataBaseView::applyFilter);
        hlayout->addWidget(m_filterKeyword[i]);
        hlayout->addWidget(m_search[i]);
        hlayout->addWidget(m_limit[i]);
    }

    QPushButton *filterButton = new QPushButton(tr("Filter"), this);
    connect(filterButton, &QPushButton::pressed, this, &DataBaseView::applyFilter);
    hlayout->addWidget(filterButton);

    connect(m_database, &Database::databaseChanged, [this, &addFilterItems](){
        QStringList fitsKeywords = m_database->getFitsKeywords();
        for(int i=0; i<3; i++)
            addFilterItems(m_filterKeyword[i], fitsKeywords);

        applyFilter();
    });
}

DataBaseView::~DataBaseView()
{
    QStringList filterKey;
    for(int i = 0; i < 3; i++)
        filterKey.append(m_filterKeyword[i]->currentText());

    QSettings settings;
    settings.setValue("databaseview/header", m_tableView->horizontalHeader()->saveState());
    settings.setValue("databaseview/filterKey", filterKey);
}

void DataBaseView::selectColumns()
{
    SelectColumnsDialog dialog;
    QStringList columns = m_database->getFitsKeywords();
    dialog.setColumns(columns);
    if(dialog.exec() == QDialog::Accepted)
    {
        QSettings settings;
        QStringList columns = dialog.selectedColumns();
        settings.setValue("databaseview/selectedColumns", columns);
        m_model->setColumns(columns);
    }
}

void DataBaseView::loadDatabase()
{
    QSettings settings;
    m_model->setColumns(settings.value("databaseview/selectedColumns", DEFAULT_COLUMNS).toStringList());
}

void DataBaseView::itemActivated(const QModelIndex &index)
{
    emit loadFile(m_model->data(index.siblingAtColumn(0)).toString());
}

void DataBaseView::applyFilter()
{
    QStringList keys;
    QStringList values;
    QStringList limits;
    for(int i=0; i<3; i++)
    {
        QString key = m_filterKeyword[i]->currentText();
        if(!m_search[i]->text().isEmpty() && !keys.contains(key))
        {
            keys.append(key);
            values.append(m_search[i]->text());
            limits.append(m_limit[i]->text());
        }
    }
    m_model->setFilter(keys, values, limits);
}

bool DataBaseView::exportCSV(const QString &path)
{
    QFile csv(path);
    if(!csv.open(QIODevice::WriteOnly | QIODevice::Text))
        return false;

    m_model->load();
    QSqlQuery sql(m_model->query().lastQuery());
    int colCount = m_model->columnCount();
    QStringList header;
    for(int i=0; i<colCount; i++)
         header.append(m_model->headerData(i, Qt::Horizontal).toString());

    csv.write(header.join(",").toUtf8());
    csv.write("\n");

    while(sql.next())
    {
        QStringList columns;
        for(int i=0; i<colCount; i++)
        {
            QString val = sql.value(i).toString();
            val.replace("\"", "\"\"");
            if(val.contains('"') || val.contains(','))
            {
                val.prepend('"');
                val.append('"');
            }
            columns.append(val);
        }
        csv.write(columns.join(",").toUtf8());
        csv.write("\n");
    }
    return true;
}

void DataBaseView::visible(bool visible)
{
    if(visible)m_model->load();
}

CopyTableView::CopyTableView(QWidget *parent) : QTableView(parent)
{
}

void CopyTableView::keyPressEvent(QKeyEvent *event)
{
    if(event->matches(QKeySequence::Copy))
    {
        QModelIndexList list = selectedIndexes();
        QString table;
        if(list.size() == 0)return;

        int row = list.first().row();
        int col = list.first().column();
        for(auto &index : list)
        {
            if(row != index.row())
                table.append('\n');
            else if(col != index.column())
                table.append('\t');

            table.append(index.data().toString());
            row = index.row();
            col = index.column();
        }
        qApp->clipboard()->setText(table);
        event->accept();
    }
    else
    {
        QTableView::keyPressEvent(event);
    }
}
