#include <maps/libs/log8/include/log8.h>
#include <maps/libs/cmdline/include/cmdline.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/static_geometry_searcher.h>
#include <maps/libs/geolib/include/units.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/json/include/std.h>
#include <maps/libs/pgpool/include/pgpool3.h>
#include <maps/libs/ymapsdf/include/rd.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/extended_xml_doc.h>
#include <maps/wikimap/mapspro/libs/common/include/yandex/maps/wiki/common/pgpool3_helpers.h>
#include <maps/wikimap/mapspro/libs/revision/include/yandex/maps/wiki/revision/revisionsgateway.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/url.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/location/include/rotation.h>
#include <maps/wikimap/mapspro/services/mrc/libs/config/include/config.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/frame_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/recognition_gateway.h>

using namespace maps::wiki::revision;
using namespace maps::mrc;

namespace {

template<typename TGeom>
TGeom getGeometry(const ObjectRevision& object)
{
    std::istringstream in(*object.data().geometry);
    return maps::geolib3::WKB::read<TGeom>(in);
}

std::list<DBID> extractIds(const Revisions& revs)
{
    std::list<DBID> ids;
    for (auto it = revs.begin(); it != revs.end(); it++) {
        ids.push_back(it->id().objectId());
    }
    return ids;
}

db::TIds extractObjectIds(const db::eye::ObjectLocations& locations)
{
    db::TIds objectIds;
    for (size_t i = 0; i < locations.size(); i++) {
        objectIds.emplace_back(locations[i].objectId());
    }
    return objectIds;
}

struct Edge {
    DBID edgeId;
    DBID edgeCommitId;
    DBID fromNodeId;
    DBID toNodeId;
    maps::geolib3::Polyline2 geom;
    int f_zlev = 0;
    int t_zlev = 0;
    int accessId; // maps/libs/ymapsdf/include/rd.h enum class AccessId
    int speedLimitF = 0;
    int speedLimitT = 0;
    int speedLimitTruckF = 0;
    int speedLimitTruckT = 0;
    int fc;
    int oneWay; //maps/libs/ymapsdf/include/rd.h enum class Direction ; Forward = fromNodeId -> toNodeId, Backward = toNodeId -> fromNodeId

    db::TIds trafficSignFIds; // ids объектов(знаков) относящихся к движению в Forward направлении по ребру
    db::TIds trafficSignTIds; // ids объектов(знаков) относящихся к движению в Backward направлении по ребру
};
using Edges = std::vector<Edge>;

struct Condition {
    DBID condId;
    DBID condCommitId;
    DBID fromEdgeId;
    DBID viaNodeId;
    std::map<size_t, DBID> toEdgeIds; // map from seq_num to edgeId
    int accessId; // maps/libs/ymapsdf/include/rd.h enum class AccessId
    int condType;
};
using Conditions = std::vector<Condition>;

bool validateCondition(const Condition& condition) {
    const size_t seqCount = condition.toEdgeIds.size();
    size_t maxSeqNum = 0;
    for (auto it = condition.toEdgeIds.begin(); it != condition.toEdgeIds.end(); it++) {
        if (maxSeqNum < it->first) {
            maxSeqNum = it->first;
        }
    }
    return ((seqCount - 1) == maxSeqNum);
}

std::vector<DBID> conditionToEdgeIdsAsVector(const Condition& condition) {
    std::vector<DBID> result(condition.toEdgeIds.size());
    for (auto it = condition.toEdgeIds.begin(); it != condition.toEdgeIds.end(); it++) {
        result[it->first] = it->second;
    }
    return result;
}

struct EdgeSegmentId {
    DBID edgeId;
    size_t segmentIdx; // индекс начальной точки сегмента
    bool forward; // направление ребра к которому привязался знак
};
struct TimeInterval {
    maps::chrono::TimePoint fromTime;
    maps::chrono::TimePoint toTime;
};
struct ImageFeature {
    ImageFeature(
        const db::TId _id,
        const std::string& _url,
        const cv::Rect& _bbox)
        : id(_id)
        , url(_url)
        , bbox(_bbox)
    {}

    db::TId id;
    std::string url;
    cv::Rect bbox;
};
using ImageFeatures = std::vector<ImageFeature>;

struct ObjectExtended {
    ObjectExtended(
        const db::eye::Object& _object,
        const db::eye::ObjectLocation& _location,
        const TimeInterval& _timeInterval,
        const ImageFeatures& _imageFeatures,
        const std::vector<traffic_signs::TrafficSign>& _slaveSignTypes)
        : object(_object)
        , location(_location)
        , timeInterval(_timeInterval)
        , imageFeatures(_imageFeatures)
        , slaveSignTypes(_slaveSignTypes)
    {}

