/**********************************************************************************************
    Copyright (C) 2014-2015 Oliver Eichler <oliver.eichler@gmx.de>

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

**********************************************************************************************/

#include "gis/db/IDBSqlite.h"

#include <QtSql>
#include <QtWidgets>

#include "CMainWindow.h"
#include "gis/db/macros.h"
#include "helpers/CProgressDialog.h"

IDBSqlite::IDBSqlite() {}

bool IDBSqlite::setupDB(const QString& filename, const QString& connectionName, QString& error) {
  // this is important!
  IDB::setup(connectionName);

  if (!QSqlDatabase::contains(connectionName)) {
    db = QSqlDatabase::addDatabase("QSQLITE", connectionName);
    db.setDatabaseName(filename);
    if (!db.open()) {
      qDebug() << "failed to open database" << db.lastError();
    }
  } else {
    db = QSqlDatabase::database(connectionName);
  }

  QSqlQuery query(db);

  QUERY_RUN("PRAGMA locking_mode=NORMAL", return false)
  QUERY_RUN("PRAGMA temp_store=MEMORY", return false)
  QUERY_RUN("PRAGMA default_cache_size=50", return false)
  QUERY_RUN("PRAGMA page_size=8192", return false)
  QUERY_RUN("PRAGMA synchronous=off", return false)

  // When migrating the database these tables are used.
  // Due to caching they can't be dropped right after the
  // migration. That is why we look for them on startup.
  // And delete them as a second chance.
  if (query.exec("select * from tmp_folders")) {
    QUERY_RUN("DROP TABLE tmp_folders;", NO_CMD)
  }
  if (query.exec("select * from tmp_items")) {
    QUERY_RUN("DROP TABLE tmp_items;", NO_CMD)
  }

  return setupDB(error);
}

bool IDBSqlite::initDB() {
  QSqlQuery query(db);

  QUERY_RUN("BEGIN TRANSACTION;", return false);
  try {
    if (query.exec("CREATE TABLE versioninfo ( version TEXT, type TEXT )")) {
      query.prepare("INSERT INTO versioninfo (version, type) VALUES(:version, 'QMapShack')");
      query.bindValue(":version", DB_VERSION);
      QUERY_EXEC(throw -1);
    }

    QUERY_RUN(
        "CREATE TABLE folders ("
        "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
        "type           INTEGER NOT NULL,"
        "keyqms         TEXT,"
        "date           DATETIME DEFAULT CURRENT_TIMESTAMP,"
        "name           TEXT NOT NULL,"
        "comment        TEXT,"
        "locked         BOOLEAN DEFAULT FALSE,"
        "data           BLOB,"
        "sortmode       INTEGER NOT NULL DEFAULT 0"
        ")",
        throw -1)

    QUERY_RUN(
        "CREATE TABLE items ("
        "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
        "type           INTEGER,"
        "keyqms         TEXT NOT NULL UNIQUE,"
        "date           DATETIME DEFAULT CURRENT_TIMESTAMP,"
        "icon           BLOB NOT NULL,"
        "name           TEXT NOT NULL,"
        "comment        TEXT,"
        "data           BLOB NOT NULL,"
        "hash           TEXT NOT NULL,"
        "last_user      TEXT DEFAULT 'QMapShack',"
        "last_change    DATETIME DEFAULT CURRENT_TIMESTAMP,"
        "trash          DATETIME DEFAULT NULL"
        ")",
        throw -1)

    QUERY_RUN(
        "CREATE TRIGGER items_update_last_change "
        "AFTER UPDATE ON items BEGIN "
        "UPDATE items SET last_change=CURRENT_TIMESTAMP WHERE id=NEW.id; "
        "END;",
        throw -1)

    query.prepare("INSERT INTO folders (type, name, comment) VALUES (2, :name, '')");
    query.bindValue(":name", db.connectionName());
    QUERY_EXEC(return false);

    QUERY_RUN(
        "CREATE TABLE folder2folder ("
        "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
        "parent         INTEGER NOT NULL,"
        "child          INTEGER NOT NULL,"
        "FOREIGN KEY(parent) REFERENCES folders(id),"
        "FOREIGN KEY(child) REFERENCES folders(id)"
        ")",
        throw -1)

    QUERY_RUN(
        "CREATE TABLE folder2item ("
        "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
        "parent         INTEGER NOT NULL,"
        "child          INTEGER NOT NULL,"
        "FOREIGN KEY(parent) REFERENCES folders(id),"
        "FOREIGN KEY(child) REFERENCES items(id)"
        ")",
        return false)

    QUERY_RUN(
        "CREATE TRIGGER folder2item_insert "
        "BEFORE INSERT ON folder2item BEGIN "
        "UPDATE items SET trash=NULL "
        "WHERE id=NEW.child; "
        "END;",
        throw -1);

    QUERY_RUN(
        "CREATE TRIGGER folder2item_delete "
        "AFTER DELETE ON folder2item BEGIN "
        "UPDATE items SET trash=CURRENT_TIMESTAMP "
        "WHERE id=OLD.child AND OLD.child NOT IN(SELECT child FROM folder2item); "
        "END;",
        throw -1);

    // create virtual table with search index
    QUERY_RUN("CREATE VIRTUAL TABLE searchindex USING fts4(id, comment)", throw -1);

    QUERY_RUN(
        "CREATE TRIGGER searchindex_update "
        "AFTER UPDATE ON items BEGIN "
        "UPDATE searchindex SET comment=NEW.comment "
        "WHERE id=OLD.id; "
        "END;",
        throw -1);

    QUERY_RUN(
        "CREATE TRIGGER searchindex_insert "
        "AFTER INSERT ON items BEGIN "
        "INSERT INTO searchindex(id, comment) VALUES(NEW.id, NEW.comment); "
        "END;",
        throw -1);

    QUERY_RUN("END TRANSACTION;", throw -1);
  } catch (int i) {
    if (i == -1) {
      QUERY_RUN("ROLLBACK;", return false);
      return false;
    }
  }

  return true;
}

