#include <maps/wikimap/mapspro/services/mrc/libs/object/include/revision_loader.h>

#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>

#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/attr_value.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/string_utils.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/serialization.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/log8/include/log8.h>

#include <memory>
#include <string>
#include <string_view>
#include <type_traits>
#include <vector>
#include <utility>

namespace maps::mrc::object {

namespace revision = wiki::revision;
namespace filters = wiki::revision::filters;

using AttrsWrap = wiki::common::AttrsWrap;

namespace {

enum class HasGeom { Undefined = -1, No = 0, Yes = 1 };

template<typename TGeom>
TGeom getGeometry(const revision::ObjectRevision& object)
{
    REQUIRE(
        object.data().geometry,
        "Object " << object.id() << " has no geometry"
    );

    std::istringstream in(*object.data().geometry);
    return geolib3::WKB::read<TGeom>(in);
}

const revision::RelationData& getRelation(const revision::ObjectRevision& object)
{
    REQUIRE(
        object.data().relationData,
        "Object " << object.id() << " is no relation"
    );

    return *object.data().relationData;
}

using GeomFilterOperation = revision::filters::GeomFilterExpr::Operation;

template<typename Geom> constexpr GeomFilterOperation geomFilterOperation = GeomFilterOperation::Intersects;

template<> constexpr GeomFilterOperation geomFilterOperation<geolib3::Point2> = GeomFilterOperation::IntersectsPoints;
template<> constexpr GeomFilterOperation geomFilterOperation<geolib3::Polyline2> = GeomFilterOperation::IntersectsLinestrings;

filters::ProxyFilterExpr createObjectFilter(
        std::string_view categoryId,
        GeomFilterOperation operation,
        const geolib3::BoundingBox& mercatorBox)
{
    const geolib3::Point2 lowerCorner = mercatorBox.lowerCorner();
    const geolib3::Point2 upperCorner = mercatorBox.upperCorner();

    return filters::Attr(std::string(categoryId)).defined()
        && filters::ObjRevAttr::isRegularObject()
        && filters::ObjRevAttr::isNotDeleted()
        && filters::Geom::intersects(
            operation,
            lowerCorner.x(), lowerCorner.y(),
            upperCorner.x(), upperCorner.y()
        );
}

filters::ProxyFilterExpr createObjectFilter(std::string_view categoryId, HasGeom hasGeom = HasGeom::Undefined)
{
    filters::ProxyFilterExpr filter = filters::Attr(std::string(categoryId)).defined()
        && filters::ObjRevAttr::isRegularObject()
        && filters::ObjRevAttr::isNotDeleted();

    if (hasGeom == HasGeom::Yes) {
        filter = std::move(filter) && filters::Geom::defined();
    } else if (hasGeom == HasGeom::No) {
        filter = std::move(filter) && not filters::Geom::defined();
    }
    return filter;
}

revision::ConstRange<revision::DBID> range(const TIds& ids)
{
    return { // forced bad cast
        reinterpret_cast<const revision::DBID*>(std::addressof(*ids.begin())),
        reinterpret_cast<const revision::DBID*>(std::addressof(*ids.end()))
    };
}

filters::ProxyFilterExpr createObjectFilter(
        const TIds& objectIds,
        HasGeom hasGeom = HasGeom::Undefined)
{
    auto base = filters::ObjRevAttr::objectId().in(range(objectIds))
        && filters::ObjRevAttr::isNotDeleted()
        && filters::ObjRevAttr::isRegularObject();

    if (hasGeom == HasGeom::Yes) {
        return std::move(base) && filters::Geom::defined();
    }

    if (hasGeom == HasGeom::No) {
        return std::move(base) && not filters::Geom::defined();
    }

    return std::move(base);
}

filters::ProxyFilterExpr createRelationFilter(const TIds& objectIds)
{
    return filters::ObjRevAttr::isRelation()
        && filters::ObjRevAttr::isNotDeleted()
        && (
            filters::ObjRevAttr::slaveObjectId().in(range(objectIds))
                || filters::ObjRevAttr::masterObjectId().in(range(objectIds))
        );
}

filters::ProxyFilterExpr createRelationFilterByMaster(const TIds& masterIds)
{
    return filters::ObjRevAttr::isRelation()
        && filters::ObjRevAttr::isNotDeleted()
        && filters::ObjRevAttr::masterObjectId().in(range(masterIds));
}

using ObjectIdToRelations = std::map<TId, std::vector<revision::ObjectRevision>>;

ObjectIdToRelations loadRelations(revision::Snapshot& snapshot, const TIds& ids)
{
    if (ids.empty()) {
        return {};
    }

    ObjectIdToRelations result;

    for (const auto id: ids) {
        result.emplace(id, std::vector<revision::ObjectRevision>());
    }

    for (const auto& object: snapshot.objectRevisionsByFilter(createRelationFilter(ids))) {
        const auto& relation = getRelation(object);

        const auto masterIt = result.find(relation.masterObjectId());
        if (masterIt != result.end()) {
            masterIt->second.push_back(object);
        }

        const auto slaveIt = result.find(relation.slaveObjectId());
        if (slaveIt != result.end()) {
            slaveIt->second.push_back(object);
        }
    }

    return result;
}

TIds collectIds(const revision::Revisions& revisions)
{
    TIds ids;

    for (const auto& revision: revisions) {
        ids.emplace_back(revision.id().objectId());
    }

    return ids;
}

bool isRelation(
        const AttrsWrap& attrs,
        std::string_view role,
        std::string_view master,
        std::string_view slave)
{
    return attrs["rel:role"] == role
        && attrs["rel:master"] == master
        && attrs["rel:slave"] == slave;
}

bool isConditionViaJunction(const AttrsWrap& attrs)
{
    return isRelation(attrs, "via", "cond", "rd_jc");
}

bool isConditionFromElement(const AttrsWrap& attrs) {
    return isRelation(attrs, "from", "cond", "rd_el");
};

bool isConditionToElement(const AttrsWrap& attrs) {
    return isRelation(attrs, "to", "cond", "rd_el");
};

bool isIncidence(const AttrsWrap& attrs) {
    return (attrs["rel:role"] == "start" || attrs["rel:role"] == "end")
        && attrs["rel:master"] == "rd_el"
        && attrs["rel:slave"] == "rd_jc";
}

bool isElementStart(const AttrsWrap& attrs)
{
    return isRelation(attrs, "start", "rd_el", "rd_jc");
}

bool isElementEnd(const AttrsWrap& attrs)
{
    return isRelation(attrs, "end", "rd_el", "rd_jc");
}

bool isElementControlledByTrafficLight(const AttrsWrap& attrs)
{
    return isRelation(attrs, "controlled", "cond_traffic_light", "rd_el");
}

bool isTrafficLightPlacedAtJunction(const AttrsWrap& attrs)
{
    return isRelation(attrs, "placed_at", "cond_traffic_light", "rd_jc");
}

bool isElementApproachedBySpeedBump(const AttrsWrap& attrs)
{
    return isRelation(attrs, "approach", "cond_speed_bump", "rd_el");
}

bool isSpeedBumpLocatedAtJunction(const AttrsWrap& attrs)
{
    return isRelation(attrs, "located_at", "cond_speed_bump", "rd_jc");
}

template<typename TObject>
constexpr std::string_view categoryId = "";

template<> constexpr std::string_view categoryId<RoadElement> = "cat:rd_el";
template<> constexpr std::string_view categoryId<RoadJunction> = "cat:rd_jc";
template<> constexpr std::string_view categoryId<Condition> = "cat:cond";
template<> constexpr std::string_view categoryId<ParkingLot> = "cat:urban_roadnet_parking_lot";
template<> constexpr std::string_view categoryId<LinearParkingLot> = "cat:urban_roadnet_parking_lot_linear";
template<> constexpr std::string_view categoryId<MrcRegion> = "cat:mrc_region";
template<> constexpr std::string_view categoryId<TrafficLight> = "cat:cond_traffic_light";
template<> constexpr std::string_view categoryId<SpeedBump> = "cat:cond_speed_bump";

template<typename TObject>
std::vector<TObject> construct(revision::Snapshot snapshot, const revision::Revisions& objects);

template<>
RoadElements construct<RoadElement>(
        revision::Snapshot snapshot,
        const revision::Revisions& objects)
{
    const auto objectIdToRelations = loadRelations(snapshot, collectIds(objects));

    RoadElements result;
    for (const auto& object: objects) {
        const auto attrs = AttrsWrap::extract(object);

        auto element = RoadElement(object.id(), getGeometry<geolib3::Polyline2>(object))
            .direction(attrs["rd_el:oneway"])
            .accessId(attrs["rd_el:access_id"])
            .fc(attrs["rd_el:fc"])
            .fow(attrs["rd_el:fow"])
            .startZLevel(attrs["rd_el:f_zlev"])
            .endZLevel(attrs["rd_el:t_zlev"])
            .speedLimit(attrs["rd_el:speed_limit"].as<int>(object::RoadElement::NO_SPEED_LIMIT))
            .speedLimitF(attrs["rd_el:speed_limit_f"].as<int>(object::RoadElement::NO_SPEED_LIMIT))
            .speedLimitT(attrs["rd_el:speed_limit_t"].as<int>(object::RoadElement::NO_SPEED_LIMIT))
            .speedLimitTruckF(attrs["rd_el:speed_limit_truck_f"].as<int>(object::RoadElement::NO_SPEED_LIMIT))
            .speedLimitTruckT(attrs["rd_el:speed_limit_truck_t"].as<int>(object::RoadElement::NO_SPEED_LIMIT))
            .backTransportLane(attrs["rd_el:back_bus"])
            .underConstruction(attrs["rd_el:srv_uc"])
            .fLanes(wiki::common::lanesFromString(attrs["rd_el:f_lane"]))
            .tLanes(wiki::common::lanesFromString(attrs["rd_el:t_lane"]));

        TIds trafficLightIds;
        TIds speedBumpIds;
        for (const auto& revision: objectIdToRelations.at(object.id().objectId())) {
            const auto& relation = getRelation(revision);
            const auto attrs = AttrsWrap::extract(revision);

            if (isElementStart(attrs)) {
                element.startJunctionId(relation.slaveObjectId());
            } else if (isElementEnd(attrs)) {
                element.endJunctionId(relation.slaveObjectId());
            } else if (isElementControlledByTrafficLight(attrs)) {
                trafficLightIds.push_back(relation.masterObjectId());
            } else if (isElementApproachedBySpeedBump(attrs)) {
                speedBumpIds.push_back(relation.masterObjectId());
            }
        }
        element.trafficLightIds(std::move(trafficLightIds));
        element.speedBumpIds(std::move(speedBumpIds));

        result.push_back(element);
    }

    return result;
}

template<>
RoadJunctions construct<RoadJunction>(
        revision::Snapshot snapshot,
        const revision::Revisions& objects)
{
    const auto objectIdToRelations = loadRelations(snapshot, collectIds(objects));

    RoadJunctions result;
    for (const auto& object: objects) {
        auto junction = RoadJunction(object.id(), getGeometry<geolib3::Point2>(object));

        TIds elementIds, conditionIds;
        for (const auto& revision: objectIdToRelations.at(object.id().objectId())) {
            const auto& relation = getRelation(revision);
            const auto attrs = AttrsWrap::extract(revision);

            if (isIncidence(attrs)) {
                elementIds.push_back(relation.masterObjectId());
            } else if (isConditionViaJunction(attrs)) {
                conditionIds.push_back(relation.masterObjectId());
            } else if (isTrafficLightPlacedAtJunction(attrs)) {
                REQUIRE(
                    junction.trafficLightId() == NO_OBJECT_ID,
                    "There can be only one traffic light placed at junction " << object.id()
                );
                junction.trafficLightId(relation.masterObjectId());
            } else if (isSpeedBumpLocatedAtJunction(attrs)) {
                REQUIRE(
                    junction.speedBumpId() == NO_OBJECT_ID,
                    "There can be only one speed bump located at junction " << object.id()
                );
                junction.speedBumpId(relation.masterObjectId());
            }
        }
        junction.elementIds(std::move(elementIds))
                .conditionIds(std::move(conditionIds));

        result.push_back(junction);
    }

    return result;
}

template<>
Conditions construct<Condition>(
        revision::Snapshot snapshot,
        const revision::Revisions& objects)
{
    const auto objectIdToRelations = loadRelations(snapshot, collectIds(objects));

    Conditions conditions;
    for (const auto& object: objects) {
        const auto attrs = AttrsWrap::extract(object);

        auto condition = Condition(object.id())
            .type(attrs["cond:cond_type"])
            .accessId(attrs["cond:access_id"]);

        std::map<size_t, TId> seqToElementId;

        for (const auto& revision: objectIdToRelations.at(object.id().objectId())) {
            const auto& relation = getRelation(revision);
            const auto attrs = AttrsWrap::extract(revision);

            if (isConditionFromElement(attrs)) {
                condition.fromElementId(relation.slaveObjectId());
            } else if (isConditionViaJunction(attrs)) {
                condition.viaJunctionId(relation.slaveObjectId());
            } else if (isConditionToElement(attrs)) {
                seqToElementId.emplace(
                    attrs["rel:seq_num"].as<size_t>(),
                    relation.slaveObjectId()
                );
            }
        }

        size_t seqNum = 0;

        TIds toElementIds;
        for (const auto [i, elementId]: seqToElementId) {
            REQUIRE(i == seqNum, "Invalid sequence number");
            ++seqNum;

            toElementIds.push_back(elementId);
        }
        condition.toElementIds(std::move(toElementIds));

        conditions.push_back(condition);
    }

    return conditions;
}

template<>
TrafficLights construct<TrafficLight>(
        revision::Snapshot snapshot,
        const revision::Revisions& objects)
{
    const auto objectIdToRelations = loadRelations(snapshot, collectIds(objects));

    TrafficLights trafficLights;
    for (const auto& object: objects) {
        TrafficLight trafficLight(object.id());

        TIds elementIds;
        for (const auto& revision: objectIdToRelations.at(object.id().objectId())) {
            const auto& relation = getRelation(revision);
            const auto& attrs = AttrsWrap::extract(revision);

            if (isTrafficLightPlacedAtJunction(attrs)) {
                trafficLight.junctionId(relation.slaveObjectId());
            } else if (isElementControlledByTrafficLight(attrs)) {
                elementIds.push_back(relation.slaveObjectId());
            }
        }
        trafficLight.elementIds(std::move(elementIds));

        trafficLights.push_back(trafficLight);
    }

    return trafficLights;
}

template<>
SpeedBumps construct<SpeedBump>(
        revision::Snapshot snapshot,
        const revision::Revisions& objects)
{
    const auto objectIdToRelations = loadRelations(snapshot, collectIds(objects));

    SpeedBumps speedBumps;
    for (const auto& object : objects) {
        SpeedBump speedBump(object.id());

        TIds elementIds;
        for (const auto& revision : objectIdToRelations.at(object.id().objectId())) {
            const auto& relation = getRelation(revision);
            const auto& attrs = AttrsWrap::extract(revision);

            if (isSpeedBumpLocatedAtJunction(attrs)) {
                speedBump.junctionId(relation.slaveObjectId());
            } else if (isElementApproachedBySpeedBump(attrs)) {
                elementIds.push_back(relation.slaveObjectId());
            }
        }
        speedBump.elementIds(std::move(elementIds));

        speedBumps.push_back(std::move(speedBump));
    }

    return speedBumps;
}

template<>
ParkingLots construct<ParkingLot>(
        revision::Snapshot /*snapshot*/,
        const revision::Revisions& objects)
{
    ParkingLots result;
    for (const auto& object: objects) {
        bool isToll = false;

        const revision::ObjectData &data = object.data();
        if (data.attributes) {
            isToll = data.attributes->contains("urban_roadnet_parking_lot:toll");
        }

        result.emplace_back(
            object.id(),
            getGeometry<geolib3::Point2>(object),
            isToll
        );
    }

    return result;
}

template<>
LinearParkingLots construct<LinearParkingLot>(
        revision::Snapshot /*snapshot*/,
        const revision::Revisions& objects)
{
    LinearParkingLots result;
    for (const auto& object: objects) {
        const auto attrs = AttrsWrap::extract(object);

        result.push_back(
            LinearParkingLot(object.id(), getGeometry<geolib3::Polyline2>(object))
                .type(attrs["urban_roadnet_parking_lot_linear:ft_type_id"])
        );
    }

    return result;
}


template<>
MrcRegions construct<MrcRegion>(
        revision::Snapshot /*snapshot*/,
        const revision::Revisions& objects)
{
    MrcRegions result;
    for (const auto& object: objects) {
        const auto attrs = AttrsWrap::extract(object);

        std::string typeStr = attrs["mrc_region:type"].as<std::string>();
        result.push_back(
            MrcRegion(object.id(), getGeometry<geolib3::Polygon2>(object))
                .type(enum_io::fromString<MrcRegion::Type>(typeStr))
        );
    }

    return result;
}


template<typename TObject, typename Geom = void>
constexpr HasGeom hasGeom = HasGeom::No;

template<typename TObject>
constexpr HasGeom hasGeom<TObject, std::void_t<typename TObject::Geom>> = HasGeom::Yes;

template<typename TObject>
std::vector<TObject> load(revision::Snapshot snapshot, const TIds& ids)
{
    if (ids.empty()) {
        return {};
    }

    const auto revisions = snapshot.objectRevisionsByFilter(
        createObjectFilter(ids, hasGeom<TObject>)
    );

    for (const auto& revision: revisions) {
        auto attrs = AttrsWrap::extract(revision);

        REQUIRE(
            attrs[categoryId<TObject>],
            "Object " << revision.id() << " has not attr " << categoryId<TObject>
        );
    }

    return construct<TObject>(snapshot, revisions);

}

template<typename TObject>
std::vector<TObject> load(revision::Snapshot snapshot, const geolib3::BoundingBox& mercatorBox)
{
    return construct<TObject>(
        snapshot,
        snapshot.objectRevisionsByFilter(
            createObjectFilter(
                categoryId<TObject>,
                geomFilterOperation<typename TObject::Geom>,
                mercatorBox
            )
        )
    );
}

template<typename TObject>
std::vector<TObject> load(revision::Snapshot snapshot)
{
    return construct<TObject>(
        snapshot,
        snapshot.objectRevisionsByFilter(
            createObjectFilter(categoryId<TObject>, hasGeom<TObject>)
        )
    );
}

template <typename T>
TIds extractIds(const std::map<TId, T> &map)
{
    TIds Ids;
    if (map.empty())
        return Ids;
    Ids.reserve(map.size());
    for (auto cit = map.cbegin(); cit != map.end(); cit++)
    {
        Ids.push_back(cit->first);
    }
    return Ids;
}

std::map<TId, maps::geolib3::Point2> loadAddressPoints(
    const wiki::revision::Snapshot &snapshot,
    const geolib3::BoundingBox& mercatorBox)
{
    static const std::string_view ADDRESS_CATEGORY_NAME = "cat:addr";

    const auto addrRevisions = snapshot.objectRevisionsByFilter(
        createObjectFilter(
            ADDRESS_CATEGORY_NAME,
            geomFilterOperation<geolib3::Point2>,
            mercatorBox
        )
    );

    std::map<TId, maps::geolib3::Point2> pts;
    for (auto cit = addrRevisions.begin(); cit != addrRevisions.end(); cit++)
    {
        pts[(TId)cit->id().objectId()] = getGeometry<geolib3::Point2>(*cit);
    }
    return pts;
}

std::map<TId, TId> loadLinkToAddressNames(
    const wiki::revision::Snapshot &snapshot,
    const std::vector<TId> &addrPointsIds)
{
    static const std::string ADDRESS_CATEGORY_NAME = "addr";
    static const std::string ADDRESS_NAME_CATEGORY_NAME = "addr_nm";
    static const std::string RELATION_MASTER_ATTR_NAME = "rel:master";
    static const std::string RELATION_SLAVE_ATTR_NAME = "rel:slave";

    std::map<TId, TId> nameIdToPtId;
    if (addrPointsIds.empty())
        return nameIdToPtId;
    maps::wiki::revision::Revisions relations =
        snapshot.objectRevisionsByFilter(createRelationFilterByMaster(addrPointsIds));
    for (maps::wiki::revision::Revisions::const_iterator cit = relations.begin();
        cit != relations.end(); cit++) {
        const maps::wiki::revision::ObjectData &data = cit->data();

        if (data.attributes && data.relationData)
        {
            std::map<std::string, std::string>::const_iterator citMaster =
                data.attributes->find(RELATION_MASTER_ATTR_NAME);
            if (citMaster == data.attributes->end() ||
                citMaster->second != ADDRESS_CATEGORY_NAME)
                continue;
            std::map<std::string, std::string>::const_iterator citSlave =
                data.attributes->find(RELATION_SLAVE_ATTR_NAME);
            if (citSlave == data.attributes->end() ||
                citSlave->second != ADDRESS_NAME_CATEGORY_NAME)
                continue;
            nameIdToPtId[data.relationData->slaveObjectId()] = data.relationData->masterObjectId();
        }
    }
    return nameIdToPtId;
}

AddressPointWithNames loadAddressNames(
    const wiki::revision::Snapshot &snapshot,
    const TIds &Ids,
    const std::map<TId, maps::geolib3::Point2> &pts,
    const std::map<TId, TId> &nameIdToPtId)
{
    static const std::string ADDRESS_NAME_NANE_ATTR_NAME = "addr_nm:name";
    if (Ids.empty() || pts.empty() || nameIdToPtId.empty())
        return {};
    auto nameRevisions = snapshot.objectRevisionsByFilter(createObjectFilter(Ids, HasGeom::No));

    AddressPointWithNames results;
    for (maps::wiki::revision::Revisions::const_iterator cit = nameRevisions.begin();
        cit != nameRevisions.end(); cit++)
    {
        const maps::wiki::revision::ObjectData &data = cit->data();
        if (!data.attributes)
            continue;
        std::map<std::string, std::string>::const_iterator citAttr = data.attributes->find(ADDRESS_NAME_NANE_ATTR_NAME);
        if (citAttr == data.attributes->end())
            continue;
        std::map<TId, TId>::const_iterator
            citPtID = nameIdToPtId.find(cit->id().objectId());
        if (citPtID == nameIdToPtId.end())
            continue;
        std::map<TId, maps::geolib3::Point2>::const_iterator
            citPt = pts.find(citPtID->second);
        if (citPt == pts.end())
            continue;
        results.emplace_back(AddressPointWithName(cit->id(), citPt->second, citAttr->second));
    }
    return results;
}

wiki::revision::Snapshot makeSnapshot(pqxx::transaction_base& txn, std::optional<std::uint64_t> optCommitId)
{
    wiki::revision::RevisionsGateway gateway{txn};
    const std::uint64_t commitId =
        optCommitId.has_value() ? optCommitId.value()
            :gateway.headCommitId();
    return gateway.snapshot(commitId);
}

} // namespace

class RevisionLoader: public Loader {
public:
    RevisionLoader(wiki::revision::Snapshot snapshot)
        : snapshot_(std::move(snapshot))
    {}