    db::eye::Object object;
    db::eye::ObjectLocation location;
    TimeInterval timeInterval;
    ImageFeatures imageFeatures;
    std::optional<EdgeSegmentId> edgeSegmentId;
    std::vector<traffic_signs::TrafficSign> slaveSignTypes;
};
using ObjectExtendeds = std::vector<ObjectExtended>;

std::set<traffic_signs::TrafficSign> loadTrafficSignTypes(const std::string& filePath) {
    std::ifstream ifs(filePath);
    REQUIRE(ifs.is_open(), "Unable to open file with valid traffic signs types");
    std::set<traffic_signs::TrafficSign> result;
    for (; !ifs.eof();) {
        std::string line;
        std::getline(ifs, line);
        if (line.empty())
            continue;
        if ('#' == line[0])
            continue;
        result.insert(traffic_signs::stringToTrafficSign(line));
    }
    return result;
}

// load
Revisions loadJunctions(
    const Snapshot& snapshot,
    const maps::geolib3::Point2& pt1,
    const maps::geolib3::Point2& pt2)
{
    return snapshot.objectRevisionsByFilter(
        filters::Attr("cat:rd_jc").defined() &&
        filters::ObjRevAttr::isRegularObject() &&
        filters::ObjRevAttr::isNotDeleted() &&
        filters::Geom::intersects(
            filters::GeomFilterExpr::Operation::IntersectsPoints,
                pt1.x(), pt1.y(),
                pt2.x(), pt2.y()
        )
    );
}

Revisions loadJunctionRelations(
    const Snapshot& snapshot,
    const std::list<DBID>& rdJunctionIds)
{
    return snapshot.objectRevisionsByFilter(
        filters::ObjRevAttr::isRelation() &&
        filters::ObjRevAttr::isNotDeleted() &&
        (
            filters::ObjRevAttr::slaveObjectId().in({ rdJunctionIds.begin(), rdJunctionIds.end() })
        )
    );
}

// only edge with vehicle access_id
Edges loadEdges(
    const Snapshot& snapshot,
    const Revisions& junctionRelations)
{
    Edges edges;
    std::list<DBID> rdEdgeIds;
    std::map<DBID, std::pair<DBID, DBID>> mapEdgeIdToStartEndJcIds;
    for (auto it = junctionRelations.begin();it != junctionRelations.end(); it++) {
        const ObjectData &data = it->data();
        if (!data.attributes || !data.relationData)
            continue;
        auto citMaster = data.attributes->find("rel:master");
        if (citMaster == data.attributes->end() || citMaster->second != "rd_el")
            continue;
        auto citSlave = data.attributes->find("rel:slave");
        if (citSlave == data.attributes->end() || citSlave->second != "rd_jc")
            continue;
        rdEdgeIds.push_back(data.relationData->masterObjectId());

        auto citRole = data.attributes->find("rel:role");
        if (citRole == data.attributes->end())
            continue;
        if (citRole->second == "start") {
            mapEdgeIdToStartEndJcIds[data.relationData->masterObjectId()].first = data.relationData->slaveObjectId();
        } else if (citRole->second == "end") {
            mapEdgeIdToStartEndJcIds[data.relationData->masterObjectId()].second = data.relationData->slaveObjectId();
        }
    }

    Revisions rdEdges = snapshot.objectRevisionsByFilter(
        filters::ObjRevAttr::isRegularObject() &&
        filters::ObjRevAttr::isNotDeleted() &&
        filters::ObjRevAttr::objectId().in({ rdEdgeIds.begin(), rdEdgeIds.end() })
    );
    for (auto it = rdEdges.begin();it != rdEdges.end(); it++) {
        const ObjectData &data = it->data();
        if (!data.attributes)
            continue;
        Edge edge;
        //access_id
        {
            auto cit = data.attributes->find("rd_el:access_id");
            if (cit == data.attributes->end()) {
                INFO() << "rd_el:access_id not found";
                continue;
            }
            edge.accessId = std::stoi(cit->second);
            if (0 == (edge.accessId & (int)maps::ymapsdf::rd::AccessId::Vehicles)) {
                continue;
            }
        }
        //oneway
        {
            auto citOneWay = data.attributes->find("rd_el:oneway");
            if (citOneWay == data.attributes->end()) {
                INFO() << "rd_el:oneway not found";
                continue;
            }
            if (citOneWay->second == "F") {
                edge.oneWay = (int)maps::ymapsdf::rd::Direction::Forward;
            } else if (citOneWay->second == "T") {
                edge.oneWay = (int)maps::ymapsdf::rd::Direction::Backward;
            } else if (citOneWay->second == "B") {
                edge.oneWay = (int)maps::ymapsdf::rd::Direction::Both;
            } else {
                REQUIRE(false, "Unsupported oneway value");
            }
        }
        //speed_limit
        {
            int slValid = (edge.oneWay == (int)maps::ymapsdf::rd::Direction::Both) ? 2 : 1;
            if (0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Forward)) {
                auto citSpeedLimitF = data.attributes->find("rd_el:speed_limit_f");
                if (citSpeedLimitF != data.attributes->end()) {
                    edge.speedLimitF = std::stoi(citSpeedLimitF->second);
                    slValid--;
                }
                auto citSpeedLimitTruckF = data.attributes->find("rd_el:speed_limit_truck_f");
                if (citSpeedLimitTruckF != data.attributes->end()) {
                    edge.speedLimitTruckF = std::stoi(citSpeedLimitTruckF->second);
                }
            }
            if (0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Backward)) {
                auto citSpeedLimitT = data.attributes->find("rd_el:speed_limit_t");
                if (citSpeedLimitT != data.attributes->end()) {
                    edge.speedLimitT = std::stoi(citSpeedLimitT->second);
                    slValid--;
                }
                auto citSpeedLimitTruckT = data.attributes->find("rd_el:speed_limit_truck_t");
                if (citSpeedLimitTruckT != data.attributes->end()) {
                    edge.speedLimitTruckT = std::stoi(citSpeedLimitTruckT->second);
                }
            }

            auto citSpeedLimit = data.attributes->find("rd_el:speed_limit");
            const int speedLimit = (citSpeedLimit == data.attributes->end()) ? -1 : std::stoi(citSpeedLimit->second);
            if (0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Forward)) {
                if (0 == edge.speedLimitF) {
                    edge.speedLimitF = speedLimit;
                }
                if (0 == edge.speedLimitTruckF) {
                    edge.speedLimitTruckF = edge.speedLimitF;
                }
            }
            if (0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Backward)) {
                if (0 == edge.speedLimitT) {
                    edge.speedLimitT = speedLimit;
                }
                if (0 == edge.speedLimitTruckT) {
                    edge.speedLimitTruckT = edge.speedLimitT;
                }
            }
        }
        //fc
        {
            auto cit = data.attributes->find("rd_el:fc");
            if (cit == data.attributes->end()) {
                INFO() << "rd_el:fc not found";
                continue;
            }
            edge.fc = std::stoi(cit->second);
        }
        // f_zlev
        {
            auto cit = data.attributes->find("rd_el:f_zlev");
            if (cit != data.attributes->end()) {
                edge.f_zlev = std::stoi(cit->second);
            }
        }
        // t_zlev
        {
            auto cit = data.attributes->find("rd_el:t_zlev");
            if (cit != data.attributes->end()) {
                edge.t_zlev = std::stoi(cit->second);
            }
        }
        //base properties
        edge.edgeId = it->id().objectId();
        edge.edgeCommitId = it->id().commitId();
        edge.fromNodeId = mapEdgeIdToStartEndJcIds.at(edge.edgeId).first;
        edge.toNodeId = mapEdgeIdToStartEndJcIds.at(edge.edgeId).second;
        edge.geom = getGeometry<maps::geolib3::Polyline2>(*it);
        if ((0 < edge.fromNodeId) && (0 < edge.toNodeId)) {
            edges.emplace_back(edge);
        }
    }
    return edges;
}

