#include "objectid_generator.h"
#include "sql_strings.h"
#include "helpers.h"

#include <sstream>

namespace maps::wiki::revision {

using namespace helpers;

namespace {

const size_t NEW_ID_BULK_SIZE = 10;

void
setLastId(pqxx::transaction_base& work, DBID id)
{
    std::stringstream ss;
    ss << "SELECT SETVAL('" << sql::seq::OBJECT_ID << "'," << id << ")";
    auto r = work.exec(ss.str());
    ASSERT(r.size() == 1);
    ASSERT(r[0][0].as<DBID>() == id);
}

pqxx::result
createNewIds(pqxx::transaction_base& work, uint64_t number)
{
    std::stringstream ss;
    ss << "SELECT NEXTVAL('" << sql::seq::OBJECT_ID << "') AS id "
            "FROM generate_series(1, " << number << ")";
    auto r = work.exec(ss.str());
    ASSERT(r.size() == number);
    return r;
}

} // namespace

DBID ObjectIdGenerator::lastObjectId(pqxx::transaction_base& work)
{
    auto r = work.exec("SELECT LAST_VALUE FROM " + sql::seq::OBJECT_ID);
    ASSERT(r.size() == 1);
    return r[0][0].as<DBID>();
}

void
ObjectIdGenerator::clear()
{
    std::lock_guard<std::mutex> lock(mutex_);
    freeIds_.clear();
}

DBID
ObjectIdGenerator::tryNewObjectId()
{
    std::lock_guard<std::mutex> lock(mutex_);
    if (freeIds_.empty()) {
        return 0;
    }

    DBID newId = freeIds_.back();
    freeIds_.pop_back();
    return newId;
}

DBID
ObjectIdGenerator::newObjectId(pqxx::transaction_base& work)
{
    DBID newId = tryNewObjectId();
    if (newId) {
        return newId;
    }

    auto r = createNewIds(work, NEW_ID_BULK_SIZE);
    newId = r[0][0].as<DBID>();

    std::lock_guard<std::mutex> lock(mutex_);
    freeIds_.reserve(NEW_ID_BULK_SIZE - 1);
    for (int i = (NEW_ID_BULK_SIZE - 1); i > 0; --i) {
        freeIds_.push_back(r[i][0].as<DBID>());
    }
    return newId;
}

std::list<ObjectIdRange>
ObjectIdGenerator::newObjectIds(
    pqxx::transaction_base& work, size_t number, bool optimize)
{
    std::list<ObjectIdRange> result;

    //Optimization works as follows:
    //Acquire single object id. If this id equals to 1
    //(which means that no object ids were acquited yet)
    //
    //Than we can just set set sequence value to number and use
    //continous range 1..number as acqured object ids
    //
    //WARN: this optimization IS NOT SAFE for concurrent usage
    //
    if (optimize && number > 1) {
        auto r = createNewIds(work, 1);
        auto id = r[0][0].as<DBID>();
        result.emplace_back(id, id);
        if (id == 1) {
            setLastId(work, number);
            result.back().second = number;
            return result;
        }
        --number;
    }

    auto r = createNewIds(work, number);

    for (size_t i = 0; i < number; ++i) {
        auto id = r[i][0].as<DBID>();
        if (!result.empty()) {
            ObjectIdRange& prev = result.back();
            if (prev.second + 1 == id) {
                prev.second = id;
                continue;
            }
        }
        result.emplace_back(id, id);
    }
    return result;
}

DBID ObjectIdGenerator::reserveObjectIds(pqxx::transaction_base& work, size_t number)
{
    std::stringstream ss;
    ss <<
        "WITH tmp AS (SELECT nextval('revision.object_id_seq') - 1 as max_id)"
        " SELECT max_id, setval('revision.object_id_seq', max_id + " << number << ")"
        " FROM tmp";

    auto r = work.exec(ss.str());
    ASSERT(r.size() == 1);
    return r[0][0].as<DBID>();
}

} // namespace maps::wiki::revision
