#include "helper.h"

#include "util.h"

#include <maps/wikimap/mapspro/services/editor/src/configs/config.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/categories_strings.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/distance.h>
#include <yandex/maps/wiki/graph/shortest_path.h>
#include <yandex/maps/wiki/configs/editor/categories.h>

#include <boost/optional.hpp>

#include <map>
#include <string>


namespace maps {
namespace wiki {

IdToRevision load(revision::Snapshot& snapshot, const TOIds& ids)
{
    auto idToRevision = snapshot.objectRevisions(ids);

    REQUIRE(
        idToRevision.size() == ids.size(),
        "Impossible to load all objects"
    );

    return idToRevision;
}

revision::Revisions loadSequence(revision::Snapshot& snapshot, const std::vector<TOid>& ids)
{
    // Revision reqires collection of unique ids
    const TOIds idSet{ids.begin(), ids.end()};
    auto idToRevision = snapshot.objectRevisions(idSet);

    revision::Revisions sequence;
    for (const auto& id: ids) {
        auto it = idToRevision.find(id);
        WIKI_REQUIRE(
            it != idToRevision.end() && !(it->second.data().deleted),
            ERR_MISSING_OBJECT,
            "Object with id " << id << " is not found"
        );

        sequence.push_back(it->second);
    }

    return sequence;
}

namespace {

template<typename TObject>
geolib3::BoundingBox findBBoxOfObjectsWithGeolibGeom(const std::vector<TObject>& objects){
    REQUIRE(
        !objects.empty(),
        "Object list is empty"
    );

    auto it = objects.begin();
    geolib3::BoundingBox bbox = it->geom().boundingBox();
    for (++it; it != objects.end(); ++it) {
        bbox = geolib3::expand(bbox, it->geom());
    }

    return bbox;
}

} // namespace

geolib3::BoundingBox findBoundingBox(const RouteElements& elements) {
    return findBBoxOfObjectsWithGeolibGeom(elements);
}

geolib3::BoundingBox findBoundingBox(const routing::Stops& stops) {
    return findBBoxOfObjectsWithGeolibGeom(stops);
}

bool isSizeLimitExceeded(const geolib3::BoundingBox& bbox, double maxSizeMeters)
{
    return maxSizeMeters < mercatorRealLength(
        geolib3::Point2(bbox.minX(), bbox.minY()),
        geolib3::Point2(bbox.maxX(), bbox.minY())
    ) || maxSizeMeters <  mercatorRealLength(
        geolib3::Point2(bbox.minX(), bbox.minY()),
        geolib3::Point2(bbox.minX(), bbox.maxY())
    );
}

std::string getElementRoleByThreadCategory(const std::string& categoryId)
{
    const StringSet roleIds = cfg()->editor()->categories()[categoryId].slaveRoleIds(
        [](const SlaveRole& role) {
            return role.geomPart() && role.roleId() != ROLE_RASP_PART;
        }
    );

    REQUIRE(roleIds.size() == 1, "Impossible define element role");

    return *roleIds.begin();
}

namespace {

std::string getElementRoleIdByThread(const revision::ObjectRevision& object)
{
    return getElementRoleByThreadCategory(extractCategoryId(object));
}

} // namespace


TOIds getThreadElementIds(
        revision::Snapshot& snapshot,
        TOid threadId,
        const TOIds& addElementIds,
        const TOIds& removeElementIds)
{
    TOIds elementIds{addElementIds.begin(), addElementIds.end()};

    if (threadId)  {
        const auto thread = snapshot.objectRevision(threadId);

        WIKI_REQUIRE(
            thread && !thread->data().deleted,
            ERR_MISSING_OBJECT,
            "There is no thread with id " << threadId
        );

        const std::string partRoleId = getElementRoleIdByThread(*thread);
        for (const auto& relation: snapshot.loadSlaveRelations(threadId)) {
            if (extractAttributes(relation).at(ATTR_REL_ROLE) != partRoleId) {
                continue;
            }

            const auto elementId = extractRelation(relation).slaveObjectId();
            elementIds.insert(elementId);
        }
    }

    WIKI_REQUIRE(
        threadId || removeElementIds.empty(),
        ERR_BAD_REQUEST,
        "Impossible remove elements for new transport thread"
    );

    for (const auto id: removeElementIds) {
        WIKI_REQUIRE(
            !addElementIds.count(id),
            ERR_BAD_REQUEST,
            "Impossible remove and add elements " << id << " at the same time"
        );
        WIKI_REQUIRE(
            elementIds.erase(id),
            ERR_BAD_REQUEST,
            "Impossible remove element " << id << ", which is not in thransport thread"
        );
    }

    return elementIds;
}

namespace {

boost::optional<graph::NodeID> getPriorityNode(
        DynamicRouteGraph& graph,
        const geolib3::Point2& point)
{
    const auto nodeIds = graph.nodesAtDistance(point, DEFAULT_STOP_SEARCH_RADIUS_METERS);

    if (nodeIds.empty()) {
        return boost::none;
    }

    graph::NodeID resultNodeId = nodeIds.front();
    const RouteElement& firstElement = graph.getElement(
        graph.getCompoundNodeId(resultNodeId).directedElementId.id()
    );

    double resultDistance = geolib3::distance(point, firstElement.geom());
    for (auto it = nodeIds.begin() + 1; it != nodeIds.end(); ++it) {
        const auto& element = graph.getElement(
            graph.getCompoundNodeId(*it).directedElementId.id()
        );

        const double distance = geolib3::distance(point, element.geom());
        if (distance > resultDistance) {
            continue;
        }

        resultNodeId = *it;
        resultDistance = distance;
    }

    return resultNodeId;
};

} // namespace

TOIds findThreadElementIds(DynamicRouteGraph& graph, const routing::Stops& stops)
{
    WIKI_REQUIRE(
        stops.size() >= 2,
        ERR_BAD_REQUEST,
        "Need at least two stops"
    );

    auto fromNodeId = getPriorityNode(graph, stops.front().geom());
    if (!fromNodeId) {
        throw LogicExceptionWithLocation(ERR_ROUTING_IMPOSSIBLE_SNAP_STOP, stops.front().geom())
            << "Stop " << stops.front().id() << " is too distant from transport graph";
    }

    TOIds routeElementIds;
    for (size_t i = 1; i < stops.size(); ++i) {
        const auto toNodeId = getPriorityNode(graph, stops[i].geom());
        if (!toNodeId) {
            throw LogicExceptionWithLocation(ERR_ROUTING_IMPOSSIBLE_SNAP_STOP, stops[i].geom())
                << "Stop " << stops[i].id() << " is too distant from transport graph";
        }

        const CompoundNodeID toCompountNodeId = graph.getCompoundNodeId(*toNodeId);
        const auto result = graph::findShortestPath(
            /* outEdges = */ [&graph](const graph::NodeID nodeId) {
                return graph.edges(nodeId);
            },
            /* fromNodeId = */ *fromNodeId,
            /* isFinishNode = */ [&](graph::NodeID nodeId) {
                return toCompountNodeId.directedElementId ==
                    graph.getCompoundNodeId(nodeId).directedElementId;
            }
        );
        if (result.path().empty()) {
            throw LogicExceptionWithLocation(ERR_ROUTING_IMPOSSIBLE_BUILD_ROUTE, stops[i].geom())
                << "There is no path between stops " << stops[i - 1].id()
                << " and " << stops[i].id();
        }

        for (const auto& nodeId: result.path()) {
            routeElementIds.insert(graph.getCompoundNodeId(nodeId).directedElementId.id());
        }

        fromNodeId = result.path().back();
    }

    return routeElementIds;
}

routing::Stops toStopSequence(const IdToRevision& idToRevision, const std::vector<TOid>& stopIds)
{
    routing::Stops stops;

    for (const auto stopId: stopIds) {
        const auto it = idToRevision.find(stopId);
        REQUIRE(it != idToRevision.end(), "Missed object " << stopId);

        const auto& revision = it->second;

        REQUIRE( // support only stop category for routing
            extractCategoryId(revision) == CATEGORY_TRANSPORT_STOP,
            "Object " << stopId << " is not " << CATEGORY_TRANSPORT_STOP
        );
        stops.emplace_back(stopId, extractGeom<geolib3::Point2>(revision));
    }

    return stops;
}

routing::Stops loadStops(const revision::Snapshot& snapshot, const std::vector<TOid>& stopIds)
{
    const TOIds idSet{stopIds.begin(), stopIds.end()};
    return toStopSequence(snapshot.objectRevisions(idSet), stopIds);
}

routing::Conditions loadConditions(
        revision::Snapshot& snapshot,
        const RoutingConfig& config,
        const TOIds& elementIds)
{
    if (!config.condition) {
        return {};
    }

    TOIds conditionIds;
    for (const auto& relationRevision: snapshot.loadMasterRelations(elementIds)) {
        const std::string& masterCategoryId = extractAttributes(relationRevision).at(ATTR_REL_MASTER);

        if (masterCategoryId == CATEGORY_COND) {
            conditionIds.insert(extractRelation(relationRevision).masterObjectId());
        }
    }

    routing::Conditions conditions;
    for (const auto& condition: RouteCondition::load(snapshot, conditionIds)) {
        if (!config.condition->filter(condition)) {
            continue;
        }

        conditions.emplace_back(
            condition.id(),
            config.condition->type(condition) == RouteConditionType::Turnabout
                ? common::ConditionType::Uturn
                : common::ConditionType::Prohibited,
            condition.fromElementId(),
            condition.viaJunctionId(),
            condition.toElementIds()
        );
    }

    return conditions;
}

routing::Elements toElements(
        revision::Snapshot& snapshot,
        const RoutingConfig& config,
        const IdToRevision& idToRevision)
{
    std::map<TOid, TOid> elIdToStartJcId;
    std::map<TOid, TOid> elIdToEndJcId;

    TOIds elementIds;
    for (const auto& pair: idToRevision) {
        elementIds.insert(pair.first);
    }

    for (const auto& relationRevision: snapshot.loadSlaveRelations(elementIds)) {
        const std::string& roleId = extractAttributes(relationRevision).at(ATTR_REL_ROLE);

        const auto& relation = extractRelation(relationRevision);
        const TOid elementId = relation.masterObjectId();
        const TOid junctionId = relation.slaveObjectId();

        if (roleId == ROLE_START) {
            elIdToStartJcId[elementId] = junctionId;
        }
        if (roleId == ROLE_END) {
            elIdToEndJcId[elementId] = junctionId;
        }
    }

    REQUIRE(
        elementIds.size() == elIdToStartJcId.size()
            && elementIds.size() == elIdToEndJcId.size(),
        "Missed relations with junctions"
    );

    REQUIRE(
        idToRevision.size() == elementIds.size(),
        "Impossible to load some elements"
    );

    routing::Elements elements;
    for (const auto& pair: idToRevision) {
        const auto& revision = pair.second;
        REQUIRE(
            !revision.data().deleted,
            "Object with id " << pair.first << " is not found"
        );

        RouteElement element(revision);

        const auto itStart = elIdToStartJcId.find(element.id());
        const auto itEnd = elIdToEndJcId.find(element.id());

        WIKI_REQUIRE(
            itStart != elIdToStartJcId.end(),
            ERR_MISSING_OBJECT,
            "Miss start junction for element " << element.id()
        );
        WIKI_REQUIRE(
            itEnd != elIdToEndJcId.end(),
            ERR_MISSING_OBJECT,
            "Miss end junction for element " << element.id()
        );

        elements.emplace_back(
            element.id(),
            config.direction(element),
            element.geom(),
            routing::ElementEnd {
                itStart->second,
                element[STR_START_ZLEV].as<int>(0)
            },
            routing::ElementEnd {
                itEnd->second,
                element[STR_END_ZLEV].as<int>(0)
            }
        );
    }

    return elements;
}

IdToRouteElement mapIdToRouteElement(const IdToRevision& idToRevision)
{
    std::map<TOid, RouteElement> idToRouteElement;

    for (const auto& pair: idToRevision) {
        idToRouteElement.insert({pair.first, RouteElement(pair.second)});
    }

    return idToRouteElement;
}

std::vector<TId> orderedThreadStopIds(const ObjectPtr& thread)
{
    const auto threadStopRelations = thread->slaveRelations().range(ROLE_PART);
    if (threadStopRelations.empty()) {
        return {};
    }

    const auto it = std::find_if(
        threadStopRelations.begin(), threadStopRelations.end(),
        [](const RelationInfo& relation) {
            const auto* threadStop = relation.relative();
            return threadStop->slaveRelations().range(ROLE_PREVIOUS).empty();
        }
    );

    REQUIRE(
        it != threadStopRelations.end(),
        "Can't find first thread stop for thread " << thread->id()
    );

    std::vector<TId> threadStopIds;
    threadStopIds.reserve(threadStopRelations.size());
    for (const auto* threadStop = it->relative(); ;) {
        threadStopIds.push_back(threadStop->id());

        const auto nextRelations = threadStop->masterRelations().range(ROLE_PREVIOUS);
        REQUIRE(
            nextRelations.size() < 2,
            "Two or more next thread_stops, thread " << threadStop->id()
        );

        if (nextRelations.empty()) {
            break;
        }
        threadStop = nextRelations.begin()->relative();
    }

    REQUIRE(
        threadStopIds.size() == threadStopRelations.size(),
        "Missing some thread stops, thread " << thread->id()
    );

    return threadStopIds;
}

void restoreStopIds(revision::Snapshot& snapshot, ThreadStopSequence& threadStopSequence)
{
    const TOIds ids = threadStopSequence.threadStopsWithoutStopId();

    for (const auto& revision: snapshot.loadMasterRelations(ids)) {
        if (extractAttributes(revision).at(ATTR_REL_ROLE) != ROLE_ASSIGNED_THREAD_STOP) {
            continue;
        }

        const auto& relation = extractRelation(revision);
        const TOid stopId = relation.masterObjectId();
        const TOid threadStopId = relation.slaveObjectId();

        threadStopSequence.byId(threadStopId).stopId = stopId;
    }
}

void restoreAttrs(revision::Snapshot& snapshot, ThreadStopSequence& threadStopSequence)
{
    const TOIds ids = threadStopSequence.threadStopsWithoutAttrs();

    for (auto& pair: snapshot.objectRevisions(ids)) {
        const auto id = pair.first;
        const auto& revision = pair.second;

        threadStopSequence.byId(id).attrs = common::AttrsWrap::extract(revision);
    }
}

} // namespace wiki
} // namespace maps
