#include "linear_element.h"

#include <maps/wikimap/mapspro/services/editor/src/exception.h>
#include <maps/wikimap/mapspro/services/editor/src/collection.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/objectvisitor.h>
#include <maps/wikimap/mapspro/services/editor/src/objects_cache.h>
#include <maps/wikimap/mapspro/services/editor/src/relations_manager.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/category_traits.h>

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/restrictions.h>
#include <yandex/maps/wiki/configs/editor/master_role.h>
#include <yandex/maps/wiki/configs/editor/topology_groups.h>
#include <maps/libs/log8/include/log8.h>
#include <cmath>

using namespace maps::wiki::common;

namespace maps {
namespace wiki {
LinearElement::LinearElement(const TRevisionId& id, ObjectsCache& cache)
    : GeoObject(id, cache)
    , affectedBySplitId_(0)
    , partOfPrimary_(false)
{}

TOid
LinearElement::commonJunction(const LinearElement* other) const
{
    const TOid junctionAId = this->junctionAId();
    if (other->doesUseJunction(junctionAId)) {
        return junctionAId;
    }
    const TOid junctionBId = this->junctionBId();
    if (other->doesUseJunction(junctionBId)) {
        return junctionBId;
    }
    return 0;
}

TOid
LinearElement::junctionAId() const
{
    auto topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId_);
    REQUIRE(topoGroup, "Topology group is not set for linear element, id: " << id());
    const auto& startJcInfos = slaveRelations().range((*topoGroup).startJunctionRole());
    REQUIRE(startJcInfos.size() == 1,
        "Incorrect start junctions count, id: " << id() <<
        ", junctions count: " << startJcInfos.size());
    return (*startJcInfos.begin()).id();
}

TOid
LinearElement::junctionBId() const
{
    auto topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId_);
    REQUIRE(topoGroup, "Topology group is not set for linear element, id: " << id());
    const auto& endJcInfos = slaveRelations().range((*topoGroup).endJunctionRole());
    REQUIRE(endJcInfos.size() == 1,
        "Incorrect end junctions count, id: " << id() <<
        ", junctions count: " << endJcInfos.size());
    return (*endJcInfos.begin()).id();
}

bool
LinearElement::canMerge(const LinearElement* other) const
{
    if (id() == other->id()) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of equal linear elements";
        return false;
    }

    if (categoryId() != other->categoryId()) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of different categories";
        return false;
    }
    const auto* topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId_);
    REQUIRE(topoGroup,
        "Topology group not set for linear element category: " << categoryId_);

    cache_.relationsManager().loadRelations(
            RelationType::Slave, TOIds { id(), other->id() } );

    if (other->doesUseJunction(junctionAId()) &&
        other->doesUseJunction(junctionBId()) &&
        !(*topoGroup).allowClosedEdge())
    {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of common ends";
        return false;
    }
    if (commonJunction(other) == 0) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of no common junction";
        return false;
    }
    if (!isDirectionContinuous(other)) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of direction change";
        return false;
    }

    for (auto attrDef : category().attrDefs()) {
        if (!attrDef->mergeable()) {
            bool unchanged = true;
            if (!attrDef->junctionSpecific()) {
                unchanged = (attributes_.value(attrDef->id()) ==
                    other->attributes_.value(attrDef->id()));
            } else {
                unchanged = (attributes_.value(attrDef->idA()) == attributes_.value(attrDef->idB()))
                    && (attributes_.value(attrDef->idA()) == other->attributes_.value(attrDef->idA()))
                    && (other->attributes_.value(attrDef->idA()) == other->attributes_.value(attrDef->idB()));
            }
            if (!unchanged) {
                DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of '" << attrDef->id() << "' change";
                return false;
            }
        }
    }

    double mergedLen = geom_.realLength() + other->geom_.realLength();
    if (restrictions().gabarits()
        && restrictions().gabarits()->length()
        && mergedLen > *restrictions().gabarits()->length())
    {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of length restrictions";
        return false;
    }

    StringSet masterRoleIds;
    for (const auto& role : category().masterRoles()) {
        if (isNamedCategory(role.categoryId())) {
            masterRoleIds.insert(role.roleId());
        }
    }

    TOIds mastersThis;
    for (const auto& masterInfo : masterRelations().range(masterRoleIds)) {
        mastersThis.insert(masterInfo.id());
    }
    TOIds mastersOther;
    for (const auto& masterInfo: other->masterRelations().range(masterRoleIds)) {
        mastersOther.insert(masterInfo.id());
    }
    if (mastersOther != mastersThis) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " CAN NOT because of different masters";
        return false;
    }

    return true;
}

bool
LinearElement::isDirectionContinuous(const LinearElement* other) const
{
    TOid commonJunctionId = commonJunction(other);
    if (commonJunctionId == 0) {
        return false;
    };
    for (const auto& attrDef : attributes().definitions()) {
        const auto& directional = attrDef->directional();
        if (!directional) {
            continue;
        }
        const std::string& to = directional->toValue;
        const std::string& from = directional->fromValue;
        const std::string& both = directional->bothValue;
        const std::string& directionalAttribute = attrDef->id();
        std::string  thisDirection = attributes().value(directionalAttribute);
        std::string  otherDirection = other->attributes().value(directionalAttribute);
        if (thisDirection == both && otherDirection == both) {
            continue;
        } else if (thisDirection == both
            || otherDirection == both) {
            return false;
        }
        bool thisToCommon = (junctionBId() == commonJunctionId
            ? thisDirection == from
            : thisDirection == to);
        bool otherToCommon = (other->junctionBId() == commonJunctionId
            ? otherDirection == from
            : otherDirection == to);
        if (thisToCommon == otherToCommon) {
            return false;
        }
    }
    return true;
}

