#include "sqlite3.hpp"

#include <QSqlDatabase>
#include <QSqlError>
#include <QSqlQuery>
#include <QStringList>
#include <QThread>
#include <QVariant>
#include <QAtomicInt>

#include <cassert>
#include <cstring>
#include <cstdio>
#include <chrono>
#include <limits>

#include <mbgl/util/chrono.hpp>
#include <mbgl/util/logging.hpp>
#include <mbgl/util/optional.hpp>
#include <mbgl/util/string.hpp>
#include <mbgl/util/traits.hpp>

namespace mapbox {
namespace sqlite {

// https://www.sqlite.org/rescode.html#ok
static_assert(mbgl::underlying_type(ResultCode::OK) == 0, "error");
// https://www.sqlite.org/rescode.html#cantopen
static_assert(mbgl::underlying_type(ResultCode::CantOpen) == 14, "error");
// https://www.sqlite.org/rescode.html#notadb
static_assert(mbgl::underlying_type(ResultCode::NotADB) == 26, "error");

void checkQueryError(const QSqlQuery& query) {
    QSqlError lastError = query.lastError();
    if (lastError.type() != QSqlError::NoError) {
#if QT_VERSION >= 0x050300
        throw Exception { lastError.nativeErrorCode().toInt(), lastError.text().toStdString() };
#else
        throw Exception { lastError.number(), lastError.text().toStdString() };
#endif
    }
}

void checkDatabaseError(const QSqlDatabase &db) {
    QSqlError lastError = db.lastError();
    if (lastError.type() != QSqlError::NoError) {
#if QT_VERSION >= 0x050300
        throw Exception { lastError.nativeErrorCode().toInt(), lastError.text().toStdString() };
#else
        throw Exception { lastError.number(), lastError.text().toStdString() };
#endif
    }
}

void checkDatabaseOpenError(const QSqlDatabase &db) {
    // Assume every error when opening the data as CANTOPEN. Qt
    // always returns -1 for `nativeErrorCode()` on database errors.
    QSqlError lastError = db.lastError();
    if (lastError.type() != QSqlError::NoError) {
        throw Exception { ResultCode::CantOpen, "Error opening the database." };
    }
}

namespace {
    QString incrementCounter() {
        static QAtomicInt count = 0;
        return QString::number(count.fetchAndAddAcquire(1));
    }
}

class DatabaseImpl {
public:
    DatabaseImpl(const char* filename, int flags)
        : connectionName(QString::number(uint64_t(QThread::currentThread())) + incrementCounter())
    {
        if (!QSqlDatabase::drivers().contains("QSQLITE")) {
            throw Exception { ResultCode::CantOpen, "SQLite driver not found." };
        }

        assert(!QSqlDatabase::contains(connectionName));
        auto db = QSqlDatabase::addDatabase("QSQLITE", connectionName);

        QString connectOptions = db.connectOptions();
        if (flags & OpenFlag::ReadOnly) {
            if (!connectOptions.isEmpty()) connectOptions.append(';');
            connectOptions.append("QSQLITE_OPEN_READONLY");
        }
        if (flags & OpenFlag::SharedCache) {
            if (!connectOptions.isEmpty()) connectOptions.append(';');
            connectOptions.append("QSQLITE_ENABLE_SHARED_CACHE");
        }

        db.setConnectOptions(connectOptions);
        db.setDatabaseName(QString(filename));

        if (!db.open()) {
            checkDatabaseOpenError(db);
        }
    }

    ~DatabaseImpl() {
        auto db = QSqlDatabase::database(connectionName);
        db.close();
        checkDatabaseError(db);
    }

    QString connectionName;
};

class StatementImpl {
public:
    StatementImpl(const QString& sql, const QSqlDatabase& db) : query(db) {
        if (!query.prepare(sql)) {
            checkQueryError(query);
        }
    }

    ~StatementImpl() {
        query.clear();
    }