db::eye::ObjectLocations loadLocations(
    pqxx::transaction_base& txn,
    const maps::geolib3::BoundingBox& box) {
    return db::eye::ObjectLocationGateway(txn).load(
         db::eye::table::ObjectLocation::position.intersects(box)
    );
}

Conditions loadConditions(
    const Snapshot& snapshot,
    const Revisions& junctionRelations)
{
    std::vector<DBID> rdConditionIds;
    {
        std::set<DBID> temp;
        for (auto it = junctionRelations.begin();it != junctionRelations.end(); it++) {
            const ObjectData &data = it->data();
            if (!data.attributes || !data.relationData)
                continue;
            auto citMaster = data.attributes->find("rel:master");
            if (citMaster == data.attributes->end() || citMaster->second != "cond")
                continue;
            temp.insert(data.relationData->masterObjectId());
        }
        rdConditionIds = { temp.begin(), temp.end() };
    }
    if (0 == rdConditionIds.size()) {
        return{};
    }

    std::map<DBID, Condition> mapConditions;
    maps::wiki::revision::Revisions conds = snapshot.objectRevisionsByFilter(
        filters::ObjRevAttr::isRegularObject() &&
        filters::ObjRevAttr::isNotDeleted() &&
        filters::ObjRevAttr::objectId().in(rdConditionIds)
    );
    for (auto it = conds.begin();it != conds.end(); it++) {
        const ObjectData &data = it->data();
        if (!data.attributes)
            continue;
        Condition cond;
        cond.condId = it->id().objectId();
        cond.condCommitId = it->id().commitId();
        //access_id
        {
            auto cit = data.attributes->find("cond:access_id");
            if (cit == data.attributes->end()) {
                INFO() << "cond:access_id not found";
                continue;
            }
            cond.accessId = std::stoi(cit->second);
            if (0 == (cond.accessId & (int)maps::ymapsdf::rd::AccessId::Vehicles)) {
                continue;
            }
        }
        //cond_type
        {
            auto cit = data.attributes->find("cond:cond_type");
            if (cit == data.attributes->end()) {
                INFO() << "cond:cond_type not found";
                continue;
            }
            cond.condType = std::stoi(cit->second);
        }
        mapConditions.emplace(cond.condId, cond);
    }

    maps::wiki::revision::Revisions relations = snapshot.objectRevisionsByFilter(
        filters::ObjRevAttr::isRelation() &&
        filters::ObjRevAttr::isNotDeleted() &&
        (
            filters::ObjRevAttr::slaveObjectId().in(rdConditionIds)
            || filters::ObjRevAttr::masterObjectId().in(rdConditionIds)
        )
    );
    for (auto it = relations.begin();it != relations.end(); it++) {
        const ObjectData &data = it->data();
        if (!data.attributes || !data.relationData)
            continue;
        auto citMaster = data.attributes->find("rel:master");
        if (citMaster == data.attributes->end() || citMaster->second != "cond")
            continue;
        DBID condId = data.relationData->masterObjectId();
        auto itCond = mapConditions.find(condId);
        if (itCond == mapConditions.end()) {
            INFO() << "Unable to find condition with id: " << condId;
            continue;
        }
        Condition& cond = itCond->second;

        DBID slaveId = data.relationData->slaveObjectId();
        //
        std::string slaveType = "";
        {
            auto cit = data.attributes->find("rel:slave");
            if (cit == data.attributes->end()) {
                INFO() << "rel:slave in condition not found";
                continue;
            }
            slaveType = cit->second;
        }
        std::string role = "";
        {
            auto cit = data.attributes->find("rel:role");
            if (cit == data.attributes->end()) {
                INFO() << "rel:role in condition not found";
                continue;
            }
            role = cit->second;
        }
        if ("rd_el" == slaveType) {
            if ("from" == role) {
                cond.fromEdgeId = slaveId;
            } else if ("to" == role) {
                auto cit = data.attributes->find("rel:seq_num");
                if (cit == data.attributes->end()) {
                    INFO() << "rel:seq_num in condition not found";
                    continue;
                }
                cond.toEdgeIds[std::stoi(cit->second)] = slaveId;
            } else {
                INFO() << "Unknown rel:role = " << role << " in condition with slave = rd_el";
            }
        } else if ("rd_jc" == slaveType) {
            if ("via" != role) {
                INFO() << "Unknown rel:role = " << role << " in condition with slave = rd_jc";
                continue;
            }
            cond.viaNodeId = slaveId;
        } else {
            continue;
        }
    }

    Conditions conditions;
    for (auto it = mapConditions.begin(); it != mapConditions.end(); it++) {
        if (validateCondition(it->second)) {
            conditions.emplace_back(it->second);
        } else {
            INFO() << "Invalid sequence in condition with id: " << it->first;
        }
    }
    return conditions;
}

