#include "database.h"
#include <QStandardPaths>
#include <QDir>
#include <QSqlError>
#include <QDebug>
#include <QDateTime>
#include "loadimage.h"

Database::Database(QObject *parent) : QObject(parent)
{
}

bool Database::init(const QLatin1String &connectionName)
{
    QString path = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation);
    QDir dir(path);

    database = QSqlDatabase::addDatabase("QSQLITE", connectionName);
    ngc = QSqlDatabase::addDatabase("QSQLITE", connectionName + "ngc");

    if(!dir.mkpath("."))
        return false;

    if(ngc.isValid())
    {
        QString ngcDb = dir.absoluteFilePath("ngc.db");
        if(!QFile::exists(ngcDb))
            QFile::copy(":/ngc.db", ngcDb);

        ngc.setDatabaseName(ngcDb);
        if(ngc.open())
        {
            m_getNgc = QSqlQuery(ngc);
            m_getNgc.prepare("SELECT *,IIF(V_Mag IS NULL, B_Mag, V_Mag) AS mag FROM ngc WHERE RA_deg BETWEEN ? AND ? AND DEC_deg BETWEEN ? AND ?");
        }
        else
        {
            qWarning() << "Could not open NGC database";
        }
    }

    if(database.isValid())
    {
        database.setDatabaseName(dir.absoluteFilePath("database2.db"));
        if(database.open())
        {
            QSqlQuery query(database);
            query.exec("PRAGMA foreign_keys = ON");
            int version = checkVersion(database);
            if(version == 0)
            {
                query.exec("PRAGMA user_version = 1");
                query.exec("CREATE TABLE IF NOT EXISTS files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE)");
                query.exec("CREATE TABLE IF NOT EXISTS fits_files (id INTEGER PRIMARY KEY AUTOINCREMENT, file VARCHAR(255) UNIQUE, mtime DATETIME,"
                                " minRa REAL, maxRa REAL, minDec REAL, maxDec REAL, crVal1 REAL, crVal2 REAL)");
                query.exec("CREATE TABLE IF NOT EXISTS fits_headers (id INTEGER PRIMARY KEY AUTOINCREMENT, id_file INTEGER,"
                                "key VARCHAR(81), value VARCHAR(81), comment VARCHAR(81), FOREIGN KEY(id_file) REFERENCES fits_files(id) ON DELETE CASCADE)");
                query.exec("CREATE INDEX IF NOT EXISTS key_value ON fits_headers(key, value)");
                query.exec("CREATE INDEX IF NOT EXISTS id_file ON fits_headers(id_file)");
                query.exec("CREATE INDEX IF NOT EXISTS minRa_idx ON fits_files(minRa)");
                query.exec("CREATE INDEX IF NOT EXISTS maxRa_idx ON fits_files(maxRa)");
                query.exec("CREATE INDEX IF NOT EXISTS minDec_idx ON fits_files(minDec)");
                query.exec("CREATE INDEX IF NOT EXISTS maxDec_idx ON fits_files(maxDec)");
                version = 1;
            }
            if(version == 1)
            {
                query.exec("CREATE INDEX IF NOT EXISTS id_file_key ON fits_headers(id_file, key)");
                query.exec("PRAGMA user_version = 2");
                version = 2;
            }
            if(version > 2)
            {
                qWarning() << "Database version is too new";
                return false;
            }

            QSqlError error = database.lastError();

            if(error.type() == QSqlError::NoError)
            {
                m_markQuery = QSqlQuery(database);
                m_markQuery.prepare("INSERT INTO files (file) VALUES (?)");
                m_unmarkQuery = QSqlQuery(database);
                m_unmarkQuery.prepare("DELETE FROM files WHERE file = (?)");
                m_isMarkedQuery = QSqlQuery(database);
                m_isMarkedQuery.prepare("SELECT * FROM files WHERE file = (:name)");

                m_insertFile = QSqlQuery(database);
                m_insertFile.prepare("INSERT INTO fits_files (file, mtime) VALUES (?, ?)");
                m_insertFileWcs = QSqlQuery(database);
                m_insertFileWcs.prepare("INSERT INTO fits_files (file, mtime, minRa, maxRa, minDec, maxDec, crVal1, crVal2) VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
                m_insertFitsHeader = QSqlQuery(database);
                m_insertFitsHeader.prepare("INSERT INTO fits_headers (id_file, key, value, comment) VALUES (?, ?, ?, ?)");
                m_checkFile = QSqlQuery(database);
                m_checkFile.prepare("SELECT id,mtime FROM fits_files WHERE file=?");
                m_headerKeywords = QSqlQuery(database);
                m_headerKeywords.prepare("SELECT DISTINCT key FROM fits_headers ORDER BY key");
                m_deleteFile = QSqlQuery(database);
                m_deleteFile.prepare("DELETE FROM fits_files WHERE id=?");
                return true;
            }
            qWarning() << error.text();
        }
        else
        {
            qWarning() << "Failed to open database" << connectionName;
        }
    }
    else
    {
        qWarning() << "Database is invalid";
    }
    return false;
}

