#include "topo_storage.h"
#include "objects/junction.h"
#include "objects/linear_element.h"
#include "factory.h"
#include "objectclassinfo.h"
#include "utils.h"
#include "objects/relation_object.h"
#include "objects_cache.h"
#include "relation_infos.h"
#include "relations_manager.h"
#include "geom.h"
#include "topological/topo_cache.h"

#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/intersection.h>

#include <geos/geom/Point.h>

#include <sstream>
#include <memory>
#include <algorithm>

namespace maps {
namespace wiki {

namespace {

template <class SetT>
TOIds convertIds(const SetT& ids)
{
    TOIds result;
    std::copy(ids.begin(), ids.end(), std::inserter(result, result.end()));
    return result;
}

} // namespace

TopoStorage::TopoStorage(
        const TopologyGroup& topoGroup, ObjectsCache& cache)
    : topoGroup_(topoGroup)
    , jcToLeRoles_({topoGroup_.startJunctionRole(), topoGroup_.endJunctionRole()})
    , cache_(cache)
{}

TopoStorage::~TopoStorage()
{
    // impl_
}

void
TopoStorage::preloadObjects(const geolib3::BoundingBox& bbox)
{
    if (!topoCache_) {
        topoCache_ = make_unique<TopoCache>(topoGroup_, cache_, bbox);
    }
}

topo::NodeIDSet TopoStorage::nodeIds(const geolib3::BoundingBox& bbox)
{
    preloadObjects(bbox);
    TOIds ids = convertIds(topoCache_->nodeIdsByBBox(bbox));
    return {ids.begin(), ids.end()};
}

topo::EdgeIDSet TopoStorage::edgeIds(const geolib3::BoundingBox& bbox)
{
    preloadObjects(bbox);
    TOIds ids = convertIds(topoCache_->edgeIdsByBBox(bbox));
    return {ids.begin(), ids.end()};
}

namespace {

topo::IncidentNodes nodesFromLinearElement(const LinearElement* lePtr)
{
    topo::IncidentNodes nodes(lePtr->junctionAId(), lePtr->junctionBId());
    REQUIRE(nodes.start,
        "Start junction is not set for LinearElement " << lePtr->id());
    REQUIRE(nodes.end,
        "End junction is not set for LinearElement " << lePtr->id());
    return nodes;
}

} // namespace

topo::NodeVector TopoStorage::nodes(const topo::NodeIDSet& nodeIds)
{
    TOIds ids = convertIds(nodeIds);
    GeoObjectCollection junctions = cache_.get(ids);

    topo::NodeVector res;
    for (const auto& obj : junctions) {
        const Junction* jcPtr = as<const Junction>(obj);
        if (jcPtr && !jcPtr->isDeleted()) {
            res.emplace_back(jcPtr->id(), geomToPoint(jcPtr->geom()));
        }
    }
    return res;
}

topo::EdgeVector TopoStorage::edges(const topo::EdgeIDSet& edgeIds)
{
    TOIds ids = convertIds(edgeIds);
    GeoObjectCollection edges = cache_.get(ids);

    topo::EdgeVector res;
    for (const auto& obj : edges) {
        const LinearElement* lePtr = as<LinearElement>(obj);
        if (lePtr && !lePtr->isDeleted()) {
            topo::IncidentNodes nodes = nodesFromLinearElement(lePtr);
            res.emplace_back(
                lePtr->id(),
                geomToPolyline(lePtr->geom()),
                nodes.start,
                nodes.end);
            DEBUG() << "Edge: " << lePtr->id();
        }
    }
    return res;
}

topo::NodeID TopoStorage::newNodeId()
{
    return cache_.newObjectId().objectId();
}

topo::EdgeID TopoStorage::newEdgeId()
{
    return cache_.newObjectId().objectId();
}

topo::IncidencesByNodeMap
TopoStorage::incidencesByNodes(const topo::NodeIDSet& nodeIds)
{
    TOIds ids = convertIds(nodeIds);
    cache_.relationsManager().loadRelations(RelationType::Master, ids, jcToLeRoles_);
    GeoObjectCollection nodes = cache_.get(ids);
    topo::IncidencesByNodeMap res;
    for (const auto& objPtr : nodes) {
        const Junction* jc = as<const Junction>(objPtr);
        REQUIRE(!jc->isDeleted(), "Attempt to load incidences for deleted node " << jc->id());
        const auto& geomMasters = jc->relations(RelationType::Master).range(jcToLeRoles_);
        for (const auto& mInfo : geomMasters) {
            const std::string& roleId = mInfo.roleId();
            if (roleId == topoGroup_.startJunctionRole()) {
                res[jc->id()].emplace_back(mInfo.id(), topo::IncidenceType::Start);
            } else if (roleId == topoGroup_.endJunctionRole()) {
                res[jc->id()].emplace_back(mInfo.id(), topo::IncidenceType::End);
            }
        }
    }
    return res;
}

topo::IncidencesByEdgeMap
TopoStorage::incidencesByEdges(const topo::EdgeIDSet& edgeIds)
{
    TOIds ids = convertIds(edgeIds);
    cache_.relationsManager().loadRelations(RelationType::Slave, ids, jcToLeRoles_);
    GeoObjectCollection edges = cache_.get(ids);
    topo::IncidencesByEdgeMap res;
    for (const auto& objPtr : edges) {
        const LinearElement* le = as<const LinearElement>(objPtr);
        REQUIRE(!le->isDeleted(), "Attempt to load incidences for deleted edge " << le->id());
        const auto& geomSlaves = le->relations(RelationType::Slave).range(jcToLeRoles_);
        for (const auto& sInfo : geomSlaves) {
            const std::string& roleId = sInfo.roleId();
            if (roleId == topoGroup_.startJunctionRole() ||
                roleId == topoGroup_.endJunctionRole())
            {
                (roleId == topoGroup_.startJunctionRole()
                    ? res[le->id()].start
                    : res[le->id()].end)
                = sInfo.id();
            }
        }
    }
    return res;
}

topo::IncidencesByEdgeMap
TopoStorage::originalIncidencesByEdges(const topo::EdgeIDSet& edgeIds)
{
    TOIds ids = convertIds(edgeIds);
    GeoObjectCollection edges = cache_.get(ids);
    TOIds existingIds;
    for (const auto& obj : edges) {
        if (!obj->isDeleted()) {
            existingIds.insert(obj->id());
        }
    }

    cache_.relationsManager().loadRelations(RelationType::Slave, existingIds, jcToLeRoles_);

    topo::IncidencesByEdgeMap result;
    for (const auto& objPtr : edges) {
        const LinearElement* el = as<LinearElement>(objPtr);
        if (!el->isDeleted()) {
            TOid originalStartId = el->junctionAId();
            TOid originalEndId = el->junctionBId();
            const auto& deletedStartJcDiff = el->slaveRelations().diff("start").deleted;
            if (!deletedStartJcDiff.empty()) {
                ASSERT(deletedStartJcDiff.size() == 1);
                originalStartId = deletedStartJcDiff.front().id();
            }
            const auto& deletedEndJcDiff = el->slaveRelations().diff("end").deleted;
            if (!deletedEndJcDiff.empty()) {
                ASSERT(deletedEndJcDiff.size() == 1);
                originalEndId = deletedEndJcDiff.front().id();
            }
            result.insert({el->id(), topo::IncidentNodes{originalStartId, originalEndId}});
        } else {
            const auto& deletedStartJcDiff = el->slaveRelations().diff("start").deleted;
            ASSERT(deletedStartJcDiff.size() == 1);
            TOid startId = deletedStartJcDiff.front().id();
            const auto& deletedEndJcDiff = el->slaveRelations().diff("end").deleted;
            ASSERT(deletedEndJcDiff.size() == 1);
            TOid endId = deletedEndJcDiff.front().id();
            result.insert({el->id(), topo::IncidentNodes{startId, endId}});
        }
    }

    return result;
}

topo::EdgeIDSet TopoStorage::incidentEdges(topo::NodeID nodeId)
{
    auto objPtr = cache_.get(nodeId);
    REQUIRE(objPtr, "Node not found: " << nodeId);
    Junction* jcPtr = as<Junction>(*objPtr);
    const auto& linearElementInfos =
        jcPtr->relations(RelationType::Master).range(jcToLeRoles_);
    topo::EdgeIDSet res;
    for (const auto& mInfo : linearElementInfos) {
        res.insert(mInfo.id());
    }
    return res;
}


topo::IncidentNodes TopoStorage::incidentNodes(topo::EdgeID edgeId)
{
    auto objPtr = cache_.get(edgeId);
    REQUIRE(objPtr, "Edge not found: " << edgeId);
    LinearElement* lePtr = as<LinearElement>(*objPtr);
    topo::IncidentNodes nodes = nodesFromLinearElement(lePtr);
    return nodes;
}

topo::Node TopoStorage::node(topo::NodeID id)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Node not found: " << id);
    const Junction* jcPtr = as<Junction>(*objPtr);
    return topo::Node(jcPtr->id(), geomToPoint(jcPtr->geom()));
}

topo::Edge TopoStorage::edge(topo::EdgeID id)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Edge not found: " << id);
    const LinearElement* lePtr = as<LinearElement>(*objPtr);
    return topo::Edge(
        lePtr->id(),
        geomToPolyline(lePtr->geom()),
        lePtr->junctionAId(),
        lePtr->junctionBId());
}