db::eye::Objects loadTrafficSignObjects(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds,
    const std::set<traffic_signs::TrafficSign>& validTrafficSigns,
    bool removeTemporary)
{
    db::eye::Objects objects = db::eye::ObjectGateway(txn).load(
        db::eye::table::Object::id.in(objectIds) &&
        db::eye::table::Object::type == db::eye::ObjectType::Sign &&
        !db::eye::table::Object::deleted
    );
    objects.erase(
        std::remove_if(objects.begin(), objects.end(), [&](db::eye::Object& object) {
            db::eye::SignAttrs attr = object.attrs<db::eye::SignAttrs>();
            if (removeTemporary && attr.temporary) {
                return true;
            }
            return (validTrafficSigns.count(attr.type) == 0);
        }),
        objects.end()
    );
    return objects;
}

std::map<db::TId, traffic_signs::TrafficSign> loadSlaveTrafficSignTypes(
    pqxx::transaction_base& txn,
    const db::eye::ObjectRelations& relations)
{
    db::TIds slaveIds(relations.size());
    for (size_t i = 0; i < relations.size(); i++) {
        slaveIds[i] = relations[i].slaveObjectId();
    }

    db::eye::Objects slaves = db::eye::ObjectGateway(txn).load(
        db::eye::table::Object::id.in(slaveIds));
    std::map<db::TId, traffic_signs::TrafficSign> slaveTypeById;
    for (size_t i = 0; i < slaves.size(); i++) {
        const db::eye::Object& slave = slaves[i];
        slaveTypeById[slave.id()] = slave.attrs<db::eye::SignAttrs>().type;
    }
    return slaveTypeById;
}

std::map<db::TId, std::vector<traffic_signs::TrafficSign>> loadTrafficSignSlaves(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    db::eye::ObjectRelations relations = db::eye::ObjectRelationGateway(txn).load(
        db::eye::table::ObjectRelation::masterObjectId.in(objectIds) &&
        !db::eye::table::ObjectRelation::deleted
    );

    if (0 == relations.size()) {
        return {};
    }

    std::map<db::TId, traffic_signs::TrafficSign> slaveTypeById =
        loadSlaveTrafficSignTypes(txn, relations);

    std::map<db::TId, std::vector<traffic_signs::TrafficSign>> result;
    for (size_t i = 0; i < relations.size(); i++) {
        const db::eye::ObjectRelation& relation = relations[i];
        result[relation.masterObjectId()].emplace_back(slaveTypeById[relation.slaveObjectId()]);
    }
    return result;
}

std::map<db::TId, std::pair<TimeInterval, ImageFeatures>> loadObjectsFeaturesProperties(
    pqxx::transaction_base& txn,
    const db::eye::Objects& objects,
    const eye::FrameUrlResolver& frameUrlResolver)
{
    std::map<db::TId, db::TIds> detectionIdsByObjectId;
    std::map<db::TId, db::TId> objectIdByPrimaryDetectionId;
    db::TIds detectionIds;
    for (size_t i = 0; i < objects.size(); i++) {
        const db::eye::Object& object = objects[i];
        detectionIds.push_back(object.primaryDetectionId());
        detectionIdsByObjectId[object.id()].push_back(object.primaryDetectionId());
        objectIdByPrimaryDetectionId[object.primaryDetectionId()] = object.id();
    }
    db::eye::PrimaryDetectionRelations pdrelations = db::eye::PrimaryDetectionRelationGateway(txn).load(
        db::eye::table::PrimaryDetectionRelation::primaryDetectionId.in(detectionIds) &&
        ! db::eye::table::PrimaryDetectionRelation::deleted
    );
    for (size_t i = 0; i < pdrelations.size(); i++) {
        const db::eye::PrimaryDetectionRelation& pdrelation = pdrelations[i];
        detectionIds.push_back(pdrelation.detectionId());
        detectionIdsByObjectId[objectIdByPrimaryDetectionId[pdrelation.primaryDetectionId()]].push_back(pdrelation.detectionId());
    }
    db::eye::Detections detections = db::eye::DetectionGateway(txn).load(
        db::eye::table::Detection::id.in(detectionIds) &&
        ! db::eye::table::Detection::deleted
    );
    std::set<db::TId> groupIds;
    std::map<db::TId, db::TId> groupIdByDetectionId;
    std::map<db::TId, cv::Rect> bboxByDetectionId;
    for (size_t i = 0; i < detections.size(); i++) {
        const db::eye::Detection& detection = detections[i];
        groupIds.insert(detection.groupId());
        groupIdByDetectionId[detection.id()] = detection.groupId();
        bboxByDetectionId[detection.id()] = detection.box();
    }
    db::eye::DetectionGroups groups = db::eye::DetectionGroupGateway(txn).load(
        db::eye::table::DetectionGroup::id.in({ groupIds.begin(), groupIds.end()})
    );
    db::TIds frameIds;
    std::map<db::TId, db::TId> frameIdByGroupId;
    for (size_t i = 0; i < groups.size(); i++) {
        const db::eye::DetectionGroup& group = groups[i];
        frameIds.push_back(group.frameId());
        frameIdByGroupId[group.id()] = group.frameId();
    }
    db::eye::Frames frames = db::eye::FrameGateway(txn).load(
        db::eye::table::Frame::id.in(frameIds)
    );
    std::map<db::TId, db::eye::Frame> frameById;
    for (size_t i = 0; i < frames.size(); i++) {
        const db::eye::Frame& frame = frames[i];
        frameById.emplace(frame.id(), frame);
    }

    std::map<db::TId, std::pair<TimeInterval, ImageFeatures>> result;
    for (size_t i = 0; i < objects.size(); i++) {
        const db::eye::Object& object = objects[i];
        const db::TIds& objDetectionIds = detectionIdsByObjectId[object.id()];

        maps::chrono::TimePoint time = frameById.at(frameIdByGroupId[groupIdByDetectionId[objDetectionIds[0]]]).time();

        TimeInterval timeInterval = {time, time};
        ImageFeatures features;

        for (size_t j = 0; j < objDetectionIds.size(); j++) {
            const db::TId detId = objDetectionIds[j];
            const db::eye::Frame& frame = frameById.at(frameIdByGroupId[groupIdByDetectionId[detId]]);
            time = frame.time();
            timeInterval.fromTime = std::min(time, timeInterval.fromTime);
            timeInterval.toTime = std::max(time, timeInterval.toTime);
            features.emplace_back(
                frame.id(),
                frameUrlResolver.image(frame, db::FeaturePrivacy::Public),
                common::transformByImageOrientation(bboxByDetectionId[detId], frame.originalSize(), frame.orientation()));
        }
        result.emplace(object.id(), std::pair<TimeInterval, ImageFeatures>(timeInterval, std::move(features)));
    }
    return result;
 }

