#include "importer.h"
#include "jsonhelper.h"

#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/common.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/variant.h>

#include <boost/spirit/include/classic_core.hpp>

#include <algorithm>

namespace json = maps::json;
namespace revision = maps::wiki::revision;
namespace filters = revision::filters;

using namespace boost::spirit::classic;

namespace {

using ParserRule = rule<scanner<>>;

revision::DBID parseGlobalId(const std::string& jsonId)
{
    uint_parser<revision::DBID> idParser;
    revision::DBID objectId = 0;

    //Rule=<objectID>(':'<commitId>)?
    ParserRule globalIdRule =
        idParser[assign_a(objectId)] >>
        !(ch_p(':') >> idParser);

    return parse(jsonId.begin(), jsonId.end(), globalIdRule).full
        ? objectId : 0;
}

template <typename T>
T getChunk(T& cont, size_t maxSize)
{
    T res;
    if (cont.size() <= maxSize) {
        res.swap(cont);
    } else {
        while (maxSize--) {
            auto first = cont.begin();
            res.insert(*first);
            cont.erase(first);
        }
    }
    return res;
}

} // namespace

namespace maps::wiki::revisionapi {

class IdMapper : public BaseIdMapper {
public:
    IdMapper(revision::RevisionsGateway& gateway)
        : gateway_(gateway)
    {
    }

    revision::RevisionID acquireNewRevisionId() const override
    {
        return gateway_.acquireObjectId();
    }

    static std::unique_ptr<IdMapper> create(
        revision::RevisionsGateway& gateway,
        IdMode,
        const std::vector<std::string>& jsonIds, size_t idOffset);

private:
    revision::RevisionsGateway& gateway_;
};

class IdMapperPreserve: public IdMapper {
public:
    IdMapperPreserve(revision::RevisionsGateway& gateway, const size_t offset)
        : IdMapper(gateway)
        , offset_(offset)
        , maxId_(std::numeric_limits<revision::DBID>::max() - offset_)
    {
    }

    revision::RevisionID getRevisionId(const std::string& jsonId) const override;

private:
    const size_t offset_;
    const size_t maxId_;
};

class IdMapperStartFromJson: public IdMapper {
public:
    IdMapperStartFromJson(
        revision::RevisionsGateway& gateway,
        const std::vector<std::string>& jsonIds);

    revision::RevisionID getRevisionId(const std::string& jsonId) const override;
};

class IdMapperIgnoreJson: public IdMapper {
public:
    IdMapperIgnoreJson(
        revision::RevisionsGateway& gateway,
        const std::vector<std::string>& jsonIds);