topo::Node TopoStorage::createNode(const geolib3::Point2& pos)
{
    GeoObjectFactory factory(cache_);
    ObjectPtr newPtr = factory.createNewObject(
        ObjectsClassInfos::junctionClassInfo,
        topoGroup_.junctionsCategory());
    REQUIRE(newPtr, "Object could not be created");
    Junction* jcPtr = as<Junction>(newPtr);
    jcPtr->setGeometry(pointToGeom(pos));
    if (topoCache_) {
        topoCache_->addObject(jcPtr->id());
    }
    return topo::Node(jcPtr->id(), pos);
}

void TopoStorage::updateNodePos(topo::NodeID id, const geolib3::Point2& pos)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Node not found: " << id);
    Junction* jcPtr = as<Junction>(*objPtr);
    jcPtr->setGeometry(pointToGeom(pos));
    if (topoCache_) {
        topoCache_->updateObject(jcPtr->id(), jcPtr->geom());
    }
}

void TopoStorage::deleteNode(topo::NodeID id)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Node not found: " << id);
    Junction* jcPtr = as<Junction>(*objPtr);
    jcPtr->setState(GeoObject::State::Deleted);
    if (topoCache_) {
        topoCache_->deleteObject(jcPtr->id());
    }
}

topo::Edge TopoStorage::createEdge(const geolib3::Polyline2& geom,
    const topo::IncidentNodes& incidentNodes)
{
    GeoObjectFactory factory(cache_);
    ObjectPtr newPtr = factory.create(
        ObjectsClassInfos::linearElementClassInfo,
        cache_.newObjectId());
    REQUIRE(newPtr, "Object could not be created");
    LinearElement* lePtr = as<LinearElement>(newPtr);
    lePtr->setGeometry(polylineToGeom(geom));
    if (topoCache_) {
        topoCache_->addObject(lePtr->id());
    }
    return topo::Edge(lePtr->id(), geom, incidentNodes.start, incidentNodes.end);
}