ObjectExtendeds mergeObjectsAndLocations(
    const db::eye::Objects& objects,
    const db::eye::ObjectLocations& locations,
    const std::map<db::TId, std::pair<TimeInterval, ImageFeatures>>& objectProperiesById,
    const std::map<db::TId, std::vector<traffic_signs::TrafficSign>> slaveTrafficSignTypesById)
{
    const std::vector<traffic_signs::TrafficSign> EMPTY_TRAFFIC_SIGN_VECTOR;
    std::map<db::TId, size_t> mapObjectIdToLocationIndex;
    for (size_t i = 0; i < locations.size(); i++) {
        mapObjectIdToLocationIndex[locations[i].objectId()] = i;
    }
    ObjectExtendeds result;
    for (size_t i = 0; i < objects.size(); i++) {
        auto itLocationIdx = mapObjectIdToLocationIndex.find(objects[i].id());
        REQUIRE(itLocationIdx != mapObjectIdToLocationIndex.end(),
            "Unable to find location for object id:" << objects[i].id());
        const std::pair<TimeInterval, ImageFeatures> prop = objectProperiesById.at(objects[i].id());
        auto itSlave = slaveTrafficSignTypesById.find(objects[i].id());

        result.emplace_back(
            objects[i],
            locations[itLocationIdx->second],
            prop.first,
            prop.second,
            (itSlave != slaveTrafficSignTypesById.end() ? itSlave->second : EMPTY_TRAFFIC_SIGN_VECTOR)
        );
    }
    return result;
}

ObjectExtendeds loadObjectsExtended(
    maps::pgpool3::Pool& pool,
    const maps::geolib3::BoundingBox& box,
    const std::set< traffic_signs::TrafficSign>& validTrafficSignTypes,
    const eye::FrameUrlResolver& frameUrlResolver)
{
    maps::pgpool3::TransactionHandle txn = pool.slaveTransaction();
    db::eye::ObjectLocations locations = loadLocations(*txn, box);
    db::TIds objectIds = extractObjectIds(locations);
    db::eye::Objects objects =
        loadTrafficSignObjects(
            *txn,
            objectIds,
            validTrafficSignTypes,
            true);
    std::map<db::TId, std::vector<traffic_signs::TrafficSign>> trafficSignTypeById =
        loadTrafficSignSlaves(*txn, objectIds);

    std::map<db::TId, std::pair<TimeInterval, ImageFeatures>> objectProperiesById =
        loadObjectsFeaturesProperties(*txn, objects, frameUrlResolver);
    return mergeObjectsAndLocations(objects, locations, objectProperiesById, trafficSignTypeById);
 }

void removeJunctionsWOEdges(
    Revisions& rdJunctions,
    const Edges& edges)
{
    std::set<DBID> validJunctionIds;
    for (size_t i = 0; i < edges.size(); i++) {
        const Edge& edge = edges[i];
        validJunctionIds.insert(edge.fromNodeId);
        validJunctionIds.insert(edge.toNodeId);
    }
    rdJunctions.remove_if(
        [&](ObjectRevision& object) {
            return (validJunctionIds.count(object.id().objectId()) == 0);
        }
    );
}

// match traffic signs to edges
class EdgeSearcher {
public:
    typedef maps::geolib3::StaticGeometrySearcher<maps::geolib3::Polyline2, size_t> InternalSearcher;
public:
    explicit EdgeSearcher(const Edges& edges) {
        for (size_t i = 0; i < edges.size(); i++) {
            const Edge& edge = edges[i];
            searcher_.insert(&edge.geom, i);
        }
        searcher_.build();
    }

    const InternalSearcher::SearchResult find(const maps::geolib3::Point2& mercatorPos) const {
        constexpr double SEARCH_RADIUS_METERS = 25.;
        const double mercatorDiameter = 2. * maps::geolib3::toMercatorUnits(SEARCH_RADIUS_METERS, mercatorPos);
        maps::geolib3::BoundingBox searchBox(mercatorPos, mercatorDiameter, mercatorDiameter);
        return searcher_.find(searchBox);
    }
private:
    InternalSearcher searcher_;
};