bool Database::mark(const QString &filename)
{
    m_markQuery.bindValue(0, filename);
    m_markQuery.exec();
    return checkError(m_markQuery);
}

bool Database::unmark(const QString &filename)
{
    m_unmarkQuery.bindValue(0, filename);
    m_unmarkQuery.exec();
    return checkError(m_unmarkQuery);
}

bool Database::mark(const QStringList &filenames)
{
    m_markQuery.bindValue(0, filenames);
    m_markQuery.execBatch();
    return checkError(m_markQuery);
}

bool Database::unmark(const QStringList &filenames)
{
    m_unmarkQuery.bindValue(0, filenames);
    m_unmarkQuery.execBatch();
    return checkError(m_unmarkQuery);
}

bool Database::isMarked(const QString &filename)
{
    m_isMarkedQuery.bindValue(":name", filename);
    m_isMarkedQuery.exec();
    checkError(m_isMarkedQuery);
    return m_isMarkedQuery.next();
}

QStringList Database::getMarkedFiles()
{
    QSqlQuery markedFiles("SELECT * from files");

    QStringList files;
    while(markedFiles.next())
    {
        files << markedFiles.value("file").toString();
    }

    return files;
}

void Database::clearMarkedFiles()
{
    QSqlQuery query("DELETE FROM files");
}

bool Database::checkError(QSqlQuery &query)
{
    QSqlError error = query.lastError();
    if(error.type() == QSqlError::NoError)
        return true;
    else
    {
        qWarning() << error.text();
        return false;
    }
}

int Database::checkVersion(QSqlDatabase &db)
{
    QSqlQuery query("PRAGMA user_version", db);
    if(query.next())
        return query.value(0).toInt();
    return -1;
}

static QStringList nameFilters = {"*.fit", "*.fits", "*.fz", "*.fts", "*.xisf"};

static int countFiles(const QDir &dir, QStringList &scannedDirs)
{
    if(scannedDirs.contains(dir.canonicalPath()))return 0;
    scannedDirs.append(dir.canonicalPath());

    int count = dir.entryList(nameFilters, QDir::Files).size();
    QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
    for(const QString &d : dirs)
        count += countFiles(dir.filePath(d), scannedDirs);
    return count;
}

void Database::indexDir(const QDir &dir, QProgressDialog *progress)
{
    m_progress = 0;
    QStringList scannedDirs;
    int count = countFiles(dir, scannedDirs);
    progress->setMaximum(count);
    database.transaction();

    scannedDirs.clear();
    if(indexDir2(dir, progress, scannedDirs))
    {
        database.commit();
        emit databaseChanged();
    }
    else
    {
        database.rollback();
    }
}

void Database::reindex(QProgressDialog *progress)
{
    QVariantList deleteids;
    database.transaction();
    QSqlQuery size("SELECT COUNT(*) FROM fits_files", database);
    size.next();
    progress->setMaximum(size.value(0).toInt());
    QSqlQuery files("SELECT id,file,mtime FROM fits_files", database);
    int i = 0;
    while(files.next())
    {
        QString path = files.value(1).toString();
        QFileInfo file(path);
        if(file.exists() && file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate) != files.value(2).toString())
            indexFile(file);
        if(!file.exists())
            deleteids.append(files.value(0));
        progress->setValue(i++);
        if(progress->wasCanceled())
        {
            database.rollback();
            return;
        }
    }
    QSqlQuery deleteFiles("DELETE FROM fits_files WHERE id = ?", database);
    deleteFiles.bindValue(0, deleteids);
    deleteFiles.execBatch();
    database.commit();
}

QStringList Database::getFitsKeywords()
{
    m_headerKeywords.exec();
    QStringList keywords;
    while(m_headerKeywords.next())
    {
        keywords << m_headerKeywords.value(0).toString();
    }
    return keywords;
}