bool IDBSqlite::migrateDB(int version) {
  QSqlQuery query(db);

  QUERY_RUN("BEGIN TRANSACTION;", return false);
  try {
    if (version < 2) {
      if (!migrateDB1to2()) {
        throw -1;
      }
    }

    if (version < 3) {
      if (!migrateDB2to3()) {
        throw -1;
      }
    }

    if (version < 4) {
      if (!migrateDB3to4()) {
        throw -1;
      }
    }

    if (version < 5) {
      if (!migrateDB4to5()) {
        throw -1;
      }
    }

    if (version < 6) {
      if (!migrateDB5to6()) {
        throw -1;
      }
    }

    QUERY_RUN("END TRANSACTION;", throw -1);
  } catch (int i) {
    if (i == -1) {
      QUERY_RUN("ROLLBACK;", return false);
      return false;
    }
  }

  query.prepare("UPDATE versioninfo set version=:version");
  query.bindValue(":version", DB_VERSION);
  QUERY_EXEC(return false);
  return true;
}

bool IDBSqlite::migrateDB1to2() {
  QSqlQuery query(db);

  QUERY_RUN("ALTER TABLE folders RENAME TO tmp_folders;", return false)

  QUERY_RUN(
      "CREATE TABLE folders ("
      "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
      "type           INTEGER NOT NULL,"
      "keyqms         TEXT,"
      "date           DATETIME DEFAULT CURRENT_TIMESTAMP,"
      "name           TEXT NOT NULL,"
      "comment        TEXT,"
      "locked         BOOLEAN DEFAULT FALSE,"
      "data           BLOB"
      ");",
      return false);

  QUERY_RUN("INSERT INTO folders(id,type,keyqms,date,name,comment,locked,data) SELECT * FROM tmp_folders;",
            return false);

  QUERY_RUN("ALTER TABLE items RENAME TO tmp_items;", return false);
  QUERY_RUN(
      "CREATE TABLE items ("
      "id             INTEGER PRIMARY KEY AUTOINCREMENT,"
      "type           INTEGER,"
      "keyqms         TEXT NOT NULL,"
      "date           DATETIME DEFAULT CURRENT_TIMESTAMP,"
      "icon           BLOB NOT NULL,"
      "name           TEXT NOT NULL,"
      "comment        TEXT,"
      "data           BLOB NOT NULL"
      ");",
      return false);
  QUERY_RUN("INSERT INTO items(id,type,keyqms,date,icon,name,comment,data) SELECT * FROM tmp_items;", return false);

  return true;
}

bool IDBSqlite::migrateDB2to3() {
  QSqlQuery query(db);

  QUERY_RUN("ALTER TABLE items ADD COLUMN hash TEXT NOT NULL DEFAULT '-'", return false);
  QUERY_RUN("ALTER TABLE items ADD COLUMN last_user TEXT NOT NULL DEFAULT 'QMapShack'", return false);
  QUERY_RUN("ALTER TABLE items ADD COLUMN last_change DATETIME NOT NULL DEFAULT '-'", return false);

  QUERY_RUN(
      "CREATE TRIGGER items_update_last_change "
      "AFTER UPDATE ON items BEGIN "
      "UPDATE items SET last_change=datetime(CURRENT_TIMESTAMP, 'localtime') WHERE id=NEW.id; "
      "END;",
      return false);

  QUERY_RUN("SELECT Count(*) FROM items", return false);
  query.next();
  quint32 N = query.value(0).toUInt();

  QUERY_RUN("SELECT id, type FROM items WHERE hash='-'", return false);

  PROGRESS_SETUP(tr("Update to database version 3. Migrate all GIS items."), 0, N,
                 CMainWindow::self().getBestWidgetForParent());
  progress.enableCancel(false);
  quint32 cnt = 0;
  while (query.next()) {
    PROGRESS(cnt++, ;);

    quint64 idItem = query.value(0).toULongLong();
    quint32 typeItem = query.value(1).toUInt();

    IGisItem* item = IGisItem::newGisItem(typeItem, idItem, db, nullptr);

    if (nullptr == item) {
      continue;
    }

    QSqlQuery query2(db);
    query2.prepare("UPDATE items SET hash=:hash, last_user='QMapShack' WHERE id=:id");
    query2.bindValue(":hash", item->getHash());
    query2.bindValue(":id", idItem);
    if (!query2.exec()) {
      qWarning() << query2.lastQuery();
      qWarning() << query2.lastError();
    }

    delete item;
  }

  return true;
}

