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

#include <yandex/maps/wiki/common/geom.h>
#include <yandex/maps/wiki/common/rd/fow.h>
#include <yandex/maps/wiki/diffalert/revision/snapshot.h>
#include <yandex/maps/wiki/diffalert/revision/diff_context.h>

namespace maps {
namespace wiki {
namespace diffalert {
namespace {

bool isHighFc(const Object& object)
{
    const int fc = object.attr(attr::FC).as<int>();
    return 1 <= fc && fc <= 4;
}

bool is5or6Fc(const Object& object)
{
    const int fc = object.attr(attr::FC).as<int>();
    return fc == 5 || fc == 6;
}

common::FOW getFow(const Object& object)
{
    return static_cast<common::FOW>(object.attr(attr::FOW).as<int>());
}

constexpr double MAX_SEPARATION = 100.0; // meters

} // namespace

void checkHighFcAccessId(const DiffContext& diff, MessageReporter& messages)
{
    if (!(diff.categoryId() == cat::RD_EL && diff.newObject()
            && isHighFc(*diff.newObject()) && diff.attrsChanged())) {
        return;
    }

    auto hasTransportAccess = [](AccessId accessId) {
        return !!((AccessId::Bus | AccessId::Car | AccessId::Truck) & accessId);
    };

    if (!diff.oldObject()) {
        const auto accessId = getAccessId(*diff.newObject());
        if(!hasTransportAccess(accessId)) {
            messages.report({0, 1}, "high-fc-rd-el-access-id-created");
        }
    } else {
        const auto oldAccessId = getAccessId(*diff.oldObject());
        const auto newAccessId = getAccessId(*diff.newObject());
        if (hasTransportAccess(oldAccessId) != hasTransportAccess(newAccessId)) {
             messages.report({0, 1}, "high-fc-rd-el-access-id-change");
        } else if (!hasTransportAccess(newAccessId)) {
            if (oldAccessId != newAccessId || !isHighFc(*diff.oldObject())) {
                messages.report({0, 1}, "high-fc-rd-el-access-id-change");
            }
        }
    }
}

void checkRdElAttrs(const DiffContext& diff, MessageReporter& messages)
{
    if (diff.categoryId() != cat::RD_EL) {
        return;
    }

    if (diff.oldObject() && !diff.newObject() &&
        getFow(*diff.oldObject()) == common::FOW::Driveway) {
            messages.report({1, 0}, "rd-el-fow-driveway-deleted");
    }
    if (diff.newObject() && !diff.oldObject() &&
        getFow(*diff.newObject()) == common::FOW::Driveway) {
            messages.report({1, 0}, "rd-el-fow-driveway-created");
    }

    if (!diff.oldObject() || !diff.newObject()) {
        return;
    }

    const auto oldToll = diff.oldObject()->attr(attr::TOLL).as<bool>();
    const auto newToll = diff.newObject()->attr(attr::TOLL).as<bool>();

    if (oldToll != newToll) {
        messages.report({1, 2}, "rd-el-toll-changed");
    }

    if (isAny(diff, isHighFc)) {
        const auto oldSpeedCat = diff.oldObject()->attr(attr::SPEED_CAT).as<int>();
        const auto newSpeedCat = diff.newObject()->attr(attr::SPEED_CAT).as<int>();

        if (oldSpeedCat != newSpeedCat) {
            messages.report({3, 2}, "high-fc-rd-el-speed-cat-changed");
        }
    }

    const auto oldFc = diff.oldObject()->attr(attr::FC).as<int>();
    const auto newFc = diff.newObject()->attr(attr::FC).as<int>();

    if (std::abs(newFc - oldFc) >= 2) {
        if (isAny(diff, isHighFc)) {
            messages.report({1, 1}, "high-fc-rd-el-fc-changed");
        } else if (isAny(diff, is5or6Fc)) {
            messages.report({2, 3}, "rd-el-fc-changed");
        }
    }

    const auto oldFow = getFow(*diff.oldObject());
    const auto newFow = getFow(*diff.newObject());

    if (oldFow != newFow) {
        if (newFow == common::FOW::Driveway) {
            messages.report({1, 0}, "rd-el-fow-driveway-is-set");
        }
        if (oldFow == common::FOW::Driveway) {
            messages.report({1, 0}, "rd-el-fow-driveway-is-unset");
        }
    }
}

namespace {
uint32_t fcToPoorConditionMajor(int fc)
{
    ASSERT(fc);
    return
        (1 <= fc && fc <=4)
            ? 0
            : (5 == fc || fc == 6)
                ? 1
                : fc == 7
                    ? 2
                    : 3;//10
}

void checkRdElPoorCondition(const DiffContext& diff, MessageReporter& messages, bool only1to4)
{
    if (diff.categoryId() != cat::RD_EL || !diff.oldObject() || !diff.newObject()) {
        return;
    }
    if (diff.oldObject()->attr(attr::POOR_CONDITION) == diff.newObject()->attr(attr::POOR_CONDITION)) {
        return;
    }
    const auto oldFc = diff.oldObject()->attr(attr::FC).as<int>();
    const auto fc = diff.newObject()->attr(attr::FC).as<int>();
    if (only1to4 && fc > 4 && oldFc > 4) {
        return;
    }
    if ((fc == 8 || fc == 9) && (oldFc == 8 || oldFc == 9)) {
        return;
    }

    messages.report(
        {std::min(fcToPoorConditionMajor(fc), fcToPoorConditionMajor(oldFc)), 0},
        "rd-el-poor_condition-changed");
}
} // namespace

void
checkRdElPoorCondition(const DiffContext& diff, MessageReporter& messages)
{
    checkRdElPoorCondition(diff, messages, false);
}

void
checkRdElPoorConditionHighFc(const DiffContext& diff, MessageReporter& messages)
{
    checkRdElPoorCondition(diff, messages, true);
}

void checkRdElUnderConstructionEditor(
        const DiffContext& diff,
        MessageReporter& messages)
{
    if (!(diff.categoryId() == cat::RD_EL && diff.oldObject() && diff.newObject())) {
        return;
    }

    const auto oldUnderConstruction = isUnderConstruction(*diff.oldObject());
    const auto newUnderConstruction = isUnderConstruction(*diff.newObject());

    if (isAny(diff, isHighFc)) {
        if (oldUnderConstruction != newUnderConstruction) {
            messages.report({1, 3}, "high-fc-rd-el-srv-uc-changed");
        }
    } else {
        if (oldUnderConstruction && !newUnderConstruction) {
            messages.report({2, 2}, "rd-el-srv-uc-turn-off");
        }
    }
}

void checkLoneHighFcRdEl(const DiffContext& diff, MessageReporter& messages)
{
    if (!(diff.categoryId() == cat::RD_EL && diff.newObject() &&
            isHighFc(*diff.newObject()))) {
        return;
    }
    if (diff.oldObject() && isHighFc(*diff.oldObject())) {
        return;
    }

    const auto& geom = diff.newObject()->geom();
    const auto* envelopePtr = geom->getEnvelopeInternal();
    ASSERT(envelopePtr);

    auto envelope = *envelopePtr;
    auto mercatorSeparation = MAX_SEPARATION / mercatorDistanceRatio(envelope);
    envelope.expandBy(mercatorSeparation);

    auto closeElements = diff.oldSnapshot().primitivesByEnvelope(
        envelope, GeometryType::LineString, {cat::RD_EL});
    for (const auto& other : closeElements) {
        const auto& otherGeom = other->geom();
        if (isHighFc(*other) && geom.distance(otherGeom) < mercatorSeparation) {
            return;
        }
    }
    messages.report({0, 3}, "lone-high-fc-rd-el");
}

void checkRdElNearBoundJcOrBlockedRd(const DiffContext& diff, MessageReporter& messages)
{
    if (!(diff.categoryId() == cat::RD_EL)) {
        return;
    }

    constexpr double MAX_DISTANCE = 300.0; // meters

    auto checkProximity = [](const Object& object, Snapshot& snapshot)
    {
        const auto& geom = object.geom();
        const auto* envelopePtr = geom->getEnvelopeInternal();
        ASSERT(envelopePtr);

        auto envelope = *envelopePtr;
        auto mercatorDistance = MAX_DISTANCE / mercatorDistanceRatio(envelope);
        envelope.expandBy(mercatorDistance);

        auto closeElements = snapshot.primitivesByEnvelope(
            envelope, GeometryType::Point, {cat::RD_JC});

        for (const auto& jc : closeElements) {
            if (jc->attr(attr::CAT_BOUND_JC).as<bool>() &&
                geom.distance(jc->geom()) <= mercatorDistance) {
                return true;
            }
        }

        closeElements = snapshot.primitivesByEnvelope(
            envelope, GeometryType::LineString, {cat::RD_EL});

        for (const auto& el : closeElements) {
            if (el->attr(attr::SYS_BLOCKED).as<bool>() &&
                geom.distance(el->geom()) <= mercatorDistance) {
                return true;
            }
        }

        return false;
    };

    if ((diff.oldObject() &&
            checkProximity(*diff.oldObject(), diff.oldSnapshot())) ||
            (diff.newObject() &&
            checkProximity(*diff.newObject(), diff.newSnapshot()))) {
        if (!diff.oldObject()) {
            messages.report({2, 3}, "rd-el-near-bound-jc-or-blocked-el-created");
        }
        else if (!diff.newObject()) {
            messages.report({2, 3}, "rd-el-near-bound-jc-or-blocked-el-deleted");
        } else {
            if (diff.geomChanged()) {
                auto oldLength = diff.oldObject()->geom()->getLength();
                auto newLength = diff.newObject()->geom()->getLength();
                auto sortPriority = -std::abs(newLength - oldLength);
                messages.report({2, 3, sortPriority}, "rd-el-near-bound-jc-or-blocked-el-geom-changed",
                                Message::Scope::GeomDiff);
            }
            if (diff.attrsChanged()) {
                messages.report({2, 3}, "rd-el-near-bound-jc-or-blocked-el-attrs-changed");
            }
        }
    }
}

} // namespace diffalert
} // namespace wiki
} // namespace maps
