#include "utils.h"
#include "magic_string.h"
#include "../message_reporter.h"

#include <yandex/maps/wiki/diffalert/diff_context.h>
#include <yandex/maps/wiki/diffalert/object.h>
#include <yandex/maps/wiki/diffalert/snapshot.h>

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

#include <maps/libs/log8/include/log8.h>
#include <maps/libs/geolib/include/conversion.h>
#include <geos/geom/Coordinate.h>
#include <geos/geom/Envelope.h>
#include <geos/geom/Polygon.h>
#include <geos/geom/GeometryFactory.h>
#include <geos/operation/polygonize/Polygonizer.h>
#include <geos/operation/union/CascadedPolygonUnion.h>

#include <boost/algorithm/string.hpp>
#include <boost/regex/pending/unicode_iterator.hpp>

#include <algorithm>
#include <cmath>
#include <set>

namespace maps::wiki::diffalert {

namespace {

bool anyHasRoleOf(const Relations& relations, const std::set<std::string>& roles) {
    return std::any_of(
        relations.begin(), relations.end(),
        [&roles](const Relation& relation) {
            return roles.count(relation.role);
        }
    );
}

typedef boost::u8_to_u32_iterator<std::string::const_iterator> U8ToU32Iter;

} // namespace

bool namesCreated(const DiffContext& diff)
{
    return anyHasRoleOf(diff.tableAttrsAdded(), role::name::ALL_ROLES);
}

bool namesDeleted(const DiffContext& diff)
{
    return anyHasRoleOf(diff.tableAttrsDeleted(), role::name::ALL_ROLES);
}

bool officialNamesCreated(const DiffContext& diff)
{
    return anyHasRoleOf(diff.tableAttrsAdded(), {role::name::OFFICIAL});
}

bool officialNamesDeleted(const DiffContext& diff)
{
    return anyHasRoleOf(diff.tableAttrsDeleted(), {role::name::OFFICIAL});
}

bool namesChanged(const DiffContext& diff)
{
    return namesCreated(diff) || namesDeleted(diff);
}

bool officialNamesChanged(const DiffContext& diff)
{
    return officialNamesCreated(diff) || officialNamesDeleted(diff);
}

double mercatorDistanceRatio(double mercatorY)
{
    double latitude = geolib3::mercator2GeoPoint(geolib3::Point2(0.0, mercatorY)).y();
    return cos(latitude * M_PI / 180);
}

double mercatorDistanceRatio(const Envelope& envelope)
{
    geos::geom::Coordinate centroid;
    ASSERT(envelope.centre(centroid));
    return mercatorDistanceRatio(centroid.y);
}

double mercatorDistanceRatio(const Geom& geom)
{
    geos::geom::Coordinate centroid;
    ASSERT(geom->getCentroid(centroid));
    return mercatorDistanceRatio(centroid.y);
}

FtType getFtType(const Object& obj)
{
    return static_cast<FtType>(obj.attr(attr::FT_TYPE_ID).as<int>(0));
}

TIds join(TIds lhs, const TIds& rhs)
{
    lhs.insert(rhs.begin(), rhs.end());
    return lhs;
}

size_t utf8length(const std::string& utf8string)
{
    return std::distance(U8ToU32Iter(utf8string.begin()), U8ToU32Iter(utf8string.end()));
}

AccessId getAccessId(const Object& object)
{
    return static_cast<AccessId>(object.attr(attr::ACCESS_ID).as<int>());
}

int getDispClass(const Object& object)
{
    return object.attr(attr::DISP_CLASS).as<int>(10);
}

bool isUnderConstruction(const Object& object)
{
    return object.attr(attr::SRV_UC).as<bool>();
}

namespace {

using PolygonsVecPtr = std::unique_ptr<std::vector<std::unique_ptr<geos::geom::Polygon>>>;

struct Polygons {
    TIds faceIds;
    TIds exteriorElementIds;
    TIds interiorElementIds;
    PolygonsVecPtr exterior;
    PolygonsVecPtr interior;

    size_t countIds() const
    {
        return faceIds.size() + exteriorElementIds.size() + interiorElementIds.size();
    }

