#include "indoor_level.h"

#include <yandex/maps/wiki/revision/branch.h>
#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionsgateway.h>
#include <yandex/maps/wiki/revision/snapshot.h>
#include <yandex/maps/wiki/common/geom_utils.h>

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

#include <geos/geom/CoordinateArraySequenceFactory.h>
#include <geos/geom/CoordinateSequenceFactory.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/geom/LinearRing.h>
#include <geos/geom/Polygon.h>

#include <unordered_map>
#include <unordered_set>

using namespace std::string_literals;

namespace maps::wiki::merge_poi {

namespace {
const auto APPROVED_BRANCH = "approved"s;
const auto RELEASE_BRANCH = "release"s;
const auto CAT_INDOOR_LEVEL = "cat:indoor_level"s;
const auto ATTRIBUTE_SYS_NOT_OPERATING = "sys:not_operating"s;
const auto ATTRIBUTE_BLOCK_FEEDBACK = "indoor_plan:block_sprav_feedback"s;

revision::Branch loadBranch(pqxx::transaction_base& txn)
{
    revision::BranchManager branchManager(txn);
    auto branches = branchManager.load({
            { revision::BranchType::Stable, 1}
        });
    if (!branches.empty() && branches.front().state() != revision::BranchState::Unavailable ) {
        INFO() << "Using branch: " << branches.front().id();
        return branches.front();
    }
    WARN() << "No suitable stable branch found, fallback to approved.";
    return branchManager.loadByString(APPROVED_BRANCH);
}

} // namespace

IndoorPlansData
loadIndoorPlansData(pgpool3::Pool& pool)
{
    auto txn = pool.slaveTransaction();
    const auto branch = loadBranch(*txn);
    revision::RevisionsGateway gateway(*txn, branch);
    auto snapshot = gateway.snapshot(gateway.maxSnapshotId());
    const auto filter =
        revision::filters::ObjRevAttr::isNotDeleted() &&
        revision::filters::Geom::defined() &&
        revision::filters::Attr(CAT_INDOOR_LEVEL).defined();

    const auto indoorLevelRevs = snapshot.objectRevisionsByFilter(filter);
    std::unordered_set<ObjectId> levelIds;
    for (const auto& indoorLevelRev : indoorLevelRevs) {
        levelIds.insert(indoorLevelRev.id().objectId());
    }
    const auto relationsWithIndoorPlan = snapshot.loadMasterRelations(levelIds);
    std::unordered_set<ObjectId> indoorPlanIds;
    std::unordered_map<ObjectId, ObjectId> levelToPlan;
    for (const auto& relationsWithIndoorPlan : relationsWithIndoorPlan) {
        indoorPlanIds.insert(relationsWithIndoorPlan.data().relationData->masterObjectId());
        levelToPlan.emplace(
            relationsWithIndoorPlan.data().relationData->slaveObjectId(),
            relationsWithIndoorPlan.data().relationData->masterObjectId());
    }
    IndoorPlansData indoorPlansData;
    const auto plansData = snapshot.objectRevisions(indoorPlanIds);
    auto isFeedbackEnabled = [&](ObjectId levelId) -> bool {
        const auto itPlanId = levelToPlan.find(levelId);
        if (itPlanId == levelToPlan.end()) {
            WARN() << " Indoor level without plan: " << levelId;
            return false;
        }
        const auto planDataIt = plansData.find(itPlanId->second);
        if (planDataIt == plansData.end()) {
            WARN() << "Failed to load plan data: " << itPlanId->second;
            return false;
        }
        auto& attributes = planDataIt->second.data().attributes;
        bool result = !attributes ||
            (!attributes->contains(ATTRIBUTE_SYS_NOT_OPERATING) &&
            !attributes->contains(ATTRIBUTE_BLOCK_FEEDBACK));
        INFO() << "isFeedbackEnabled: " << result << " " <<
            levelId << "->" <<  itPlanId->second <<
            " " << ATTRIBUTE_SYS_NOT_OPERATING << ": " << attributes->contains(ATTRIBUTE_SYS_NOT_OPERATING) <<
            " " << ATTRIBUTE_BLOCK_FEEDBACK << ": " << attributes->contains(ATTRIBUTE_BLOCK_FEEDBACK);
        if (!result) {
            indoorPlansData.inactivePlans.insert(itPlanId->second);
        }
        return result;
    };

    for (const auto& indoorLevelRev : indoorLevelRevs) {
        const auto levelId = indoorLevelRev.id().objectId();
        if (!isFeedbackEnabled(levelId)) {
            continue;
        }
        indoorPlansData.activeLevels.emplace_back(
            IndoorLevel {
                levelId,
                maps::wiki::common::Geom(*indoorLevelRev.data().geometry)
            });
    }
    return indoorPlansData;
}

geolib3::BoundingBox
IndoorLevel::boundingBox() const
{
    const auto* envelope = polygon->getEnvelopeInternal();
    return geolib3::BoundingBox(
            geolib3::Point2(envelope->getMinX(), envelope->getMinY()),
            geolib3::Point2(envelope->getMaxX(), envelope->getMaxY()));
}

bool
IndoorLevel::contains(const poi_feed::FeedObjectData::Position& position) const
{
    const auto positionBuffer = maps::wiki::common::Geom(
        maps::wiki::common::TGeoPoint(position.lon, position.lat)).createBuffer(2.0);
    return polygon->contains(positionBuffer.geosGeometryPtr());
}

} // namespace maps::wiki::poi
