#include "caching_region_privacy.h"

#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/spatial_relation.h>

#include <algorithm>
#include <utility>

namespace maps::mrc::privacy {

namespace {

constexpr db::FeaturePrivacy DEFAULT_PRIVACY = db::FeaturePrivacy::Restricted;

bool isValid(const geolib3::Point2& geoPoint)
{
    return std::abs(geoPoint.x()) <= 180 && std::abs(geoPoint.y()) <= 90;
}

std::string toString(const geolib3::Point2& geoPoint)
{
    return "[" + std::to_string(geoPoint.x()) + "," + std::to_string(geoPoint.y()) + "]";
}

void throwIfInvalidGeoPoint(const geolib3::Point2& geoPoint)
{
    REQUIRE(isValid(geoPoint), "Invalid geo point " << toString(geoPoint) << "!");
}

void throwIfInvalidGeoBox(const geolib3::BoundingBox& geoBox)
{
    REQUIRE(
        isValid(geoBox.lowerCorner()) && isValid(geoBox.upperCorner()),
        "Invalid geo box: "
            << "lower corner " << toString(geoBox.lowerCorner()) << ", "
            << "upper corner " << toString(geoBox.upperCorner()) << "!"
    );
}

object::MrcRegions loadForbiddenAreas(object::Loader& objectLoader)
{
    object::MrcRegions regions = objectLoader.loadAllMrcRegions();
    regions.erase(
        std::remove_if(
            regions.begin(), regions.end(),
            [](const object::MrcRegion& region) {
                return region.type() != object::MrcRegion::Type::Restricted;
            }
        ),
        regions.end()
    );
    return regions;
}

} // namespace

CachingRegionPrivacy::CachingRegionPrivacy(
        std::map<db::TId, db::FeaturePrivacy> geoIdToPrivacyMap,
        object::Loader& objectLoader,
        const std::string& geoIdPath,
        LockMemory lockMemory)
    : CachingRegionPrivacy(
        std::move(geoIdToPrivacyMap),
        objectLoader,
        makeGeoIdProvider(geoIdPath, lockMemory)
    )
{}

CachingRegionPrivacy::CachingRegionPrivacy(
        std::map<db::TId, db::FeaturePrivacy> geoIdToPrivacyMap,
        object::Loader& objectLoader,
        GeoIdProviderPtr geoIdProvider)
    : geoIdToPrivacy_(std::move(geoIdToPrivacyMap))
    , geoIdProvider_(std::move(geoIdProvider))
    , forbiddenAreas_(loadForbiddenAreas(objectLoader))
{
    for (const auto& region: forbiddenAreas_) {
        forbiddenAreasSearcher_.insert(
            &region.geom(), std::make_shared<PreparedGeom>(region.geom()));
    }
    forbiddenAreasSearcher_.build();
}

PreparedGeom& CachingRegionPrivacy::geomByGeoId(db::TId geoId)
{
    { // read only scope
        std::shared_lock<std::shared_mutex> guard(geomCacheMutex_);

        auto it = geomCache_.find(geoId);
        if (it != geomCache_.end()) {
            return it->second;
        }
    }


    std::unique_lock<std::shared_mutex> guard(geomCacheMutex_);

    auto [newIt, inserted] = geomCache_.emplace(geoId, geoIdProvider_->geomById(geoId));
    ASSERT(inserted);

    return newIt->second;
}

bool CachingRegionPrivacy::withinForbiddenAreas(
    const geolib3::BoundingBox& geoBox) const
{
    auto mercatorBox = geolib3::convertGeodeticToMercator(geoBox);
    auto range = forbiddenAreasSearcher_.find(mercatorBox);
    for (auto it = range.first; it != range.second; ++it) {
        if (it->value()->contains(mercatorBox)) {
            return true;
        }
    }
    return false;
}

bool CachingRegionPrivacy::intersectsForbiddenAreas(
    const geolib3::BoundingBox& geoBox) const
{
    auto mercatorBox = geolib3::convertGeodeticToMercator(geoBox);
    auto range = forbiddenAreasSearcher_.find(mercatorBox);
    for (auto it = range.first; it != range.second; ++it) {
        if (it->value()->intersects(mercatorBox)) {
            return true;
        }
    }
    return false;
}

std::optional<db::FeaturePrivacy>
CachingRegionPrivacy::evalGeoIdFeaturePrivacy(db::TId geoId) const
{
    auto it = geoIdToPrivacy_.find(geoId);
    if (it == geoIdToPrivacy_.end()) {
        return {};
    }
    return it->second;
}

db::FeaturePrivacy CachingRegionPrivacy::evalFeaturePrivacy(
    const geolib3::Point2& geoPoint)
{
    throwIfInvalidGeoPoint(geoPoint);

    if (withinForbiddenAreas(
            geolib3::resizeByValue(geoPoint.boundingBox(), geolib3::EPS))) {
        return db::FeaturePrivacy::Max;
    }

    std::optional<db::FeaturePrivacy> result;

    for (auto geoId: geoIdProvider_->load(geoPoint)) {
        if (const auto privacy = evalGeoIdFeaturePrivacy(geoId)) {
            if (result.has_value()) {
                result = std::max(result.value(), privacy.value());
            } else {
                result = privacy;
            }

            if (result == db::FeaturePrivacy::Max) {
                break;
            }
        }
    }

    if (result.has_value()) {
        return result.value();
    }
    return DEFAULT_PRIVACY;
}

std::pair<db::FeaturePrivacy, db::FeaturePrivacy>
CachingRegionPrivacy::evalMinMaxFeaturePrivacy(const geolib3::BoundingBox& geoBox)
{
    throwIfInvalidGeoBox(geoBox);

    if (geoBox.isDegenerate()) {
        auto privacy = evalFeaturePrivacy(geoBox.center());
        return std::make_pair(privacy, privacy);
    }

    if (withinForbiddenAreas(geoBox)) {
        return std::make_pair(db::FeaturePrivacy::Max, db::FeaturePrivacy::Max);
    }

    std::optional<db::FeaturePrivacy> maxContainedPrivacy;
    std::optional<db::FeaturePrivacy> minPrivacy;
    std::optional<db::FeaturePrivacy> maxPrivacy;

    for (auto geoId: geoIdProvider_->load(geoBox)) {
        if (const auto privacy = evalGeoIdFeaturePrivacy(geoId)) {
            if (geomByGeoId(geoId).contains(geoBox)) {
                if (maxContainedPrivacy.has_value()) {
                    maxContainedPrivacy =
                        std::max(maxContainedPrivacy.value(), privacy.value());
                } else {
                    maxContainedPrivacy = privacy;
                }

                if (maxContainedPrivacy == db::FeaturePrivacy::Max) {
                    break;
                }
            }

            if (minPrivacy.has_value()) {
                ASSERT(maxPrivacy.has_value());
                minPrivacy = std::min(minPrivacy.value(), privacy.value());
                maxPrivacy = std::max(maxPrivacy.value(), privacy.value());
            } else {
                minPrivacy = privacy;
                maxPrivacy = privacy;
            }
        }
    }

    if (maxContainedPrivacy.has_value()) {
        return std::make_pair(maxContainedPrivacy.value(), maxContainedPrivacy.value());
    }
    if (minPrivacy.has_value()) {
        ASSERT(maxPrivacy.has_value());
        return std::make_pair(
                std::min(DEFAULT_PRIVACY, minPrivacy.value()),
                std::max(DEFAULT_PRIVACY, maxPrivacy.value()));
    }
    return std::make_pair(DEFAULT_PRIVACY, DEFAULT_PRIVACY);
}

db::FeaturePrivacy CachingRegionPrivacy::evalMinFeaturePrivacy(
    const geolib3::BoundingBox& geoBox)
{
    return evalMinMaxFeaturePrivacy(geoBox).first;
}

db::FeaturePrivacy CachingRegionPrivacy::evalMaxFeaturePrivacy(
    const geolib3::BoundingBox& geoBox)
{
    return evalMinMaxFeaturePrivacy(geoBox).second;
}

} // namespace maps::mrc::privacy