    revision::RevisionID getRevisionId(const std::string& jsonId) const override;

private:
    std::unordered_map<std::string, revision::RevisionID> revisionIDs_;
};

std::unique_ptr<IdMapper> IdMapper::create(
    revision::RevisionsGateway& gateway,
    const IdMode idMode,
    const std::vector<std::string>& jsonIds, const size_t idOffset)
{
    switch (idMode) {
        case IdMode::PreserveId:
            return std::unique_ptr<IdMapper>(new IdMapperPreserve(gateway, idOffset));
        case IdMode::IgnoreJsonId:
            return std::unique_ptr<IdMapper>(new IdMapperIgnoreJson(gateway, jsonIds));
        case IdMode::StartFromJsonId:
            return std::unique_ptr<IdMapper>(new IdMapperStartFromJson(gateway, jsonIds));
    }
    return std::unique_ptr<IdMapper>();
}

revision::RevisionID IdMapperPreserve::getRevisionId(const std::string& jsonId) const
{
    revision::DBID objId = parseGlobalId(jsonId);
    ASSERT(objId);
    REQUIRE(objId <= maxId_, "Object id " << objId << " would overflow DBID after offsetting by " << offset_);
    return revision::RevisionID::createNewID(objId + offset_);
}

IdMapperIgnoreJson::IdMapperIgnoreJson(
    revision::RevisionsGateway& gateway,
    const std::vector<std::string>& jsonIds)
    : IdMapper(gateway)
{
    for (const auto& jsonId : jsonIds)
        revisionIDs_[jsonId] = gateway.acquireObjectId();
}

revision::RevisionID IdMapperIgnoreJson::getRevisionId(const std::string& jsonId) const
{
    auto revisionIDIt = revisionIDs_.find(jsonId);
    REQUIRE(revisionIDIt != revisionIDs_.end(),
        "Object '" + jsonId + "' referenced by relation without definition in current stream");
    return revisionIDIt->second;
}

IdMapperStartFromJson::IdMapperStartFromJson(
    revision::RevisionsGateway& gateway,
    const std::vector<std::string>& jsonIds)
    : IdMapper(gateway)
{
    // move sequence
    revision::DBID maxObjId = 0;
    for (const auto& id : jsonIds) {
        revision::DBID objId = parseGlobalId(id);
        maxObjId = std::max(maxObjId, objId);
    }

    if (maxObjId == 0)
        return;

    gateway.acquireObjectIds(maxObjId);
}

revision::RevisionID IdMapperStartFromJson::getRevisionId(const std::string& jsonId) const
{
    revision::DBID objId = parseGlobalId(jsonId);
    ASSERT(objId);
    return revision::RevisionID::createNewID(objId);
}

Importer::Importer(
    pgpool::Pool& pool,
    const VerboseLevel verboseLevel,
    const json::Value& objects, const IdMode idMode, const size_t idOffset,
    std::map<std::string, std::string>& jsonIdToError)
{
    auto writeTr = pool.masterWriteableTransaction();
    auto branch = revision::BranchManager(*writeTr).loadTrunk();
    REQUIRE(branch.isWritingAllowed(),
            "writing into trunk branch unallowed, state: " << branch.state());

    const auto jsonIds = objects.fields();

    revision::RevisionsGateway gateway(*writeTr);

    std::unique_ptr<IdMapper> idMapper(IdMapper::create(
            gateway, idMode, jsonIds, idOffset));

    addObjects(*idMapper, verboseLevel, objects, jsonIdToError);

    writeTr->commit();
}

Importer::Importer(
    const VerboseLevel verboseLevel,
    const json::Value& objects,
    const BaseIdMapper& idMapper,
    std::map<std::string, std::string>& jsonIdToError)
{
    addObjects(idMapper, verboseLevel, objects, jsonIdToError);
}

std::list<revision::DBID> Importer::import(
    pqxx::transaction_base& txn,
    const revision::UserID userId,
    const revision::Attributes& attributes,
    const size_t batchLoadingSize,
    PreparedData& preparedData)
{
    std::list<revision::DBID> commitIds;

    size_t lowBound = 0;

    while (lowBound < preparedData.size()) {
        size_t curBatchSize = batchLoadingSize
            ? std::min(preparedData.size() - lowBound, batchLoadingSize)
            : preparedData.size();

        revision::RevisionsGateway gateway(txn);
        commitIds.push_back(
            gateway.createCommit(
                preparedData.begin() + lowBound,
                preparedData.begin() + lowBound + curBatchSize,
                userId,
                attributes).id()
        );

        lowBound += curBatchSize;
    }

    return commitIds;
}

std::list<revision::DBID> Importer::import(
    pgpool::Pool& pool,
    const revision::UserID userId,
    const revision::Attributes& attributes,
    const size_t batchLoadingSize)
{
    if (objects_.empty()) {
        return {};
    }

    auto writeTr = pool.masterWriteableTransaction();

    auto objCommitIds = import(writeTr, userId, attributes, batchLoadingSize);

    writeTr->commit();

    return objCommitIds;
}

std::list<revision::DBID> Importer::import(
    pgpool3::TransactionHandle& writeTr,
    const revision::UserID userId,
    const revision::Attributes& attributes,
    const size_t batchLoadingSize)
{
    if (objects_.empty()) {
        return {};
    }

    auto objCommitIds = import(
        *writeTr, userId, attributes, batchLoadingSize, objects_);
    auto relCommitIds = import(
        *writeTr, userId, attributes, batchLoadingSize, relations_);
    objCommitIds.splice(objCommitIds.end(), relCommitIds);

    return objCommitIds;
}

void Importer::addObjects(
    const BaseIdMapper& idMapper,
    const VerboseLevel verboseLevel,
    const json::Value& objects,
    std::map<std::string, std::string>& jsonIdToError)
{
    for (const auto& jsonId : objects.fields()) {
        try {
            addObject(idMapper, jsonId, objects[jsonId]);
        } catch (const maps::Exception& ex) {
            jsonIdToError[jsonId] = "object '" + jsonId + "' contains error: " + ex.what();
            if (verboseLevel == VerboseLevel::Full) {
                std::cerr << "object '" << jsonId
                          << "' contains error: " << ex << std::endl;
            }
        }
    }
    if (!jsonIdToError.empty()) {
        throw DataError()
            << "exception occured while objects processing, errors: " << jsonIdToError.size();
    }
}

void Importer::addObject(
    const BaseIdMapper& idMapper,
    const std::string& jsonId,
    const json::Value& object)
{
    revision::ObjectRevision::Data data;

    if (object.hasField(JSON_FIELD_ATTRIBUTES)) {
        data.attributes =
            json2attributes(object[JSON_FIELD_ATTRIBUTES]);
    }
    if (object.hasField(JSON_FIELD_DESCRIPTION)) {
        data.description = object[JSON_FIELD_DESCRIPTION].as<std::string>();
    }
    const auto& geomField = object[JSON_FIELD_GEOMETRY];
    if (geomField.exists()) {
        try {
            auto geoGeometry = geolib3::readGeojson<geolib3::SimpleGeometryVariant>(geomField);
            auto mercatorGeometry = geolib3::convertGeodeticToMercator(geoGeometry);

            data.geometry = geolib3::WKB::toString(mercatorGeometry);
        } catch (maps::RuntimeError& ex) {
            ex << " for jsonId " << jsonId;
            throw ex;
        }
    }

    revision::RevisionID revisionId = idMapper.getRevisionId(jsonId);

    objects_.emplace_back(revisionId, std::move(data));

    if (object.hasField(JSON_FIELD_RELATIONS)) {
        addRelations(idMapper, revisionId.objectId(), object[JSON_FIELD_RELATIONS]);
    }
}

void Importer::addRelations(
    const BaseIdMapper& idMapper,
    const revision::DBID objectId,
    const json::Value& relations)
{
    for (auto relIt = relations.begin(); relIt != relations.end(); ++relIt) {
        revision::DBID jsonMasterId = 0;
        revision::DBID jsonSlaveId = 0;
        if (relIt->hasField(JSON_FIELD_MASTER)) {
            jsonMasterId = idMapper.getRevisionId((*relIt)[JSON_FIELD_MASTER].as<std::string>()).objectId();
        }
        if (relIt->hasField(JSON_FIELD_SLAVE)) {
            jsonSlaveId = idMapper.getRevisionId((*relIt)[JSON_FIELD_SLAVE].as<std::string>()).objectId();
        }
        REQUIRE((jsonMasterId != 0) != (jsonSlaveId != 0), "bad relation");

        revision::DBID masterId = 0;
        revision::DBID slaveId = 0;

        if (jsonMasterId) {
            masterId = jsonMasterId;
            slaveId = objectId;
        } else {
            masterId = objectId;
            slaveId = jsonSlaveId;
        }

        revision::ObjectRevision::Data data;

        if (relIt->hasField(JSON_FIELD_ATTRIBUTES)) {
            data.attributes =
                json2attributes((*relIt)[JSON_FIELD_ATTRIBUTES]);
        }
        data.relationData =
            revision::RelationData(masterId, slaveId);

        relations_.emplace_back(idMapper.acquireNewRevisionId(), std::move(data));
   }
}

} // namespace maps::wiki::revisionapi
