#include "rd.h"
#include "magic_string.h"
#include "utils.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 <functional>
#include <map>
#include <algorithm>

namespace maps {
namespace wiki {
namespace diffalert {
namespace {

enum class RdType {
    CityRoad = 1,
    Route = 2,
    Structure = 3,
    CityHighway = 4,
    CountryHighway = 5,
    Kilometer = 6,
    NamedExit = 7
};

RdType getRdType(const Object& object)
{
    return static_cast<RdType>(object.attr(attr::RD_TYPE).as<int>());
}

enum class RdPriority {
    None = 0,
    Fc7 = 1,
    Fc7Important = 2,
    Important = 3,
    Significant = 4,
    Major = 5
};

RdPriority getRdPriority(Object& rd, Snapshot& snapshot) {
    std::set<TId> rdElIds;
    size_t addrCount = 0;
    for (const auto& relation: rd.loadSlaveRelations()) {
        if (relation.role == role::PART) {
            rdElIds.insert(relation.slaveId);
        } else if (relation.role == role::ASSOCIATED_WITH) {
            ++addrCount;
        }
    }

    std::map<int, size_t> fcToCount;
    for (const auto& rdEl: snapshot.objectsByIds(rdElIds)) {
        ++fcToCount[rdEl->attr(attr::FC).as<int>()];
    }

    typedef std::map<int, size_t>::value_type Pair;
    const int fc = std::max_element(
        fcToCount.begin(), fcToCount.end(),
        [](const Pair& lhs, const Pair& rhs) {
            return lhs.second < rhs.second ||
                (lhs.second == rhs.second && lhs.first > rhs.first);
        }
    )->first;

    if (1 <= fc && fc <= 5 && addrCount >= 100) {
        return RdPriority::Major;
    }

    if (1 <= fc && fc <= 6) {
        return (addrCount >= 50) ? RdPriority::Significant : RdPriority::Important;
    }

    if (fc == 7) {
        return addrCount >= 50 ? RdPriority::Fc7Important : RdPriority::Fc7;
    }

    return RdPriority::None;
}


typedef std::function<void(RdPriority, const std::string&)> Reporter;

void checkImportantRd(const DiffContext& d, Reporter report)
{
    const bool anyNameChanged = namesChanged(d);
    if (!(d.categoryId() == cat::RD && (d.stateChanged() || anyNameChanged))) {
        return;
    }

    if (!d.oldObject()) {
        const auto priority = getRdPriority(*d.newObject(), d.newSnapshot());
        report(priority, "rd-created");
    } else if (!d.newObject()) {
        const auto priority = getRdPriority(*d.oldObject(), d.oldSnapshot());
        report(priority, "rd-deleted");
    } else if (anyNameChanged) {
        const auto priority = std::max(
            getRdPriority(*d.oldObject(), d.oldSnapshot()),
            getRdPriority(*d.newObject(), d.newSnapshot())
        );

        report(priority, "rd-names-changed");
    }
}

void importantRdCommonReport(
        MessageReporter& messages,
        RdPriority priority,
        const std::string& messageSuffix)
{
    switch (priority) {
        case RdPriority::Major:
            messages.report({1, 1}, "major-" + messageSuffix);
            break;

        case RdPriority::Significant:
            messages.report({1, 2}, "significant-" + messageSuffix);
            break;

        case RdPriority::Important:
            messages.report({2, 1}, "important-" + messageSuffix);
            break;

        case RdPriority::Fc7Important:
            messages.report({2, 1}, "fc7-important-" + messageSuffix);
            break;

        default:
            // do nothing
            break;
    }
}

} // namespace


void checkImportantRdLongtask(const DiffContext& diff, MessageReporter& messages)
{
    checkImportantRd(
        diff,
        [&](RdPriority priority, const std::string& messageSuffix) {
            importantRdCommonReport(messages, priority, messageSuffix);
            if (RdPriority::Fc7 == priority) {
                messages.report({3, 4}, "fc7-" + messageSuffix);
            }
        }
    );
}

void checkImportantRdEditor(const DiffContext& diff, MessageReporter& messages)
{
    checkImportantRd(
        diff,
        [&](RdPriority priority, const std::string& messageSuffix) {
            importantRdCommonReport(messages, priority, messageSuffix);
        }
    );
}

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

    auto isVisibleOnHighScale = [](RdType type)
    {
        return common::isIn(type, {
                RdType::Route,
                RdType::CityHighway,
                RdType::CountryHighway,
                RdType::NamedExit,
            }
        );
    };