double getHeading(const Eigen::Quaterniond& rotation) {
    return eye::decomposeRotation(rotation).heading.value();
}

double getEdgeSegmentDirection(
    const maps::geolib3::Point2& from,
    const maps::geolib3::Point2& to)
{
    const double dx = to.x() - from.x();
    const double dy = to.y() - from.y();
    return atan2(dy, dx);
}

double angleDiff(double a, double b)
{
    const double diff = std::fmod(abs(a-b), 2. * M_PI);
    return std::min(diff, 2 * M_PI - diff);
}

 // проектируется ли точка внутрь отрезка?
bool ptInSegment(
    const maps::geolib3::Point2& objPoint,
    const maps::geolib3::Point2& from,
    const maps::geolib3::Point2& to)
{
    constexpr double EPS = 0.01;

    const double dxOF = objPoint.x() - from.x();
    const double dyOF = objPoint.y() - from.y();
    const double dxTF = to.x() - from.x();
    const double dyTF = to.y() - from.y();
    if (dxOF * dxTF + dyOF * dyTF < -EPS)
        return false;

    const double dxOT = objPoint.x() - to.x();
    const double dyOT = objPoint.y() - to.y();
    const double dxFT = from.x() - to.x();
    const double dyFT = from.y() - to.y();

    return (dxOT * dxFT + dyOT * dyFT > -EPS);
}

void linkTrafficSignsToEdges(
    Edges& edges,
    ObjectExtendeds& objectExtendeds,
    bool linkFromObjectToEdge)
{
    const double ANGLE_DELTA_RADIAN = maps::geolib3::degreesToRadians(60.);
    const double MAX_DISTANCE_METERS = 20.;
    // ширина полосы ~ 4 метра, значит на шестиполосной дороге мы иммем 12 метров
    // от центральной линии по которой расчертили ребро, до края, плюс добавим на ошибку замера

    int unlinkedObjectsCnt = 0;
    EdgeSearcher searcher(edges);
    for (size_t i = 0; i < objectExtendeds.size(); i++) {
        const db::eye::ObjectLocation& location = objectExtendeds[i].location;
        const maps::geolib3::Point2 objPoint = location.mercatorPos();
        // heading отчитывается от севера и по часовой, мы приводим к нормальной = математической форме
        const double signHeading = 2.0 * M_PI - (maps::geolib3::degreesToRadians(getHeading(location.rotation())) - M_PI / 2.0);

        double minDist = DBL_MAX;
        size_t minEdgeIdx = -1;
        size_t minEdgeSegmentIdx = -1;
        bool minEdgeForward = true;
        auto searchResult = searcher.find(location.mercatorPos());
        for (auto itr = searchResult.first; itr != searchResult.second; itr++) {
            const Edge& edge = edges[itr->value()];
            const maps::geolib3::Polyline2& pln = edge.geom;
            for (size_t j = 0; j < pln.pointsNumber() - 1; j++) {
                if (!ptInSegment(objPoint, pln.pointAt(j), pln.pointAt(j + 1)))
                    continue;

                const double edgeSegmentDirection = getEdgeSegmentDirection(pln.pointAt(j), pln.pointAt(j + 1));
                if ((0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Forward)) &&
                    (angleDiff(edgeSegmentDirection + M_PI, signHeading) < ANGLE_DELTA_RADIAN)) {
                    const double dist = maps::geolib3::distance(objPoint, maps::geolib3::Segment2(pln.pointAt(j), pln.pointAt(j + 1)));
                    if (dist < minDist) {
                        minDist = dist;
                        minEdgeIdx = itr->value();
                        minEdgeForward = true;
                        minEdgeSegmentIdx = j;
                    }
                }
                if ((0 != (edge.oneWay & (int)maps::ymapsdf::rd::Direction::Backward)) &&
                    (angleDiff(edgeSegmentDirection, signHeading) < ANGLE_DELTA_RADIAN)) {
                    const double dist = maps::geolib3::distance(objPoint, maps::geolib3::Segment2(pln.pointAt(j), pln.pointAt(j + 1)));
                    if (dist < minDist) {
                        minDist = dist;
                        minEdgeIdx = itr->value();
                        minEdgeForward = false;
                        minEdgeSegmentIdx = j;
                    }
                }
            }
        }
        const double maxDistance = maps::geolib3::toMercatorUnits(MAX_DISTANCE_METERS, objPoint);
        if (minDist < maxDistance) {
            if (minEdgeForward) {
                edges[minEdgeIdx].trafficSignFIds.push_back(objectExtendeds[i].object.id());
            } else {
                edges[minEdgeIdx].trafficSignTIds.push_back(objectExtendeds[i].object.id());
            }
            if (linkFromObjectToEdge) {
                objectExtendeds[i].edgeSegmentId = { edges[minEdgeIdx].edgeId, minEdgeSegmentIdx, minEdgeForward };
            }
        } else {
            INFO() << "min distance: " << maps::geolib3::toMeters(minDist, objPoint) << " from edge " << edges[minEdgeIdx].edgeId << " for object " << objectExtendeds[i].object.id() << " not found";
            unlinkedObjectsCnt++;
        }
    }
    INFO() << "There are " << unlinkedObjectsCnt << " unlinked objects";
}

// dump
void dumpParams(
    DBID commitId,
    double longMin, double longMax,
    double latMin, double latMax,
    maps::json::ObjectBuilder& builder)
{
    builder["params"] << [&](maps::json::ObjectBuilder builder) {
        builder["commit_id"] = commitId;
        builder["long_min"] = longMin;
        builder["long_max"] = longMax;
        builder["lat_min"]  = latMin;
        builder["lat_max"]  = latMax;
    };
}