    RoadElements loadRoadElements(const TIds& ids) override
    {
        return load<RoadElement>(snapshot_, ids);
    }

    RoadElements loadRoadElements(const geolib3::BoundingBox& mercatorBox) override
    {
        return load<RoadElement>(snapshot_, mercatorBox);
    }

    RoadJunctions loadRoadJunctions(const TIds& ids) override
    {
        return load<RoadJunction>(snapshot_, ids);
    }

    RoadJunctions loadRoadJunctions(const geolib3::BoundingBox& mercatorBox) override
    {
        return load<RoadJunction>(snapshot_, mercatorBox);
    }

    ParkingLots loadParkingLots(const TIds& ids) override
    {
        return load<ParkingLot>(snapshot_, ids);
    }

    ParkingLots loadParkingLots(const geolib3::BoundingBox& mercatorBox) override
    {
        return load<ParkingLot>(snapshot_, mercatorBox);
    }

    MrcRegions loadMrcRegions(const TIds& ids) override
    {
        return load<MrcRegion>(snapshot_, ids);
    }

    MrcRegions loadMrcRegions(const geolib3::BoundingBox& mercatorBox) override
    {
        return load<MrcRegion>(snapshot_, mercatorBox);
    }

    MrcRegions loadAllMrcRegions() override
    {
        return load<MrcRegion>(snapshot_);
    }