    bool isHeavy() const
    {
        constexpr auto HEAVY_IDS_THRESHOLD = 1000;

        return countIds() > HEAVY_IDS_THRESHOLD;
    }
};

Polygons collectContourObjectPolygons(const DiffContext& d, SnapshotTime snapshotTime)
{
    if ((snapshotTime == SnapshotTime::Old && !d.oldObject())
        || (snapshotTime == SnapshotTime::New && !d.newObject())) {
        return {};
    }

    auto& snapshot = snapshotTime == SnapshotTime::Old
        ? d.oldSnapshot() : d.newSnapshot();

    Relations faceRelations;
    if (snapshotTime == SnapshotTime::Old) {
        faceRelations = d.newObject()
            ? d.oldObject()->loadSlaveRelations()
            : d.relationsDeleted();
    } else {
        faceRelations = d.oldObject()
            ? d.newObject()->loadSlaveRelations()
            : d.relationsAdded();
    }

    Polygons result;
    for (const auto& rel : faceRelations) {
        if (common::isIn(rel.role, {role::PART, role::FC_PART})) {
            result.faceIds.insert(rel.slaveId);
        }
    }

    for (auto& face : snapshot.objectsByIds(result.faceIds)) {
        for (const auto& rel : face->loadSlaveRelations()) {
            if (rel.role == role::PART) {
                auto& elementIds = face->attr(attr::IS_INTERIOR).as<bool>()
                    ? result.interiorElementIds
                    : result.exteriorElementIds;
                elementIds.insert(rel.slaveId);
            }
        }
    }

    auto collectPolygons = [&](const TIds& elementIds)
    {
        std::vector<Geom> elementGeoms;
        for (const auto& elem : snapshot.objectsByIds(elementIds)) {
            elementGeoms.push_back(elem->geom());
        }
        if (elementGeoms.empty()) {
            return PolygonsVecPtr{};
        }
        geos::operation::polygonize::Polygonizer polygonizer;
        for (const auto& geom : elementGeoms) {
            polygonizer.add(geom.geosGeometryPtr());
        }

        return PolygonsVecPtr(polygonizer.getPolygons());
    };

    result.exterior = collectPolygons(result.exteriorElementIds);
    result.interior = collectPolygons(result.interiorElementIds);
    return result;
}

double symDiffArea(const Geom& oldGeom, const Geom& newGeom)
{
    if (oldGeom.isNull() && newGeom.isNull()) {
        return 0.0;
    }
    if (oldGeom.isNull()) {
        double ratio = mercatorDistanceRatio(newGeom);
        return newGeom->getArea() * ratio * ratio;
    }
    if (newGeom.isNull()) {
        double ratio = mercatorDistanceRatio(oldGeom);
        return oldGeom->getArea() * ratio * ratio;
    }

    Geom symDiff(oldGeom->symDifference(newGeom.geosGeometryPtr()));
    if (symDiff->isEmpty()) {
        return 0.0;
    }
    double ratio = mercatorDistanceRatio(symDiff);
    return symDiff->getArea() * ratio * ratio;
}

double calcArea(const PolygonsVecPtr& polygons)
{
    double totalArea = 0.0;
    if (!polygons) {
        return totalArea;
    }
    for (const auto& polygon : *polygons) {
        ASSERT(polygon);
        double ratio = mercatorDistanceRatio(*(polygon->getEnvelopeInternal()));
        totalArea += polygon->getArea() * ratio * ratio;
    }
    return totalArea;
}

} // namespace

ContourObjectArea contourObjectArea(const DiffContext& d, SnapshotTime snapshotTime)
{
    auto polygons = collectContourObjectPolygons(d, snapshotTime);
    return {calcArea(polygons.exterior), calcArea(polygons.interior)};
}

double symDiffArea(const DiffContext& d)
{
    using CascadedPolygonUnion = geos::operation::geounion::CascadedPolygonUnion;

    // check for areal objects
    if ((d.oldObject() && !d.oldObject()->geom().isNull())
        || (d.newObject() && !d.newObject()->geom().isNull())) {

        auto getGeom = [](OptionalObject object) {
            return object ? object->geom() : Geom();
        };

        auto oldGeom = getGeom(d.oldObject());
        auto newGeom = getGeom(d.newObject());

        return symDiffArea(oldGeom, newGeom);
    }

    // for contour objects

    auto oldPolygons = collectContourObjectPolygons(d, SnapshotTime::Old);
    auto newPolygons = collectContourObjectPolygons(d, SnapshotTime::New);

    if (d.categoryId() == cat::HYDRO // NMAPS-11701
        && (oldPolygons.isHeavy() || newPolygons.isHeavy())) {

        auto oldArea = calcArea(oldPolygons.exterior);
        auto newArea = calcArea(newPolygons.exterior);
        auto diffArea = std::abs(newArea - oldArea);

        WARN() << "Simple symDiffArea: " << d.objectId()
               << " ids: " << oldPolygons.countIds() << ',' << newPolygons.countIds()
               << " area: " << std::to_string(oldArea) << ',' << std::to_string(newArea)
               << " diff: " << diffArea;

        return diffArea;
    }

    auto collectGeom = [](const Polygons& polygons)
    {
        if (!polygons.exterior || polygons.exterior->empty()) {
            return Geom();
        }

        std::vector<geos::geom::Polygon*> polygonPtrs;
        polygonPtrs.reserve(polygons.exterior->size());
        for (const auto& polygon: *polygons.exterior) {
            polygonPtrs.push_back(polygon.get());
        }

        Geom geom(CascadedPolygonUnion::Union(&polygonPtrs));
        if (polygons.interior) {
            if (polygons.interior->size() >= 2) {
                std::vector<geos::geom::Polygon*> interiorPolygonPtrs;
                interiorPolygonPtrs.reserve(polygons.interior->size());
                for (const auto& polygon: *polygons.interior) {
                    interiorPolygonPtrs.push_back(polygon.get());
                }

                Geom geomInterior(CascadedPolygonUnion::Union(&interiorPolygonPtrs));
                geom = Geom(geom->difference(geomInterior.geosGeometryPtr()));
            } else if (!polygons.interior->empty()) {
                geom = Geom(geom->difference(polygons.interior->front().get()));
            }
        }
        return geom;
    };

    auto oldGeom = collectGeom(oldPolygons);
    auto newGeom = collectGeom(newPolygons);

    return symDiffArea(oldGeom, newGeom);
}

bool hasRelationWithRole(const Relations& relations, const std::string& role)
{
    for (const auto& relation: relations) {
        if (relation.role == role) {
            return true;
        }
    }

    return false;
}

TIds slaveRelativesIds(Object& object, const std::set<std::string>& roles)
{
    auto slaveRelations = object.loadSlaveRelations();
    TIds ids;
    for (const auto& relation : slaveRelations) {
        if (roles.empty() || roles.count(relation.role)) {
            ids.insert(relation.slaveId);
        }
    }
    return ids;
}

double elementsLength(Snapshot& snapshot, const TIds& elementsIds)
{
    const auto elements = snapshot.objectsByIds(elementsIds);
    double totalLength = 0.0;
    for (const auto& element : elements) {
        totalLength += element->geom().realLength();
    }
    return totalLength;
}

namespace {

const std::initializer_list<FtType> nmapsPoiFtTypes {
    FtType::Landmark,
    FtType::UrbanBusinessCenter,
    FtType::UrbanReligionChrist,
    FtType::UrbanReligionIslam,
    FtType::UrbanReligionJudaism,
    FtType::UrbanReligionBuddhism,
    FtType::UrbanReligionWorship,
    FtType::UrbanEduScientific,
    FtType::UrbanEduVocational,
    FtType::UrbanEduKindergarten,
    FtType::UrbanEduSchoolSecondary,
    FtType::UrbanEduUniversity,
    FtType::UrbanMedHospital,
    FtType::UrbanEduLibrary,
    FtType::UrbanLeisureAnimalpark,
    FtType::UrbanLeisureAmusementpark,
    FtType::UrbanLeisureBeach,
    FtType::UrbanLeisureTheater,
    FtType::UrbanLeisureCircus,
    FtType::UrbanLeisureConcertHall,
    FtType::UrbanLeisureMuseum,
    FtType::UrbanCarFuelStation,
    FtType::UrbanCarTrafficPolice,
    FtType::UrbanGovGovernment,
    FtType::UrbanGovCourt,
    FtType::UrbanGovEmbassy,
    FtType::UrbanShoppingMall,
    FtType::UrbanShoppingMarketFarmers,
    FtType::UrbanSportStadium,
    FtType::UrbanSportSki,
    FtType::UrbanSportHippodrome,
    FtType::UrbanLeisureObservationDeck,
    FtType::UrbanLeisureMonument
};

const std::initializer_list<FtType> dispClass5ImportantFtTypes {
    FtType::UrbanBusinessCenter,
    FtType::UrbanCarFuelStation,
    FtType::UrbanEduUniversity,
    FtType::UrbanEduVocational,
    FtType::UrbanGovCenter,
    FtType::UrbanGovGovernment,
    FtType::Landmark,
    FtType::UrbanLeisureAnimalpark,
    FtType::UrbanLeisureCinema,
    FtType::UrbanLeisureCircus,
    FtType::UrbanLeisureConcertHall,
    FtType::UrbanLeisureHouseOfCulture,
    FtType::UrbanLeisureMonument,
    FtType::UrbanLeisureMuseum,
    FtType::UrbanLeisureObservationDeck,
    FtType::UrbanLeisureTheater,
    FtType::UrbanLeisureWaterpark,
    FtType::UrbanMedHospital,
    FtType::UrbanReligionBuddhism,
    FtType::UrbanReligionChrist,
    FtType::UrbanReligionIslam,
    FtType::UrbanReligionJudaism,
    FtType::UrbanShoppingGrocery,
    FtType::UrbanShoppingMall,
    FtType::UrbanShoppingMarketClothes,
    FtType::UrbanShoppingMarketFarmers,
    FtType::UrbanShoppingMarketHousehold,
    FtType::UrbanSportHippodrome,
    FtType::UrbanSportRacetrack,
    FtType::UrbanSportSki,
    FtType::UrbanSportStadium
};

} // namespace

bool isPoi(const std::string& categoryId)
{
    return boost::algorithm::starts_with(categoryId, cat::POI_PREFIX)
        && categoryId != cat::POI_ENTRANCE;
}

bool isIndoor(const std::string& categoryId)
{
    return
        categoryId.starts_with(cat::INDOOR_POI_PREFIX) ||
        categoryId.starts_with(cat::INDOOR_AREA_PREFIX);
}


bool isNmapsPoi(const Object& poi)
{
    return common::isIn(getFtType(poi), nmapsPoiFtTypes);
}

bool isMajorPoi(const Object& poi)
{
    const int dispClass = getDispClass(poi);
    return 1 <= dispClass && dispClass <= 4;
}

bool
isGeoproductPoi(const Object& poi)
{
    return !poi.attr(attr::POI_IS_GEOPRODUCT).value().empty();
}

bool
isGeoproductPoi(const DiffContext& context)
{
    return
        (context.oldObject() && isGeoproductPoi(*context.oldObject())) ||
        (context.newObject() && isGeoproductPoi(*context.newObject()));
}

std::string
makePoiMessage(const std::string& baseMessage, const DiffContext& context, RunMode runMode)
{
    return runMode == RunMode::LongTask && isGeoproductPoi(context)
        ? "geoproduct-" + baseMessage
        : baseMessage;
}

PoiImportance poiImportance(const Object& poi)
{
    const int dispClass = getDispClass(poi);

    if (dispClass == 5) {
        return common::isIn(getFtType(poi), dispClass5ImportantFtTypes)
            ? PoiImportance::ImportantDispClass5
            : PoiImportance::OtherDispClass5;
    }
    if (dispClass == 6) {
        return PoiImportance::ImportantDispClass6;
    }
    if (dispClass == 7) {
        return PoiImportance::ImportantDispClass7;
    }
    return PoiImportance::None;
}

Priority poiMessagePriority(PoiImportance importance)
{
    switch (importance) {
        case PoiImportance::ImportantDispClass5:
            return {2, 2};
        case PoiImportance::OtherDispClass5:
        case PoiImportance::ImportantDispClass6:
            return {3, 1};
        case PoiImportance::ImportantDispClass7:
            return {3, 2};
        default:
            break;
    }
    return {4,4};
}

TeamResponsibilityGroup teamResponsibilityGroup(const Object& object)
{
    auto ftType = object.attr(attr::FT_TYPE_ID).as<int16_t>(0);
    if (!ftType) {
        return TeamResponsibilityGroup::None;
    }
    if (NMAPS_RESPONSIBILITY_FT_TYPES.count(ftType)) {
        return TeamResponsibilityGroup::Nmaps;
    }
    if (SPRAV_RESPONSIBILITY_FT_TYPES.count(ftType)) {
        return TeamResponsibilityGroup::Sprav;
    }
    return TeamResponsibilityGroup::None;
}

TeamResponsibilityGroup teamResponsibilityGroup(const DiffContext& context)
{
    auto oldValue = context.oldObject()
        ? teamResponsibilityGroup(*context.oldObject())
        : TeamResponsibilityGroup::None;
    if (oldValue == TeamResponsibilityGroup::Nmaps) {
        return oldValue;
    }
    auto newValue = context.newObject()
        ? teamResponsibilityGroup(*context.newObject())
        : TeamResponsibilityGroup::None;
    if (newValue == TeamResponsibilityGroup::Nmaps) {
        return newValue;
    }
    return
        newValue == TeamResponsibilityGroup::Sprav ||
        oldValue == TeamResponsibilityGroup::Sprav
        ? TeamResponsibilityGroup::Sprav
        : TeamResponsibilityGroup::None;
}

Priority
prioByRespGroup(const Priority& priority, TeamResponsibilityGroup group)
{
    const auto minor = priority.minor;
    auto major = priority.major;
    if (group == TeamResponsibilityGroup::Nmaps) {
        if (major) {
            --major;
        }
        return {
            std::min(major, 2u),
            minor
        };
    } else if (group == TeamResponsibilityGroup::Sprav) {
        return {
            std::max(major, 3u),
            minor
        };
    } else {
        return priority;
    }
}

Priority prioByRespGroup(const Priority& priority, const DiffContext& context)
{
    return prioByRespGroup(
        priority,
        teamResponsibilityGroup(context));
}

} // namespace maps::wiki::diffalert