    QSqlQuery query;
    int64_t lastInsertRowId = 0;
    int64_t changes = 0;
};

template <typename T>
using optional = std::experimental::optional<T>;


Database::Database(const std::string& file, int flags)
        : impl(std::make_unique<DatabaseImpl>(file.c_str(), flags)) {
    assert(impl);
}

Database::Database(Database &&other)
        : impl(std::move(other.impl)) {
    assert(impl);
}

Database &Database::operator=(Database &&other) {
    std::swap(impl, other.impl);
    assert(impl);
    return *this;
}

Database::~Database() {
}

void Database::setBusyTimeout(std::chrono::milliseconds timeout) {
    assert(impl);

    // std::chrono::milliseconds.count() is a long and Qt will cast
    // internally to int, so we need to make sure the limits apply.
    std::string timeoutStr = mbgl::util::toString(timeout.count() & INT_MAX);

    auto db = QSqlDatabase::database(impl->connectionName);
    QString connectOptions = db.connectOptions();
    if (connectOptions.isEmpty()) {
        if (!connectOptions.isEmpty()) connectOptions.append(';');
        connectOptions.append("QSQLITE_BUSY_TIMEOUT=").append(QString::fromStdString(timeoutStr));
    }
    if (db.isOpen()) {
        db.close();
    }
    db.setConnectOptions(connectOptions);
    if (!db.open()) {
        checkDatabaseOpenError(db);
    }
}

void Database::exec(const std::string &sql) {
    assert(impl);
    QStringList statements = QString::fromStdString(sql).split(';', QString::SkipEmptyParts);
    statements.removeAll("\n");
    for (QString statement : statements) {
        if (!statement.endsWith(';')) {
            statement.append(';');
        }
        QSqlQuery query(QSqlDatabase::database(impl->connectionName));
        query.prepare(statement);

        if (!query.exec()) {
            checkQueryError(query);
        }
    }
}

Statement::Statement(Database& db, const char* sql)
    : impl(std::make_unique<StatementImpl>(QString(sql),
                                           QSqlDatabase::database(db.impl->connectionName))) {
    assert(impl);
}

Statement::~Statement() {
#ifndef NDEBUG
    // Crash if we're destructing this object while we know a Query object references this.
    assert(!used);
#endif
}

Query::Query(Statement& stmt_) : stmt(stmt_) {
    assert(stmt.impl);

#ifndef NDEBUG
    assert(!stmt.used);
    stmt.used = true;
#endif
}

Query::~Query() {
    reset();
    clearBindings();

#ifndef NDEBUG
    stmt.used = false;
#endif
}

template void Query::bind(int, int64_t);

template <typename T>
void Query::bind(int offset, T value) {
    assert(stmt.impl);
    // Field numbering starts at 0.
    stmt.impl->query.bindValue(offset - 1, QVariant::fromValue<T>(value), QSql::In);
    checkQueryError(stmt.impl->query);
}

template <>
void Query::bind(int offset, std::nullptr_t) {
    assert(stmt.impl);
    // Field numbering starts at 0.
    stmt.impl->query.bindValue(offset - 1, QVariant(QVariant::Invalid), QSql::In);
    checkQueryError(stmt.impl->query);
}

template <>
void Query::bind(int offset, int32_t value) {
    bind(offset, static_cast<int64_t>(value));
}

template <>
void Query::bind(int offset, bool value) {
    bind(offset, static_cast<int>(value));
}

template <>
void Query::bind(int offset, int8_t value) {
    bind(offset, static_cast<int64_t>(value));
}

template <>
void Query::bind(int offset, uint8_t value) {
    bind(offset, static_cast<int64_t>(value));
}

template <>
void Query::bind(int offset, mbgl::Timestamp value) {
    bind(offset, std::chrono::system_clock::to_time_t(value));
}

template <>
void Query::bind(int offset, optional<std::string> value) {
    if (value) {
        bind(offset, *value);
    } else {
        bind(offset, nullptr);
    }
}

template <>
void Query::bind(int offset, optional<mbgl::Timestamp> value) {
    if (value) {
        bind(offset, *value);
    } else {
        bind(offset, nullptr);
    }
}

void Query::bind(int offset, const char* value, std::size_t length, bool /* retain */) {
    assert(stmt.impl);
    if (length > std::numeric_limits<int>::max()) {
        // Kept for consistence with the default implementation.
        throw std::range_error("value too long");
    }

    // Field numbering starts at 0.
    stmt.impl->query.bindValue(offset - 1, QString(QByteArray(value, length)), QSql::In);

    checkQueryError(stmt.impl->query);
}

void Query::bind(int offset, const std::string& value, bool retain) {
    bind(offset, value.data(), value.size(), retain);
}

void Query::bindBlob(int offset, const void* value_, std::size_t length, bool retain) {
    assert(stmt.impl);
    const char* value = reinterpret_cast<const char*>(value_);
    if (length > std::numeric_limits<int>::max()) {
        // Kept for consistence with the default implementation.
        throw std::range_error("value too long");
    }

    // Field numbering starts at 0.
    stmt.impl->query.bindValue(offset - 1, retain ? QByteArray(value, length) :
            QByteArray::fromRawData(value, length), QSql::In | QSql::Binary);

    checkQueryError(stmt.impl->query);
}

void Query::bindBlob(int offset, const std::vector<uint8_t>& value, bool retain) {
    bindBlob(offset, value.data(), value.size(), retain);
}

bool Query::run() {
    assert(stmt.impl);

    if (!stmt.impl->query.isValid()) {
       if (stmt.impl->query.exec()) {
           stmt.impl->lastInsertRowId = stmt.impl->query.lastInsertId().value<int64_t>();
           stmt.impl->changes = stmt.impl->query.numRowsAffected();
       } else {
           checkQueryError(stmt.impl->query);
       }
    }

    const bool hasNext = stmt.impl->query.next();
    if (!hasNext) stmt.impl->query.finish();

    return hasNext;
}

template bool Query::get(int);
template int Query::get(int);
template int64_t Query::get(int);
template double Query::get(int);

template <typename T> T Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QVariant value = stmt.impl->query.value(offset);
    checkQueryError(stmt.impl->query);
    return value.value<T>();
}

template <> std::vector<uint8_t> Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QByteArray byteArray = stmt.impl->query.value(offset).toByteArray();
    checkQueryError(stmt.impl->query);
    std::vector<uint8_t> blob(byteArray.begin(), byteArray.end());
    return blob;
}