bool
LinearElement::doesUseJunction(TOid junctionId) const
{
    return junctionAId() == junctionId || junctionBId() == junctionId;
}

void
LinearElement::cloneNoGeom(LinearElement* other) const
{
    if (!other) {
        THROW_WIKI_INTERNAL_ERROR("Unable to create new LinearElement object.");
    }
    other->setCategory(categoryId_);
    other->serviceAttributes_ = serviceAttributes_;
    other->cloneAttributes(this);
    other->primaryEdit_ = false;
    other->zmax_ = zmax_;
    other->zmin_ = zmin_;
    other->setAllModified();
}

//! This works for cutting internal parts of the linear element
//! Each fragment's side receives value from closest end
void
LinearElement::chooseJunctionSpecificValue(
    double distanceToA, double distanceToB,
    const AttrDefPtr& attrDef, const Attributes& attrs,
    const std::string& policyValue,
    size_t partNum, size_t totalParts)
{
    const auto& origValueA = attrs.value(attrDef->idA());
    const auto& origValueB = attrs.value(attrDef->idB());
    if (origValueA.empty() && origValueB.empty()) {
        return;
    }
    if (policyValue == "outside") {
        if (partNum == 0) {
            attributes_.setValue(attrDef->id(), attrDef->isA() ? origValueA : attrDef->defaultValue());
        } else if (partNum == totalParts - 1) {
            attributes_.setValue(attrDef->id(), attrDef->isB() ? origValueB : attrDef->defaultValue());
        } else {
            attributes_.setValue(attrDef->id(), attrDef->defaultValue());
        }
    } else if (policyValue.empty() || policyValue == "closest") {
        if (distanceToB < distanceToA) {
            attributes_.setValue(attrDef->id(), origValueB);
        } else {
            attributes_.setValue(attrDef->id(), origValueA);
        }
        return;
    }
    if (policyValue == "odd" || policyValue == "even") {
        REQUIRE(attrDef->valueType() == ValueType::Integer,
            "Only integer attributes can be used to interpolate odd/even.");
        auto valueA = boost::lexical_cast<double>(origValueA);
        auto valueB = boost::lexical_cast<double>(origValueB);
        long long int result = std::round(valueB +
            2 * std::round(((valueA - valueB)/2) * distanceToB / (distanceToA + distanceToB)));
        attributes_.setValue(attrDef->id(), std::to_string(result));
        return;
    }
    if (policyValue == "mixed") {
        REQUIRE(
            attrDef->valueType() == ValueType::Integer ||
            attrDef->valueType() == ValueType::Float,
            "Only number attributes can be used to interpolate.");
        auto valueA = boost::lexical_cast<double>(origValueA);
        auto valueB = boost::lexical_cast<double>(origValueB);
        auto result = valueB + (valueA - valueB) * distanceToB / (distanceToA + distanceToB);
        if (attrDef->valueType() == ValueType::Integer) {
            attributes_.setValue(attrDef->id(),
                std::to_string(static_cast<long long int>(std::round(result))));
        } else {
            attributes_.setValue(attrDef->id(), std::to_string(result));
        }
        return;
    }
}

//! Set junction specific attributes in segment
void
LinearElement::setJunctionSpecificAttributes(
    double offsetFromA, double offsetFromB, const Attributes& attrs,
    size_t partNum, size_t totalParts)
{
    const double length = geom_.realLength();
    for (const auto& attrDef : category().attrDefs()) {
        if (!attrDef->junctionSpecific() ||
            !attributes_.isDefined(attrDef->id())) {
            continue;
        }
        auto policyValue = attrDef->junctionSpecificPolicy();
        if (policyValue.empty()) {
            const auto& policyAttrId = attrDef->junctionSpecificPolicyAttribute();
            if (!policyAttrId.empty()) {
                REQUIRE(attributes_.isDefined(policyAttrId),
                    "junctionSpecificPolicy attribute : " << policyAttrId
                    << " not assigned to category :" << categoryId());
                policyValue = attrs.value(policyAttrId);
            }
        }
        if (attrDef->isA()) {
            chooseJunctionSpecificValue(offsetFromA, offsetFromB + length, attrDef, attrs,
                policyValue, partNum, totalParts);
        }
        if (attrDef->isB()) {
            chooseJunctionSpecificValue(offsetFromA + length, offsetFromB, attrDef, attrs,
                policyValue, partNum, totalParts);
        }
    }
}

void
LinearElement::applyVisitor(ObjectVisitor& visitor) const
{
    visitor.visitLinearElement(this);
}

void
LinearElement::applyProcessor(ObjectProcessor& processor)
{
    processor.processLinearElement(this);
}

} // namespace wiki
} // namespace maps