void TopoStorage::updateEdgeGeom(topo::EdgeID id,
    const geolib3::Polyline2& geom,
    const topo::IncidentNodes& /*incidentNodes*/)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Edge not found: " << id);
    LinearElement* lePtr = as<LinearElement>(*objPtr);
    lePtr->setGeometry(polylineToGeom(geom));
    if (topoCache_) {
        topoCache_->updateObject(lePtr->id(), lePtr->geom());
    }
}

void TopoStorage::deleteEdge(topo::EdgeID id)
{
    auto objPtr = cache_.get(id);
    REQUIRE(objPtr, "Edge not found: " << id);
    LinearElement* lePtr = as<LinearElement>(*objPtr);
    lePtr->setState(GeoObject::State::Deleted);
    if (topoCache_) {
        topoCache_->deleteObject(lePtr->id());
    }
}

bool TopoStorage::nodeCanBeDeleted(topo::NodeID nodeId)
{
    auto obj = cache_.getExisting(nodeId);
    Junction* jc = as<Junction>(obj);
    auto relations = rangeToInfos(jc->masterRelations().range(RD_JC_INVOLVE_IN_COND_ROLES));
    return relations.empty();
}

bool TopoStorage::edgeCanBeDeleted(topo::EdgeID /*edgeId*/)
{
    return true;
}

