#include "ft.h"
#include "magic_string.h"
#include "utils.h"

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

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

namespace maps::wiki::diffalert {
namespace {

const std::string HYDRO_POINT = "hydro-point";
const std::string RELIEF_POINT = "relief-point";

const std::string URBAN_AREAL = "urban-areal";
const std::string URBAN_ROADNET_AREAL = "urban-roadnet-areal";

const std::vector<std::string> CATEGORIES_TO_COMPUTE_AREA_DIFF = {
    cat::VEGETATION,
    cat::HYDRO,
    cat::RELIEF,
    cat::URBAN,
    cat::URBAN_AREAL,
    cat::URBAN_ROADNET,
    cat::URBAN_ROADNET_AREAL
};

bool isMajorFtTypeForLongTask(const Object& obj)
{
    if (common::isIn(
            obj.categoryId(),
            {
                cat::URBAN,
                cat::URBAN_AREAL,
                cat::URBAN_ROADNET,
                cat::URBAN_ROADNET_AREAL,
            })) {
        return true;
    }

    return common::isIn(
            getFtType(obj),
            {
                FtType::VegetationPark,
                FtType::VegetationNatpark,
                FtType::UrbanCemetery,

                FtType::HydroOcean,
                FtType::HydroSea,
                FtType::HydroBay,
                FtType::HydroStrait,
                FtType::HydroReservoir,

                FtType::HydroRiverLarge,
                FtType::HydroRiverGreatest,

                FtType::ReliefIsland,
                FtType::ReliefArchipelago,
            });
}

bool isMajorFtTypeForEditor(const Object& obj)
{
    return isMajorFtTypeForLongTask(obj)
        && !common::isIn(
            getFtType(obj),
            {
                FtType::UrbanRoadnetBridge,
                FtType::UrbanRoadnetPedestrianBridge,
            });
}

void reportMajorFeaturesChanges(
    const DiffContext& d,
    MessageReporter& messages,
    CalcSortPriority calcSortPriority)
{
    if (!d.oldObject()) {
        messages.report({0, 3}, "major-feature-created");
    } else if (!d.newObject()) {
        messages.report({0, 3}, "major-feature-deleted");
    } else {
        if (d.geomChanged()) {
            auto sortPriority = 0.0;
            if (calcSortPriority == CalcSortPriority::Yes &&
                    common::isIn(d.categoryId(), CATEGORIES_TO_COMPUTE_AREA_DIFF)) {
                sortPriority = -symDiffArea(d);
            }
            messages.report({1, 3, sortPriority}, "major-feature-geom-changed",
                            Message::Scope::GeomDiff);
        }
        if (namesChanged(d)) {
            messages.report({0, 3}, "major-feature-names-changed");
        }
        if (getFtType(*d.oldObject()) != getFtType(*d.newObject())) {
            messages.report({2, 3}, "major-feature-type-changed");
        }
    }
}

std::vector<std::string>
addedNames(const DiffContext& d)
{
    TIds addedNameIds;
    for (const auto& rel : d.tableAttrsAdded()) {
        if (role::name::ALL_ROLES.contains(rel.role)) {
            addedNameIds.insert(rel.slaveId);
        }
    }
    const auto nameObjects = d.newSnapshot().objectsByIds(addedNameIds);

    std::vector<std::string> names;
    for (const auto& nameObj : nameObjects) {
        names.push_back(nameObj->attr(attr::NAME).value());
    }
    return names;
}

bool isAddrAndOnlyDigitsChange(const DiffContext& d)
{
    if (d.categoryId() != cat::ADDR) {
        return false;
    }
    const auto addrAddedNames = addedNames(d);
    for (const auto& name : addrAddedNames) {
        for (const auto ch : name) {
            if (ch < '0' || ch > '9') {
                return false;
            }
        }
    }
    return true;
}

} // namespace

void checkMajorFeaturesEditor(const DiffContext& d, MessageReporter& messages)
{
    if (isAny(d, isMajorFtTypeForEditor)) {
        reportMajorFeaturesChanges(d, messages, CalcSortPriority::No);
    }
}

void checkMajorFeaturesLongTask(const DiffContext& d, MessageReporter& messages)
{
    if (isAny(d, isMajorFtTypeForLongTask)) {
        reportMajorFeaturesChanges(d, messages, CalcSortPriority::Yes);
    }
}

void checkNamedFeatures(const DiffContext& d, MessageReporter& messages)
{
    if (!namesChanged(d)) {
        return;
    }
    if (isAddrAndOnlyDigitsChange(d)) {
        return;
    }
    if (!d.oldObject()) {
        messages.report({2, 0}, "named-feature-created");
        return;
    } else if (!d.newObject()) {
        return;
    }
    messages.report({2, 0}, "named-feature-renamed");
}

void checkUrbanAndRoadnetArealChange(const DiffContext& d, MessageReporter& messages)
{
    if (!common::isIn(d.categoryId(), {cat::URBAN_AREAL, cat::URBAN_ROADNET_AREAL})) {
        return;
    }

    const std::string& messagePrefix = (d.categoryId() == cat::URBAN_AREAL)
                                           ? URBAN_AREAL
                                           : URBAN_ROADNET_AREAL;

    auto calcArea = [](const Object& object)
    {
        double ratio = mercatorDistanceRatio(object.geom());
        return object.geom()->getArea() * ratio * ratio;
    };

    const double AREA_THRESHOLD = 1 * 1000 * 1000; // square meters
    const double DISTANCE_THRESHOLD = 300; // meters

    if (!d.oldObject()) {
        auto area = calcArea(*d.newObject());
        if (area > AREA_THRESHOLD) {
            double sortPriority = -area;
            messages.report({2, 2, sortPriority}, "big-" + messagePrefix + "-created");
        }
    }
    else if (!d.newObject()) {
        auto area = calcArea(*d.oldObject());
        if (area > AREA_THRESHOLD) {
            double sortPriority = -area;
            messages.report({2, 2, sortPriority}, "big-" + messagePrefix + "-deleted");
        }
    }
    else if (d.geomChanged()) {
        auto diffArea = symDiffArea(d);
        if (diffArea > AREA_THRESHOLD) {
            double sortPriority = -diffArea;
            messages.report({2, 2, sortPriority}, messagePrefix + "-significant-geom-changed");
        }

        geos::geom::Coordinate oldCenter, newCenter;
        ASSERT(d.oldObject()->geom()->getCentroid(oldCenter));
        ASSERT(d.newObject()->geom()->getCentroid(newCenter));

        double ratio = mercatorDistanceRatio((oldCenter.y + newCenter.y) * 0.5);

        if (oldCenter.distance(newCenter) * ratio > DISTANCE_THRESHOLD) {
            messages.report({2, 2}, messagePrefix + "-significant-geom-displacement");
        }
    }
}

void checkContourFeatureArea(const DiffContext& d, MessageReporter& messages)
{
    if (!(common::isIn(d.categoryId(), {cat::VEGETATION, cat::HYDRO, cat::RELIEF})
          && d.oldObject()
          && (!d.newObject() || d.geomChanged()))) {
        return;
    }

    const double AREA_THRESHOLD = 10*1000*1000; // square meters

    if (d.newObject()) {
        auto diffArea = symDiffArea(d);
        if (diffArea > AREA_THRESHOLD) {
            double sortPriority = -diffArea;
            messages.report({1, 3, sortPriority}, "feature-big-area-change",
                            Message::Scope::GeomDiff);
        }
    } else {
        auto oldArea = contourObjectArea(d, SnapshotTime::Old);
        if (oldArea.exterior >= AREA_THRESHOLD) {
            messages.report({1, 3}, "feature-big-area-deleted");
        }
    }
}

void checkPointHydroRelief(const DiffContext& diff, MessageReporter& messages)
{
    if (!common::isIn(diff.categoryId(), {cat::HYDRO_POINT, cat::RELIEF_POINT})) {
        return;
    }
    const std::string& messagePrefix =
        (diff.categoryId() == cat::HYDRO_POINT)
        ? HYDRO_POINT
        : RELIEF_POINT;

    const auto dispClass = std::min(
        diff.oldObject() ? getDispClass(*diff.oldObject()) : DISP_CLASS_IGNORED,
        diff.newObject() ? getDispClass(*diff.newObject()) : DISP_CLASS_IGNORED);
    const uint32_t majorPriority = dispClass < 5 ? 0 : (dispClass == 5 ? 1 : 2);

    if (!diff.oldObject()) {
        messages.report({majorPriority, 0}, messagePrefix + "-created");
        return;
    }
    if (!diff.newObject()) {
        messages.report({majorPriority, 0}, messagePrefix + "-deleted");
        return;
    }
    if (diff.attrsChanged() || diff.tableAttrsChanged()) {
        messages.report({majorPriority, 0}, messagePrefix + "-attrs-changed");
    }

    const auto& oldGeom = diff.oldObject()->geom();
    const auto& newGeom = diff.newObject()->geom();
    const auto displacement = mercatorDistanceRatio(oldGeom) * oldGeom.distance(newGeom);
    if (displacement >= 100) {
        messages.report({majorPriority, 0}, messagePrefix + "-displaced");
    }
}

} // namespace maps::wiki::diffalert