QVector<SkyObject> Database::getObjects(double minRa, double maxRa, double minDec, double maxDec)
{
    QVector<SkyObject> objects;
    if(!ngc.isOpen())return objects;

    m_getNgc.bindValue(0, minRa);
    m_getNgc.bindValue(1, maxRa);
    m_getNgc.bindValue(2, minDec);
    m_getNgc.bindValue(3, maxDec);

    if(m_getNgc.exec())
    {
        while(m_getNgc.next())
        {
            QString name;
            QString name2;
            QString m = m_getNgc.value("M").toString();
            QString ic = m_getNgc.value("IC").toString();
            if(!m.isEmpty())
            {
                name = "M" + m;
                m.clear();
            }
            else if(!ic.isEmpty())
            {
                name = "IC" + ic;
                ic.clear();
            }
            else
            {
                name = m_getNgc.value("Name").toString();
            }

            if(!ic.isEmpty())name2 += "IC" + ic + " ";
            name2 += m_getNgc.value("Common names").toString();

            objects.append({
                name,
                name2,
                {m_getNgc.value("RA_deg").toDouble(), m_getNgc.value("DEC_deg").toDouble()},
                m_getNgc.value("MajAx").toDouble(),
                m_getNgc.value("MinAx").toDouble(),
                m_getNgc.value("PosAng").toDouble(),
                m_getNgc.value("mag").isNull() ? NAN : m_getNgc.value("mag").toDouble(),
                {0, 0},
            });
        }
    }
    return objects;
}

const QSqlDatabase &Database::db() const
{
    return database;
}

bool Database::indexDir2(const QDir &dir, QProgressDialog *progress, QStringList &scannedDirs)
{
    if(scannedDirs.contains(dir.canonicalPath()))return true;
    scannedDirs.append(dir.canonicalPath());

    QFileInfoList files = dir.entryInfoList(nameFilters, QDir::Files);
    QStringList dirs = dir.entryList(QDir::Dirs | QDir::NoDotAndDotDot);

    for(const QString &d : dirs)
    {
        if(!indexDir2(dir.filePath(d), progress, scannedDirs))
            return false;
    }
    for(const QFileInfo &file : files)
    {
        progress->setValue(m_progress++);
        if(progress->wasCanceled())return false;
        if(!indexFile(file))return false;
    }
    return true;
}

bool Database::indexFile(const QFileInfo &file)
{
    ImageInfoData info;
    QString filePath = file.absoluteFilePath();
    QString mtime = file.fileTime(QFileDevice::FileModificationTime).toUTC().toString(Qt::ISODate);
    m_checkFile.bindValue(0, filePath);
    m_checkFile.exec();
    if(m_checkFile.next())
    {
        if(m_checkFile.value(1).toString() == mtime)
            return true;
        else
        {
            m_deleteFile.bindValue(0, m_checkFile.value(0).toLongLong());
            m_deleteFile.exec();
        }
    }

    bool ok = false;
    if(isXISF(file.suffix()))
        ok = readXISFHeader(filePath, info);
    else if(isFITS(file.suffix()))
        ok = readFITSHeader(filePath, info);

    qlonglong last_id = -1;
    if(ok)
    {
        if(info.wcs)
        {
            double minRa, maxRa, minDec, maxDec, crVal1, crVal2;
            info.wcs->calculateBounds(minRa, maxRa, minDec, maxDec, crVal1, crVal2);
            qDebug() << "bounds" << minRa << maxRa << minDec << maxDec;
            m_insertFileWcs.bindValue(0, filePath);
            m_insertFileWcs.bindValue(1, mtime);
            m_insertFileWcs.bindValue(2, minRa);
            m_insertFileWcs.bindValue(3, maxRa);
            m_insertFileWcs.bindValue(4, minDec);
            m_insertFileWcs.bindValue(5, maxDec);
            m_insertFileWcs.bindValue(6, crVal1);
            m_insertFileWcs.bindValue(7, crVal2);
            if(!m_insertFileWcs.exec())
            {
                qWarning() << "Database error" << m_insertFileWcs.lastError();
                return false;
            }
            last_id = m_insertFileWcs.lastInsertId().toLongLong();
        }
        else
        {
            m_insertFile.bindValue(0, filePath);
            m_insertFile.bindValue(1, mtime);
            if(!m_insertFile.exec())
            {
                qWarning() << "Database error" << m_insertFile.lastError();
                return false;
            }
            last_id = m_insertFile.lastInsertId().toLongLong();
        }

        QVariantList file_id, keys, values, comments;
        for(const auto &record : info.fitsHeader)
        {
            if(record.xisf && record.key.startsWith("PCL:"))continue;
            file_id << last_id;
            keys << QString(record.key);
            values << record.value.toString();
            comments << QString(record.comment);
        }
        m_insertFitsHeader.bindValue(0, file_id);
        m_insertFitsHeader.bindValue(1, keys);
        m_insertFitsHeader.bindValue(2, values);
        m_insertFitsHeader.bindValue(3, comments);
        if(!m_insertFitsHeader.execBatch())
        {
            qWarning() << "Database error" << m_insertFitsHeader.lastError();
            return false;
        }
    }
    qDebug() << "Indexed" << filePath << last_id;
    return true;
}