template <> mbgl::Timestamp Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QVariant value = stmt.impl->query.value(offset);
    checkQueryError(stmt.impl->query);
    return std::chrono::time_point_cast<std::chrono::seconds>(
        std::chrono::system_clock::from_time_t(value.value<::time_t>()));
}

template <> optional<int64_t> Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QVariant value = stmt.impl->query.value(offset);
    checkQueryError(stmt.impl->query);
    if (value.isNull())
        return {};
    return { value.value<int64_t>() };
}

template <> optional<double> Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QVariant value = stmt.impl->query.value(offset);
    checkQueryError(stmt.impl->query);
    if (value.isNull())
        return {};
    return { value.value<double>() };
}

template <> std::string Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QByteArray value = stmt.impl->query.value(offset).toByteArray();
    checkQueryError(stmt.impl->query);
    return std::string(value.constData(), value.size());
}

template <> optional<std::string> Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QByteArray value = stmt.impl->query.value(offset).toByteArray();
    checkQueryError(stmt.impl->query);
    if (value.isNull())
        return {};
    return { std::string(value.constData(), value.size()) };
}

template <> optional<mbgl::Timestamp> Query::get(int offset) {
    assert(stmt.impl && stmt.impl->query.isValid());
    QVariant value = stmt.impl->query.value(offset);
    checkQueryError(stmt.impl->query);
    if (value.isNull())
        return {};
    return { std::chrono::time_point_cast<mbgl::Seconds>(
        std::chrono::system_clock::from_time_t(value.value<::time_t>())) };
}

void Query::reset() {
    assert(stmt.impl);
    stmt.impl->query.finish();
}

void Query::clearBindings() {
    // no-op
}

int64_t Query::lastInsertRowId() const {
    assert(stmt.impl);
    return stmt.impl->lastInsertRowId;
}

uint64_t Query::changes() const {
    assert(stmt.impl);
    return (stmt.impl->changes < 0 ? 0 : stmt.impl->changes);
}

Transaction::Transaction(Database& db_, Mode mode)
        : db(db_) {
    switch (mode) {
    case Deferred:
        db.exec("BEGIN DEFERRED TRANSACTION");
        break;
    case Immediate:
        db.exec("BEGIN IMMEDIATE TRANSACTION");
        break;
    case Exclusive:
        db.exec("BEGIN EXCLUSIVE TRANSACTION");
        break;
    }
}

Transaction::~Transaction() {
    if (needRollback) {
        try {
            rollback();
        } catch (...) {
            // Ignore failed rollbacks in destructor.
        }
    }
}

void Transaction::commit() {
    needRollback = false;
    db.exec("COMMIT TRANSACTION");
}

void Transaction::rollback() {
    needRollback = false;
    db.exec("ROLLBACK TRANSACTION");
}

} // namespace sqlite
} // namespace mapbox