topo::FacesByEdgeMap TopoStorage::facesByEdges(const topo::EdgeIDSet& edgeIds)
{
    TOIds ids = convertIds(edgeIds);
    const auto& contourDefs = cfg()->editor()->contourObjectsDefs();
    GeoObjectCollection elements;
    std::map<TOid, StringSet> elToRolesMap;
    for (const auto& el : cache_.get(ids)) {
        if (el->isDeleted()) {
            continue;
        }
        if (contourDefs.partType(el->categoryId()) == ContourObjectsDefs::PartType::LinearElement) {
            elements.add(el);
            elToRolesMap.insert({
                el->id(),
                StringSet{contourDefs.contourDef(el->categoryId()).contour.linearElement.roleId}});
        }
    }

    cache_.relationsManager().loadRelations(RelationType::Master, elToRolesMap);
    topo::FacesByEdgeMap result;
    for (const auto& el : elements) {
        topo::FaceIDSet faceIds;
        const auto& roleId = contourDefs.contourDef(el->categoryId()).contour.linearElement.roleId;
        for (const auto& rel : el->masterRelations().range(roleId)) {
            faceIds.insert(rel.id());
        }
        result.insert({el->id(), faceIds});
    }

    return result;
}

topo::FaceDiff TopoStorage::faceDiff(topo::FaceID faceId)
{
    auto face = cache_.getExisting(faceId);

    const auto& contourObjectsDefs = cfg()->editor()->contourObjectsDefs();
    REQUIRE(contourObjectsDefs.partType(face->categoryId()) ==
            ContourObjectsDefs::PartType::Contour,
        "Object " << faceId << " is not a contour");

    const ContourObjectsDefs::LinearElementDef& linearElementDef =
        contourObjectsDefs.contourDef(face->categoryId()).contour.linearElement;

    const auto& roleId = linearElementDef.roleId;

    TOIds addedElementIds = face->slaveRelations().diff(roleId).addedIds();
    TOIds removedElementIds = face->slaveRelations().diff(roleId).deletedIds();

    TOIds movedElementIds;

    auto movedElements = cache_.find([&] (const GeoObject* obj) {
        return is<LinearElement>(obj) &&
            obj->isModifiedLinksToSlaves() &&
            obj->categoryId() == linearElementDef.categoryId &&
            obj->masterRelations().range(roleId).contains(faceId);
    });
    for (const auto& obj : movedElements) {
        const auto id = obj->id();
        if (!addedElementIds.count(id) && !removedElementIds.count(id)) {
            movedElementIds.insert(id);
        }
    }

    return {
            topo::FaceIDSet(addedElementIds.begin(), addedElementIds.end()),
            topo::FaceIDSet(movedElementIds.begin(), movedElementIds.end()),
            topo::FaceIDSet(removedElementIds.begin(), removedElementIds.end())
        };
}

// dicentra TODO implement loaded() method for ranges and return not only for new objects
boost::optional<topo::EdgeIDSet> TopoStorage::tryGetFaceEdges(topo::FaceID faceId)
{
    auto face = cache_.getExisting(faceId);

    const auto& contourObjectsDefs = cfg()->editor()->contourObjectsDefs();
    REQUIRE(contourObjectsDefs.partType(face->categoryId()) ==
            ContourObjectsDefs::PartType::Contour,
        "Object " << faceId << " is not a contour");

    if (face->isCreated()) {
        const ContourObjectsDefs::LinearElementDef& linearElementDef =
            contourObjectsDefs.contourDef(face->categoryId()).contour.linearElement;

        const auto& roleId = linearElementDef.roleId;

        const auto& addedIds = face->slaveRelations().diff(roleId).addedIds();

        return topo::EdgeIDSet{addedIds.begin(), addedIds.end()};
    }

    return boost::none;
}

topo::EdgeIDSet TopoStorage::getFaceEdges(topo::FaceID faceId)
{
    auto face = cache_.getExisting(faceId);
    const auto& roleId = cfg()->editor()->contourObjectsDefs().contourDef(face->categoryId())
        .contour.linearElement.roleId;

    topo::EdgeIDSet edgeIds;
    for (const auto& rel : face->slaveRelations().range(roleId)) {
        edgeIds.insert(rel.id());
    }

    return edgeIds;
}

} // namespace wiki
} // namespace maps