bool IDBSqlite::migrateDB3to4() {
  QSqlQuery query(db);

  QUERY_RUN("ALTER TABLE items ADD COLUMN trash DATETIME DEFAULT NULL", return false);

  QUERY_RUN(
      "CREATE TRIGGER folder2item_insert "
      "BEFORE INSERT ON folder2item BEGIN "
      "UPDATE items SET trash=NULL "
      "WHERE id=NEW.child; "
      "END;",
      return false);

  QUERY_RUN(
      "CREATE TRIGGER folder2item_delete "
      "AFTER DELETE ON folder2item BEGIN "
      "UPDATE items SET trash=CURRENT_TIMESTAMP "
      "WHERE id=OLD.child AND OLD.child NOT IN(SELECT child FROM folder2item); "
      "END;",
      return false);

  return true;
}

bool IDBSqlite::migrateDB4to5() {
  QSqlQuery query(db);

  // create virtual table with search index
  QUERY_RUN("CREATE VIRTUAL TABLE searchindex USING fts4(id, comment)", return false);

  // get number of items in the database
  QUERY_RUN("SELECT Count(*) FROM items", return false);
  query.next();
  quint32 N = query.value(0).toUInt();

  // over all items
  QUERY_RUN("SELECT id, type FROM items", return false);
  PROGRESS_SETUP(tr("Update to database version 5. Migrate all GIS items."), 0, N,
                 CMainWindow::self().getBestWidgetForParent());
  progress.enableCancel(false);
  quint32 cnt = 0;
  while (query.next()) {
    PROGRESS(cnt++, ;);

    quint64 idItem = query.value(0).toULongLong();
    quint32 typeItem = query.value(1).toUInt();

    IGisItem* item = IGisItem::newGisItem(typeItem, idItem, db, nullptr);

    if (nullptr == item) {
      continue;
    }

    // get full size info text
    QString comment = item->getInfo(IGisItem::eFeatureShowName | IGisItem::eFeatureShowFullText);

    // replace comment with full size info text in items table
    QSqlQuery query2(db);
    query2.prepare("UPDATE items SET comment=:comment WHERE id=:id");
    query2.bindValue(":comment", comment);
    query2.bindValue(":id", idItem);
    if (!query2.exec()) {
      qWarning() << query2.lastQuery();
      qWarning() << query2.lastError();
    }

    // add item id and full size info text to virtual table
    query2.prepare("INSERT INTO searchindex(id, comment) VALUES(:id, :comment)");
    query2.bindValue(":id", idItem);
    query2.bindValue(":comment", comment);
    if (!query2.exec()) {
      qWarning() << query2.lastQuery();
      qWarning() << query2.lastError();
    }

    delete item;
  }

  QUERY_RUN(
      "CREATE TRIGGER searchindex_update "
      "AFTER UPDATE ON items BEGIN "
      "UPDATE searchindex SET comment=NEW.comment "
      "WHERE id=OLD.id; "
      "END;",
      return false);
  QUERY_RUN(
      "CREATE TRIGGER searchindex_insert "
      "AFTER INSERT ON items BEGIN "
      "INSERT INTO searchindex(id, comment) VALUES(NEW.id, NEW.comment); "
      "END;",
      return false);

  return true;
}

bool IDBSqlite::migrateDB5to6() {
  QSqlQuery query(db);

  QUERY_RUN("ALTER TABLE folders ADD COLUMN sortmode INTEGER NOT NULL DEFAULT 0", return false);

  // get number of items in the database
  QUERY_RUN("SELECT Count(*) FROM items", return false);
  query.next();
  quint32 N = query.value(0).toUInt();

  // over all items
  QUERY_RUN("SELECT id, type FROM items", return false);
  PROGRESS_SETUP(tr("Update to database version 6. Migrate all GIS items."), 0, N,
                 CMainWindow::self().getBestWidgetForParent());
  progress.enableCancel(false);
  quint32 cnt = 0;
  while (query.next()) {
    PROGRESS(cnt++, ;);

    quint64 idItem = query.value(0).toULongLong();
    quint32 typeItem = query.value(1).toUInt();

    IGisItem* item = IGisItem::newGisItem(typeItem, idItem, db, nullptr);

    if (nullptr == item) {
      continue;
    }

    // get full size info text
    QString comment = item->getInfo(IGisItem::eFeatureShowName | IGisItem::eFeatureShowFullText);
    QDateTime date = item->getTimestamp();

    // replace comment with full size info text in items table
    QSqlQuery query2(db);
    query2.prepare("UPDATE items SET comment=:comment, date=:date WHERE id=:id");
    query2.bindValue(":comment", comment);
    query2.bindValue(":date", date);
    query2.bindValue(":id", idItem);
    if (!query2.exec()) {
      qWarning() << query2.lastQuery();
      qWarning() << query2.lastError();
    }

    delete item;
  }

  return true;
}