void dumpJunctions(
    const Revisions& rdJunctions,
    maps::json::ObjectBuilder& builder)
{
    builder["rd_jc"] << [&](maps::json::ArrayBuilder builder) {
        for (auto it = rdJunctions.begin(); it != rdJunctions.end(); it++) {
            builder << [&](maps::json::ObjectBuilder builder) {
                builder["id"] << it->id().objectId();
                const maps::geolib3::Point2 pt = getGeometry<maps::geolib3::Point2>(*it);
                builder["point"] << [&](maps::json::ArrayBuilder builder) {
                    builder << pt.x() << pt.y();
                };
                const ObjectData &data = it->data();
                if (data.attributes) {
                    for (auto ait = data.attributes->begin(); ait != data.attributes->end(); ait++) {
                        if (ait->first.find("cat:rd_jc") == 0) {
                            continue;
                        }
                        builder[ait->first]  << ait->second;
                    };
                };
            };
        };
    };
}

void dumpEdges(
    const Edges& edges,
    maps::json::ObjectBuilder& builder)
{
    builder["rd_el"] << [&](maps::json::ArrayBuilder builder) {
        for (size_t i = 0; i < edges.size(); i++) {
            const Edge& edge = edges[i];
            const maps::geolib3::Polyline2& pln = edge.geom;
            builder << [&](maps::json::ObjectBuilder builder) {
                builder["id"] = edge.edgeId;
                builder["commit_id"] = edge.edgeCommitId;
                builder["f_rd_jc_id"] = edge.fromNodeId;
                builder["t_rd_jc_id"] = edge.toNodeId;
                builder["points"] << [&](maps::json::ArrayBuilder builder) {
                    for (size_t i = 0; i < pln.pointsNumber(); i++) {
                        builder << [&](maps::json::ArrayBuilder builder) {
                            builder <<  pln.pointAt(i).x() <<  pln.pointAt(i).y();
                        };
                    };
                };
                builder["f_zlev"] = edge.f_zlev;
                builder["t_zlev"] = edge.t_zlev;
                builder["access_id"] = edge.accessId;
                builder["speed_limit_f"] = edge.speedLimitF;
                builder["speed_limit_t"] = edge.speedLimitT;
                builder["speed_limit_truck_f"] = edge.speedLimitTruckF;
                builder["speed_limit_truck_t"] = edge.speedLimitTruckT;
                builder["fc"] = edge.fc;
                builder["oneway"] = edge.oneWay;
                builder["sign_ids_f"] << [&](maps::json::ArrayBuilder builder) {
                    for (size_t i = 0; i < edge.trafficSignFIds.size(); i++) {
                        builder << edge.trafficSignFIds[i];
                    };
                };
                builder["sign_ids_t"] << [&](maps::json::ArrayBuilder builder) {
                    for (size_t i = 0; i < edge.trafficSignTIds.size(); i++) {
                        builder << edge.trafficSignTIds[i];
                    };
                };
            };
        };
    };
}

void dumpConditions(
    const Conditions& conds,
    maps::json::ObjectBuilder& builder)
{
    builder["cond"] << [&](maps::json::ArrayBuilder builder) {
        for (size_t i = 0; i < conds.size(); i++) {
            const Condition& cond = conds[i];
            std::vector<DBID> toEdgeIds = conditionToEdgeIdsAsVector(cond);
            builder << [&](maps::json::ObjectBuilder builder) {
                builder["id"] = cond.condId;
                builder["commit_id"] = cond.condCommitId;
                builder["from_edge"] = cond.fromEdgeId;
                builder["via_node"] = cond.viaNodeId;
                builder["to_edges"] << [&](maps::json::ArrayBuilder builder) {
                    for (size_t j = 0; j < toEdgeIds.size(); j++) {
                        builder << toEdgeIds[j];
                    };
                };
                builder["access_id"] = cond.accessId;
                builder["cond_type"] = cond.condType;
            };
        };
    };
}

void dumpObjects(
    const ObjectExtendeds& objectExtendeds,
    maps::json::ObjectBuilder& builder,
    bool saveObjectImages)
{
    builder["objects"] << [&](maps::json::ArrayBuilder builder) {
        for (size_t i = 0; i < objectExtendeds.size(); i++) {
            const ObjectExtended& objectExtended = objectExtendeds[i];
            const db::eye::Object& object = objectExtended.object;
            const db::eye::ObjectLocation& location = objectExtended.location;
            builder << [&](maps::json::ObjectBuilder builder) {
                builder["id"] = object.id();
                builder["sign_type"] = traffic_signs::toString(object.attrs<db::eye::SignAttrs>().type);
                const maps::geolib3::Point2& pt = location.mercatorPos();
                builder["position"] << [&](maps::json::ArrayBuilder builder) {
                    builder << pt.x() << pt.y();
                };
                builder["heading"] = maps::geolib3::degreesToRadians(getHeading(location.rotation()));
                builder["from_time"] = maps::chrono::formatSqlDateTime(objectExtended.timeInterval.fromTime);
                builder["to_time"] = maps::chrono::formatSqlDateTime(objectExtended.timeInterval.toTime);
                builder["slaves"] << [&](maps::json::ArrayBuilder builder) {
                    for (size_t j = 0; j < objectExtended.slaveSignTypes.size(); j++) {
                        builder << traffic_signs::toString(objectExtended.slaveSignTypes[j]);
                    }
                };
                if (objectExtended.edgeSegmentId) {
                    const EdgeSegmentId& e = objectExtended.edgeSegmentId.value();
                    builder["edge_id"] = e.edgeId;
                    builder["edge_segment_idx"] = e.segmentIdx;
                    builder["edge_forward"] = e.forward;
                }
                if (saveObjectImages) {
                    builder["imageFeatures"] << [&](maps::json::ArrayBuilder builder) {
                        for (size_t j = 0; j < objectExtended.imageFeatures.size(); j++) {
                            const ImageFeature& feature = objectExtended.imageFeatures[j];
                            builder << [&](maps::json::ObjectBuilder builder) {
                                builder["id"] = feature.id;
                                builder["url"] = feature.url;
                                builder["bbox"] << [&](maps::json::ArrayBuilder builder) {
                                    builder << feature.bbox.x
                                            << feature.bbox.y
                                            << feature.bbox.x + feature.bbox.width - 1
                                            << feature.bbox.y + feature.bbox.height - 1;
                                };
                            };
                        };
                    };
                }
            };
        };
    };
}

} //namespace

