#include "sqlite_db.h"

namespace maps {
namespace mrc {
namespace common {
namespace {

struct BindVisitor : boost::static_visitor<int> {
    sqlite3_stmt* stmt;
    int order;

    int operator()(const None&)
    {
        return sqlite3_bind_null(stmt, order);
    }

    int operator()(const int64_t& val)
    {
        return sqlite3_bind_int64(stmt, order, val);
    }

    int operator()(const double& val)
    {
        return sqlite3_bind_double(stmt, order, val);
    }

    int operator()(const std::string& str)
    {
        return sqlite3_bind_text(stmt, order, str.data(), int(str.size()),
                                 SQLITE_TRANSIENT);
    }

    int operator()(const SQLiteBlob& blob)
    {
        return sqlite3_bind_blob(stmt, order, blob.data(), int(blob.size()),
                                 SQLITE_TRANSIENT);
    }
};

} // anonymous namespace

SQLiteDb::SQLiteDb(const std::string& path, int flags)
{
    sqlite3* db = nullptr;
    auto result = sqlite3_open_v2(path.c_str(), &db, flags, nullptr);
    db_.reset(db);
    check(result);
}

void SQLiteDb::exec(const std::string& sql,
                    const std::vector<SQLiteVariant>& params)
{
    sqlite3_stmt* stmt = nullptr;
    auto result = sqlite3_prepare_v2(db_.get(), sql.c_str(), (int)sql.size(),
                                     &stmt, 0);
    stmt_.reset(stmt);
    check(result);
    for (size_t i = 0; i < params.size(); ++i) {
        check(bind(i, params[i]));
    }
    step();
}

bool SQLiteDb::fetch(std::vector<SQLiteVariant>& row)
{
    if (!stmt_)
        return false;

    int cols = sqlite3_column_count(stmt_.get());
    row.resize(cols);
    for (int i = 0; i < cols; ++i) {
        switch (sqlite3_column_type(stmt_.get(), i)) {
        case SQLITE_INTEGER:
            row[i] = (int64_t)sqlite3_column_int64(stmt_.get(), i);
            break;
        case SQLITE_FLOAT:
            row[i] = sqlite3_column_double(stmt_.get(), i);
            break;
        case SQLITE_TEXT:
            {
                auto ptr = (const char*)sqlite3_column_text(stmt_.get(), i);
                auto size = (size_t)sqlite3_column_bytes(stmt_.get(), i);
                row[i] = std::string{ptr, size};
            }
            break;
        case SQLITE_BLOB:
            {
                auto begin = (SQLiteBlob::const_pointer)sqlite3_column_blob(
                    stmt_.get(), i);
                auto end = begin + sqlite3_column_bytes(stmt_.get(), i);
                row[i] = SQLiteBlob{begin, end};
            }
            break;
        default:
            row[i] = None{};
        }
    }
    step();
    return true;
}

std::string SQLiteDb::errmsg() const
{
    std::string msg;
    if (db_) {
        msg = sqlite3_errmsg(db_.get());
    }
    if (msg.empty()) {
        msg = "SQLite unknown error";
    }
    return msg;
}

void SQLiteDb::check(int result) const
{
    switch (result) {
    case SQLITE_OK:
        return;
    case SQLITE_BUSY:
    case SQLITE_LOCKED:
        throw SQLiteBusy{} << errmsg();
    default:
        throw SQLiteError{} << errmsg();
    }
}

void SQLiteDb::step()
{
    switch (sqlite3_step(stmt_.get())) {
    case SQLITE_DONE:
        stmt_.reset(nullptr);
        break;
    case SQLITE_ROW:
        break;
    case SQLITE_BUSY:
    case SQLITE_LOCKED:
        throw SQLiteBusy{} << errmsg();
    default:
        throw SQLiteError{} << errmsg();
    }
}

int SQLiteDb::bind(size_t order, const SQLiteVariant& param)
{
    BindVisitor visitor;
    visitor.stmt = stmt_.get();
    visitor.order = int(order + 1); // to one based
    return boost::apply_visitor(visitor, param);
}

} // common
} // mrc
} // maps