    if (!diff.oldObject()) {
        if (isVisibleOnHighScale(getRdType(*diff.newObject()))) {
            messages.report({1, 3}, "rd-high-scale-type-created");
        }
    } else if (!diff.newObject()){
        if (isVisibleOnHighScale(getRdType(*diff.oldObject()))) {
            messages.report({1, 3}, "rd-high-scale-type-deleted");
        }
    } else {
        const auto oldTypeIsVisibleOnHighScale = isVisibleOnHighScale(getRdType(*diff.oldObject()));
        const auto newTypeIsVisibleOnHighScale = isVisibleOnHighScale(getRdType(*diff.newObject()));

        if (newTypeIsVisibleOnHighScale && namesChanged(diff)) {
            messages.report({1, 3}, "rd-high-scale-type-names-changed");
        }
        if (oldTypeIsVisibleOnHighScale && !newTypeIsVisibleOnHighScale) {
            messages.report({2, 3}, "rd-high-scale-type-turn-off");
        }
        if (!oldTypeIsVisibleOnHighScale && newTypeIsVisibleOnHighScale) {
            messages.report({2, 3}, "rd-high-scale-type-turn-on");
        }
    }
}

void checkKilometerRdType(const DiffContext& diff, MessageReporter& messages)
{
    if (!(diff.categoryId() == cat::RD && diff.newObject()
            && RdType::Kilometer == getRdType(*diff.newObject())))
    {
        return;
    }

    auto countLocalRenderLabel = [](Snapshot& snapshot, const Relations& relations)
    {
        TIds renderLabelIds;
        for (const auto& relation: relations) {
            if (relation.role == role::name::RENDER_LABEL) {
                renderLabelIds.insert(relation.slaveId);
            }
        }

        const auto names = snapshot.objectsByIds(renderLabelIds);
        return std::count_if(
            names.begin(), names.end(),
            [](const Snapshot::ObjectPtr& object) {
                return object->attr(attr::IS_LOCAL).as<bool>();
            }
        );
    };

    if (!diff.oldObject()) {
        if (!countLocalRenderLabel(diff.newSnapshot(), diff.tableAttrsAdded())) {
            messages.report({3, 3}, "rd-kilometer-type-without-render-label-created");
        }
    } else {
        if ((RdType::Kilometer != getRdType(*diff.oldObject()) &&
                !countLocalRenderLabel(diff.newSnapshot(), diff.newObject()->loadSlaveRelations()))
                || countLocalRenderLabel(diff.newSnapshot(), diff.tableAttrsAdded()) <
                    countLocalRenderLabel(diff.oldSnapshot(), diff.tableAttrsDeleted()))
        {
            messages.report({3, 3}, "rd-kilometer-type-without-render-label");
        }
    }
}

namespace {

Priority calculateAdRdChangePriority(Object& rd, Snapshot& snapshot)
{
    std::set<TId> rdElIds;
    size_t addrCount = 0;
    for (const auto& relation: rd.loadSlaveRelations()) {
        if (relation.role == role::PART) {
            rdElIds.insert(relation.slaveId);
        } else if (relation.role == role::ASSOCIATED_WITH) {
            ++addrCount;
            if (addrCount > 100) {
                return {1, 0};
            }
        }
    }
    if (rdElIds.empty()) {
        return {3, 0};
    }
    std::map<int, size_t> fcToCount;
    for (const auto& rdEl: snapshot.objectsByIds(rdElIds)) {
        ++fcToCount[rdEl->attr(attr::FC).as<int>()];
    }

    const int fc =
        std::max_element(
            fcToCount.begin(), fcToCount.end(),
            [](const auto& lhs, const auto& rhs) {
                return lhs.second < rhs.second ||
                    (lhs.second == rhs.second && lhs.first > rhs.first);
            }
        )->first;

    if (1 <= fc && fc <= 4) {
        return {1, 0};
    }

    if ((5 <= fc && fc <= 6) || addrCount > 50) {
        return {2, 0};
    }
    return {3, 0};
}

void checkRdAssociation(const DiffContext& diff, MessageReporter& messages, bool calculatePriority)
{
    if (!(diff.categoryId() == cat::RD && diff.newObject() && diff.oldObject())) {
        return;
    }
    auto& oldRd = *diff.oldObject();
    auto& newRd = *diff.newObject();
    const auto oldAdReleations = oldRd.loadMasterRelations();
    const auto newAdReleations = newRd.loadMasterRelations();
    if (oldAdReleations.empty()) {
        return;
    }
    if (newAdReleations.empty()) {
        messages.report({0, 0}, "rd-associated-with-relation-deleted");
        return;
    }
    if (oldAdReleations.begin()->masterId == newAdReleations.begin()->masterId) {
        return;
    }
    auto priority = calculatePriority
        ? std::max(
            calculateAdRdChangePriority(oldRd, diff.oldSnapshot()),
            calculateAdRdChangePriority(newRd, diff.newSnapshot()))
        : Priority {1, 0};
    messages.report(priority, "rd-associated-with-relation-changed");
}
} // namespace

void checkRdAssociationLongTask(const DiffContext& diff, MessageReporter& messages)
{
    checkRdAssociation(diff, messages, true);
}
void checkRdAssociationEditor(const DiffContext& diff, MessageReporter& messages)
{
    checkRdAssociation(diff, messages, false);
}
} // namespace diffalert
} // namespace wiki
} // namespace maps
