#include "revision_lookup.h"

#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/log8/include/log8.h>
#include <yandex/maps/wiki/common/geom_utils.h>

#include <boost/range/algorithm_ext/erase.hpp>

namespace maps::wiki::sprav_feedback {

namespace rf = revision::filters;

namespace {

geolib3::BoundingBox addBuffer(const geolib3::BoundingBox& bbox, double buffer)
{
    return geolib3::BoundingBox(
        bbox.center(),
        bbox.width() + 2 * buffer,
        bbox.height() + 2 * buffer);
}

std::map<revision::DBID, revision::ObjectRevision>
convertToMapByObjectId(revision::Revisions& revs)
{
    std::map<revision::DBID, revision::ObjectRevision> retVal;
    for (auto& rev : revs) {
        auto objectId = rev.id().objectId();
        retVal.emplace(objectId, std::move(rev));
    }
    return retVal;
}

revision::Revisions getEntrances(
    const revision::Snapshot& snapshot,
    const geolib3::Polygon2& polygon,
    double accuracyInMetres)
{
    auto bldBbox = polygon.boundingBox();
    double intersectMercAccuracy = geolib3::toMercatorUnits(accuracyInMetres, bldBbox.center());
    bldBbox = addBuffer(bldBbox, intersectMercAccuracy);

    // find 'cat:poi_entrance'
    rf::ProxyFilterExpr bldFilter =
        rf::ObjRevAttr::isNotDeleted()
            && rf::ObjRevAttr::isNotRelation()
            && rf::Attr("cat:poi_entrance").defined()
            && rf::Geom::defined();

    rf::GeomFilterExpr geomFilter(
        rf::GeomFilterExpr::Operation::IntersectsPoints,
        bldBbox.minX(),
        bldBbox.minY(),
        bldBbox.maxX(),
        bldBbox.maxY());
    auto entranceRevisions = snapshot.objectRevisionsByFilter(bldFilter && geomFilter);

    // leave only exact contained inside bldPolygon
    boost::remove_erase_if(entranceRevisions,
        [&](const revision::ObjectRevision& entranceRevision) {
            const auto& wkbGeom = entranceRevision.data().geometry;
            auto entrancePoint = geolib3::WKB::read<geolib3::Point2>(wkbGeom.value());
            geolib3::Polygon2 entrancePolygon
                = geolib3::BoundingBox(entrancePoint,
                    2 * intersectMercAccuracy,
                    2 * intersectMercAccuracy).polygon();
            if (geolib3::spatialRelation(polygon, entrancePolygon, geolib3::SpatialRelation::Intersects)) {
                return false;
            }
            return true;
        });

    return entranceRevisions;
}

struct IdToName
{
    revision::DBID objectId;
    std::string name;
};

std::vector<IdToName> findNames(
    const revision::Snapshot& snapshot,
    const std::vector<revision::DBID>& ids)
{
    std::vector<IdToName> retVal;

    if (ids.empty()) {
        return retVal;
    }

    auto relationRevs = snapshot.relationsByFilter(
        rf::ObjRevAttr::masterObjectId().in(ids)
            && rf::ObjRevAttr::isNotDeleted()
    );

    std::vector<revision::DBID> slaveObjectIds;
    for (const auto& relation : relationRevs) {
        ASSERT(relation.data().relationData);
        slaveObjectIds.push_back(relation.data().relationData->slaveObjectId());
    }

    if (slaveObjectIds.empty()) {
        return retVal;
    }

    auto nameRevs = snapshot.objectRevisionsByFilter(
        rf::ObjRevAttr::objectId().in(slaveObjectIds) &&
            rf::ObjRevAttr::isNotRelation() &&
            rf::ObjRevAttr::isNotDeleted() &&
            rf::Attr("cat:poi_nm").defined() &&
            !rf::Geom::defined());

    auto idsToNameRev = convertToMapByObjectId(nameRevs);

    for (const auto& relation : relationRevs) {
        ASSERT(relation.data().relationData);
        auto slaveRevIt = idsToNameRev.find(relation.data().relationData->slaveObjectId());
        if (slaveRevIt != idsToNameRev.end()) {
            const auto& attrs = slaveRevIt->second.data().attributes;
            ASSERT(attrs);
            if (attrs->count("poi_nm:name")) {
                retVal.emplace_back(IdToName{
                    relation.data().relationData->masterObjectId(),
                    attrs->at("poi_nm:name")});
            }
        }
    }

    return retVal;
}

} // unnamed namespace

std::vector<RevisionEntrance> findEntrances(
    revision::RevisionsGateway& gtw,
    revision::DBID bldId,
    double accuracyInMeters)
{
    std::vector<RevisionEntrance> retVal;

    auto snapshot = gtw.snapshot(gtw.maxSnapshotId());
    auto revision = snapshot.objectRevision(bldId);
    if (!revision || !revision->data().geometry) {
        return retVal;
    }
    const auto& bldWkb = *revision->data().geometry;
    geolib3::Polygon2 bldPolygon;
    try {
        bldPolygon = geolib3::WKB::read<geolib3::Polygon2>(bldWkb);
    } catch (const maps::RuntimeError&) {
        ERROR() << "Building with id '" << bldId
            << "' has non Polygon geometry.";
        return retVal;
    }

    auto entranceRevisions = getEntrances(snapshot, bldPolygon, accuracyInMeters);

    std::vector<revision::DBID> entrancesIds;
    std::map<revision::DBID, geolib3::Point2> idsToPoint;
    for (const auto& entranceRevision : entranceRevisions) {
        auto objectId = entranceRevision.id().objectId();
        auto& theGeom = entranceRevision.data().geometry;
        REQUIRE(theGeom, "non empty geometry expected in object '" << objectId << "'");
        geolib3::Point2 point;
        point = geolib3::WKB::read<geolib3::Point2>(theGeom.value());
        entrancesIds.emplace_back(objectId);
        idsToPoint[objectId] = point;
    }

    auto idsToName = findNames(snapshot, entrancesIds);

    std::set<revision::DBID> namedIds;
    for (const auto& idToName : idsToName) {
        const auto idToPointIt = idsToPoint.find(idToName.objectId);
        if (idToPointIt == idsToPoint.end()) {
            ERROR() << "Object '" << idToName.objectId
                << "' expected in idsToName vector.";
            continue;
        }
        namedIds.emplace(idToName.objectId);
        retVal.emplace_back(
            RevisionEntrance{idToName.objectId, idToPointIt->second, idToName.name});
    }

    // add entrances with empty names
    for (const auto& idToPoint : idsToPoint) {
        if (namedIds.count(idToPoint.first) == 0) {
            retVal.emplace_back(
                RevisionEntrance{idToPoint.first, idToPoint.second, std::nullopt});
        }
    }

    return retVal;
}

std::optional<misc::PointToBldResult> getEdgePoint(
    pqxx::transaction_base& viewTrunkTxn,
    const geolib3::Point2& geoPoint,
    double searchRadiusInMeters)
{
    auto mercPoint = geolib3::convertGeodeticToMercator(geoPoint);

    // attract entrance point to building edge
    auto retVal = misc::pullPointTowardsBld(
        viewTrunkTxn,
        revision::TRUNK_BRANCH_ID,
        mercPoint,
        true, /* findInsideBld */
        searchRadiusInMeters,
        "" /* bldNumber */
    );

    if (!retVal) {
        retVal = misc::pullPointTowardsBld(
            viewTrunkTxn,
            revision::TRUNK_BRANCH_ID,
            mercPoint,
            false, /* findInsideBld */
            searchRadiusInMeters,
            "" /* bldNumber */
        );
    }

    return retVal;
}

} // namespace maps::wiki::sprav_feedback