    LinearParkingLots loadLinearParkingLots(const TIds& ids) override
    {
        return load<LinearParkingLot>(snapshot_, ids);
    }

    LinearParkingLots loadLinearParkingLots(const geolib3::BoundingBox& mercatorBox) override
    {
        return load<LinearParkingLot>(snapshot_, mercatorBox);
    }

    Conditions loadConditions(const TIds& ids) override
    {
        return load<Condition>(snapshot_, ids);
    }

    TrafficLights loadTrafficLights(const TIds& ids) override
    {
        return load<TrafficLight>(snapshot_, ids);
    }

    SpeedBumps loadSpeedBumps(const TIds& ids) override
    {
        return load<SpeedBump>(snapshot_, ids);
    }

    AddressPointWithNames loadAddressPointWithNames(const geolib3::BoundingBox& mercatorBox) override
    {
        std::map<TId, maps::geolib3::Point2> pts = loadAddressPoints(snapshot_, mercatorBox);
        if (pts.empty())
            return {};
        std::map<TId, TId> nameIdToPtId = loadLinkToAddressNames(snapshot_, extractIds(pts));
        return loadAddressNames(snapshot_, extractIds(nameIdToPtId), pts, nameIdToPtId);
    }

    bool hasBuildings(const geolib3::BoundingBox& mercatorBox) override
    {
        static const std::string_view BUILDING_CATEGORY_NAME = "cat:bld";
        const maps::wiki::revision::Revisions revisions =
            snapshot_.objectRevisionsByFilter(
                createObjectFilter(
                    BUILDING_CATEGORY_NAME,
                    GeomFilterOperation::Intersects,
                    mercatorBox
                )
            );
        return (0 < revisions.size());
    }
    Buildings loadBuildings(const geolib3::BoundingBox& mercatorBox) override
    {
        static const std::string_view BUILDING_CATEGORY_NAME = "cat:bld";
        const maps::wiki::revision::Revisions revisions =
            snapshot_.objectRevisionsByFilter(
                createObjectFilter(
                    BUILDING_CATEGORY_NAME,
                    GeomFilterOperation::Intersects,
                    mercatorBox
                )
            );
        Buildings result;
        for (auto cit = revisions.cbegin(); cit != revisions.cend(); cit++) {
            result.emplace_back(cit->id(), getGeometry<geolib3::Polygon2>(*cit));
        }
        return result;
    }