int main(int argc, const char** argv) try {
    maps::cmdline::Parser parser("Test");

    maps::cmdline::Option<std::string> wikiConfigPath = parser.string("wiki-config")
        .required()
        .help("Path to services config for wikimap");

    maps::cmdline::Option<std::string> mrcConfigPath = parser.string("mrc-config")
        .required()
        .help("Path to mrc config");

    maps::cmdline::Option<std::string> secretVersion = parser.string("secret-version")
        .help("version for secrets from yav.yandex-team.ru");

    maps::cmdline::Option<std::string> outJsonPath = parser.string("json-path")
        .help("Path to output json file");

    maps::cmdline::Option<double> xMin = parser.real("long1")
        .required()
        .help("Bounding box minimal longitude");

    maps::cmdline::Option<double> xMax = parser.real("long2")
        .required()
        .help("Bounding box maximum longitude");

    maps::cmdline::Option<double> yMin = parser.real("lat1")
        .required()
        .help("Bounding box minimal latitude");

    maps::cmdline::Option<double> yMax = parser.real("lat2")
        .required()
        .help("Bounding box maximum latitude");

    maps::cmdline::Option<std::string> validTrafficSignsPath = parser.string("ts-path")
        .required()
        .help("Path to list of traffic signs types for dump objects");

    maps::cmdline::Option<bool> saveRoadGraph = parser.flag("save-road-graph")
        .help("Save road graph");

    maps::cmdline::Option<bool> saveObjects = parser.flag("save-objects")
        .help("Save objects");

    maps::cmdline::Option<bool> linkFromObjectToEdge = parser.flag("save-object-link")
        .help("Save linked edges in object");

    maps::cmdline::Option<bool> saveObjectImages = parser.flag("save-object-images")
        .help("Save images url and bboxes for object");

    maps::cmdline::Option<bool> saveConditions = parser.flag("save-conds")
        .help("Save conditions");

    parser.parse(argc, const_cast<char**>(argv));

    maps::wiki::common::ExtendedXmlDoc wikiConfig(wikiConfigPath);
    maps::wiki::common::PoolHolder wiki(wikiConfig, "core", "tasks");
    auto txn = wiki.pool().slaveTransaction();
    RevisionsGateway gateway(*txn);
    const DBID commitId = gateway.headCommitId();

    INFO() << "Commit ID: " << commitId;
    Snapshot snapshot = gateway.snapshot(commitId);

    common::Config mrcConfig =
        secretVersion.defined()
        ? common::Config(maps::vault_boy::loadContextWithYaVault(secretVersion), mrcConfigPath)
        : common::templateConfigFromCmdPath(mrcConfigPath);
    maps::wiki::common::PoolHolder mrc(mrcConfig.makePoolHolder());

    eye::FrameUrlResolver frameUrlResolver(
        mrcConfig.externals().mrcBrowserUrl(),
        mrcConfig.externals().mrcBrowserProUrl());

    const maps::geolib3::Point2 pt1 = maps::geolib3::convertGeodeticToMercator({xMin, yMin});
    const maps::geolib3::Point2 pt2 = maps::geolib3::convertGeodeticToMercator({xMax, yMax});

    ObjectExtendeds objectExtendeds;
    if (saveObjects) {
        objectExtendeds =
            loadObjectsExtended(
                mrc.pool(),
                maps::geolib3::BoundingBox(pt1, pt2),
                loadTrafficSignTypes(validTrafficSignsPath),
                frameUrlResolver);
        INFO() << "Signs found: " << objectExtendeds.size();
    }

    Revisions rdJunctions = loadJunctions(snapshot, pt1, pt2);
    Revisions junctionRelations = loadJunctionRelations(snapshot, extractIds(rdJunctions));
    Edges edges = loadEdges(snapshot, junctionRelations);
    INFO() << "Edges found: " << edges.size();
    removeJunctionsWOEdges(rdJunctions, edges);
    Conditions conditions = loadConditions(snapshot, junctionRelations);

    if (saveObjects) {
        linkTrafficSignsToEdges(edges, objectExtendeds, linkFromObjectToEdge);
    }

    std::ofstream ofs(outJsonPath);
    if (!ofs.is_open())
        ERROR() << "Unable to open output file: " << outJsonPath;
    maps::json::Builder builder(ofs);
    builder << [&](maps::json::ObjectBuilder builder) {
        dumpParams(commitId, xMin, xMax, yMin, yMax, builder);
        if (saveRoadGraph) {
            dumpJunctions(rdJunctions, builder);
            dumpEdges(edges, builder);
        }
        if (saveObjects) {
            dumpObjects(objectExtendeds, builder, saveObjectImages);
        }
        if (saveConditions) {
            dumpConditions(conditions, builder);
        }
    };
    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    FATAL() << "Worker failed: " << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    FATAL() << "Worker failed: " << e.what();
    return EXIT_FAILURE;
}
