#include "module.h"
#include <yandex/maps/wiki/validator/check.h>
#include <yandex/maps/wiki/validator/categories.h>

#include "../utils/face_builder.h"
#include "../utils/geom.h"
#include "../utils/misc.h"
#include "../utils/node_neighborhood.h"
#include "../utils/object_elements_within_aoi.h"

#include <yandex/maps/wiki/common/misc.h>

#include <unordered_map>
#include <map>

namespace maps {
namespace wiki {
namespace validator {
namespace checks {

using categories::AD;
using categories::AD_FC;
using categories::AD_EL;
using categories::AD_JC;

namespace gl = maps::geolib3;

namespace {

// Return face linear ring represented as a collection of sectors -
// pairs of adjacent linear elements. Counterclockwise rotation from
// rightDirection to leftDirection should pass through face inside.
std::vector<utils::Sector> faceSectors(
        const utils::FaceBuilder& faceBuilder,
        CheckContext* context)
{
    auto viewAdEl = context->objects<AD_EL>();

    double signedArea = 0.0;

    const auto& segments = faceBuilder.segments();
    std::vector<utils::Sector> sectors(segments.size());
    for (size_t iSector = 0; iSector < segments.size(); ++iSector) {
        const auto& segment = segments[iSector];
        gl::PointsVector points = segment.points(viewAdEl);

        for (size_t iPoint = 1; iPoint < points.size(); ++iPoint) {
            const gl::Point2& point = points[iPoint];
            const gl::Point2& prev = points[iPoint - 1];
            signedArea += (point.x() - prev.x()) * (point.y() + prev.y()) / 2;
        }

        size_t iNext = (iSector + 1 < sectors.size()) ? (iSector + 1) : 0;

        gl::Direction2 startDirection(gl::Segment2(points[0], points[1]));
        gl::Direction2 endDirection(
                gl::Segment2(points.back(), points[points.size() - 2]));

        sectors[iSector].rightDirection = startDirection;
        sectors[iSector].rightEdge = segment.edgeId;

        sectors[iNext].leftDirection = endDirection;
        sectors[iNext].leftEdge = segment.edgeId;
    }

    if ((signedArea < 0 && faceBuilder.face()->isInterior())
            || (signedArea > 0 && !faceBuilder.face()->isInterior())) {
        for (auto& sector : sectors) {
            sector = sector.complement();
        }
    }

    return sectors;
}

std::vector<TId> nodeIdsInsideFace(
        const utils::FaceBuilder& faceBuilder,
        CheckContext* context)
{
    std::vector<TId> result;

    // We can't use geolib3::spatialRelation(polygon, point) here,
    // because it checks whether point is on the boundary of the
    // polygon, which is very slow... Also indexing in geos LinearRing
    // is slow too. Just stick to PointsVector.
    gl::PointsVector facePoints = faceBuilder.points();
    std::vector<const Junction*> junctionsWithin =
        context->objects<AD_JC>().byBbox(utils::boundingBox(facePoints));
    for (const Junction* junction : junctionsWithin) {
        if (utils::isPointInsideRing(junction->geom(), facePoints)) {
            result.push_back(junction->id());
        }
    }

    return result;
}

void addAdmUnitNodeNeighborhoods(
        const AdmUnit* admUnit,
        CheckContext* context,
        std::unordered_map<TId, utils::NodeNeighborhood>* nodeNeighborhoods,
        std::unordered_set<const AdmUnit*>* invalidAdmUnits)
{
    // set (nodesWithin - nodesToExclude) should contain all nodes
    // inside admUnit (but not on the boundary)
    std::unordered_set<TId> nodesWithin;
    std::unordered_set<TId> nodesToExclude;

    auto viewAdFc = context->objects<AD_FC>();
    auto viewAdEl = context->objects<AD_EL>();

    for (TId faceId : admUnit->faces()) {
        const Face* face = viewAdFc.byId(faceId);
        utils::FaceBuilder faceBuilder(face, viewAdEl);
        if (!faceBuilder.valid()) {
            invalidAdmUnits->insert(admUnit);
            continue;
        }

        bool invalid = false;
        std::vector<utils::Sector> sectors = faceSectors(faceBuilder, context);
        for (size_t iSector = 0; iSector < sectors.size(); ++iSector) {
            TId nodeId = faceBuilder.segments()[iSector].startNode.id;
            const utils::Sector& sector = sectors[iSector];
            if (sector.rightDirection.angle() == sector.leftDirection.angle()) {
                invalid = true;
                break;
            }
            (*nodeNeighborhoods)[nodeId].addToSector(sector, admUnit);
        }

        if (invalid) {
            invalidAdmUnits->insert(admUnit);
            continue;
        }

        for (const auto& segment : faceBuilder.segments()) {
            nodesToExclude.insert(segment.startNode.id);
        }

        for (TId nodeId : nodeIdsInsideFace(faceBuilder, context)) {
            if (!face->isInterior()) {
                nodesWithin.insert(nodeId);
            } else {
                nodesToExclude.insert(nodeId);
            }
        }
    }

    for (TId nodeId : nodesWithin) {
        if (!nodesToExclude.count(nodeId)) {
            (*nodeNeighborhoods)[nodeId].addToAllParts(admUnit);
        }
    }
}

gl::Polygon2 geomForReport(
        const std::unordered_set<TId>& edgeIds, CheckContext* context)
{
    REQUIRE(!edgeIds.empty(), "Empty set of edges");
    auto id = edgeIds.begin();

    auto viewAdEl = context->objects<AD_EL>();

    gl::BoundingBox totalBbox =
        viewAdEl.byId(*id)->geom().boundingBox();

    for (; id != edgeIds.end(); ++id) {
        const gl::BoundingBox bbox =
            viewAdEl.byId(*id)->geom().boundingBox();
        totalBbox = gl::expand(totalBbox, bbox);
    }
    return gl::resizeByValue(totalBbox, utils::BUFFER_DISTANCE).polygon();
}

bool areInOneHierarchyPath(CheckContext* context, TId adId, TId otherAdId)
{
    REQUIRE(adId != otherAdId, "Same adm units: " << adId);

    auto viewAd = context->objects<AD>();

    auto areInParents = [&] (TId adId, TId otherAdId) -> bool
    {
        std::unordered_set<TId> visitedAdIds;
        TId id = otherAdId;
        while (id && id != adId) {
            if (!visitedAdIds.insert(id).second) {
                return false;
            }
            id = viewAd.byId(id)->parent();
        }
        return id != 0;
    };

    return areInParents(adId, otherAdId) || areInParents(otherAdId, adId);
}

void runSameLevelKindIntersectionsCheck(
        const std::unordered_map<TId, utils::NodeNeighborhood>& nodeNeighborhoods,
        CheckContext* context)
{
    // map from pair of intersecting AdmUnit ids to ids of edges
    // composing the intersection
    std::map<std::vector<TId>, std::unordered_set<TId>> errors;

    for (const auto& neighborhood : nodeNeighborhoods) {
        for (const auto& part : neighborhood.second.parts()) {
            for (const AdmUnit* admUnit : part.admUnits) {
                if (!common::isIn(
                            admUnit->levelKind(),
                            { AdmUnit::LevelKind::Locality,
                              AdmUnit::LevelKind::District })) {
                    for (const AdmUnit* other : part.admUnits) {
                        if (other->id() > admUnit->id()
                                && admUnit->levelKind() == other->levelKind()
                                && !areInOneHierarchyPath(context, other->id(), admUnit->id())) {
                            std::vector<TId> idPair{admUnit->id(), other->id()};
                            errors[idPair].insert(part.sector.rightEdge);
                        }
                    }
                }
            }
        }
    }

    for (const auto& error : errors) {
        context->error(
                "intersecting-adm-units-same-level-kind",
                geomForReport(error.second, context),
                error.first);
    }
}

void runWithinParentCheck(
        const std::unordered_map<TId, utils::NodeNeighborhood>& nodeNeighborhoods,
        const std::unordered_set<const AdmUnit*>& parentsToExclude,
        CheckContext* context)
{
    // map from adm unit to ids of edges composing the part not within parent
    std::unordered_map<const AdmUnit*, std::unordered_set<TId>> errors;

    auto viewAd = context->objects<AD>();

    for (const auto& neighborhood : nodeNeighborhoods) {
        for (const auto& part : neighborhood.second.parts()) {
            for (const AdmUnit* admUnit : part.admUnits) {
                if (admUnit->parent()) {
                    const AdmUnit* parent = viewAd.byId(admUnit->parent());
                    if (!parentsToExclude.count(parent)
                        && !part.admUnits.count(parent)) {
                        errors[admUnit].insert(part.sector.rightEdge);
                    }
                }
            }
        }
    }

    for (const auto& error : errors) {
        context->error(
                "adm-unit-not-within-parent",
                geomForReport(error.second, context),
                { error.first->id(), error.first->parent() });
    }
}

} // namespace

VALIDATOR_SIMPLE_CHECK( ad_hierarchy_geom, AD, AD_FC, AD_EL, AD_JC )
{
    std::unordered_set<const AdmUnit*> parentsToExclude;

    std::unordered_map<TId, utils::NodeNeighborhood> nodeNeighborhoods;

    context->objects<AD>().visit([&](const AdmUnit* admUnit) {
        if (!utils::objectFacesWithinAoi<AD>(context, admUnit)) {
            parentsToExclude.insert(admUnit);
            return;
        }

        addAdmUnitNodeNeighborhoods(
                admUnit, context, &nodeNeighborhoods, &parentsToExclude);
    });

    runWithinParentCheck(nodeNeighborhoods, parentsToExclude, context);
    runSameLevelKindIntersectionsCheck(nodeNeighborhoods, context);
}

} // namespace checks
} // namespace validator
} // namespace wiki
} // namespace maps