    Entrances loadEntrances(const geolib3::BoundingBox& mercatorBox) override
    {
        static const std::string_view ENTRANCE_CATEGORY_NAME = "cat:poi_entrance";
        const maps::wiki::revision::Revisions revisions =
            snapshot_.objectRevisionsByFilter(
                createObjectFilter(
                    ENTRANCE_CATEGORY_NAME,
                    GeomFilterOperation::Intersects,
                    mercatorBox
                )
            );
        Entrances result;
        for (auto cit = revisions.cbegin(); cit != revisions.cend(); cit++) {
            result.emplace_back(cit->id(), getGeometry<geolib3::Point2>(*cit));
        }
        return result;
    }

private:
    wiki::revision::Snapshot snapshot_;
};

LoaderHolder makeRevisionLoader(wiki::revision::Snapshot snapshot)
{
    return std::make_unique<RevisionLoader>(snapshot);
}

class RevisionLoaderWithTxn: public RevisionLoader {
public:
    RevisionLoaderWithTxn(pgpool3::TransactionHandle&& txn, std::optional<std::uint64_t> commitId)
        : RevisionLoader(makeSnapshot(*txn, commitId))
        , txn_(std::move(txn))
    {}

private:
    pgpool3::TransactionHandle txn_;
};

LoaderHolder makeRevisionLoader(pgpool3::TransactionHandle&& txn, std::optional<std::uint64_t> commitId)
{
    return std::make_unique<RevisionLoaderWithTxn>(std::move(txn), commitId);
}

} // namespace maps::mrc::object
