#include "create_intersections.h"

#include "maps/wikimap/mapspro/services/editor/src/common.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/object_update_data.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/linear_element.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/complex_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/areal_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/point_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/attr_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/relation_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/model_object.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/geom.h"
#include "maps/wikimap/mapspro/services/editor/src/tablevalues.h"

#include "maps/wikimap/mapspro/services/editor/src/topo_storage.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/topo_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/topo_edit_context.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/topo_relations_processor.h"
#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"

#include <yandex/maps/wiki/topo/exception.h>

#include <maps/libs/geolib/include/conversion.h>

namespace maps {
namespace wiki {

namespace {

static const std::string TASK_METHOD_NAME = "CreateIntersections";

} // namespace

CreateIntersections::CreateIntersections(
        const ObserverCollection& observers,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : controller::BaseController<CreateIntersections>(TASK_METHOD_NAME, asyncTaskID)
    , observers_(observers)
    , request_(request)
{}

CreateIntersections::~CreateIntersections()
{}

std::string
CreateIntersections::Request::dump() const
{
    std::stringstream ss;
    ss << " user: " << userId();
    ss << " branch: " << branchId;
    if (feedbackTaskId) {
        ss << " feedback-task-id: " << *feedbackTaskId;
    }
    return ss.str();
}

std::string
CreateIntersections::printRequest() const
{
    return request_.dump();
}


void
CreateIntersections::control()
{
    WIKI_REQUIRE(
        !request_.objectsUpdateData.empty(), ERR_BAD_DATA, "Empty request");

    auto branchCtx = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ASSERT(result_);
    std::make_shared<ObjectsCache>(branchCtx, boost::none).swap(result_->cache);
    Context context(*result_->cache, request_.userId());
    bool isDataModified = doWork(context);
    if (isDataModified) {
        CommitContext commitContext{request_.feedbackTaskId};
        result_->token =
            observers_.doCommit(*result_->cache, request_.userContext, commitContext);
    }
}

void
CreateIntersections::initTopologyEditContext(Context &context)
{
    for (auto& obj : request_.objectsUpdateData) {
        const auto* topoGroup =
            cfg()->editor()->topologyGroups().findGroup(obj.categoryId());
        REQUIRE(topoGroup, "Topology group not set for category: " << obj.categoryId());
        REQUIRE(
            topoGroup->linearElementsCategories().count(obj.categoryId()),
            "Category " << obj.categoryId() << " is not a category of linear element");
        if (topologyEditContext_) {
            REQUIRE(
                topologyEditContext_->topoGroup().isInGroup(obj.categoryId()),
                "Geometry object of one topology group can only be edited at a time");
        } else {
            topologyEditContext_ = make_unique<TopologyEditContext>(
                *topoGroup, context.cache(), newLinearElementsData_);
        }
        context.addEditContext(obj.id(), request_.editContext);
    }
    ASSERT(topologyEditContext_);
}

void
CreateIntersections::checkObjectRevisions(Context& /*context*/)
{
    std::map<TOid, const TopoObjectData*> existingObjectsData;
    for (const auto& topoObjectData : request_.objectsUpdateData) {
        const bool isNew = !topoObjectData.revision().valid();
        WIKI_REQUIRE(
            isNew, ERR_BAD_DATA, "Only new objects are currently supported");
    }
}

bool
CreateIntersections::doWork(Context& context)
{
    checkObjectRevisions(context);
    initTopologyEditContext(context);

    geolib3::PolylinesVector geoms;
    geoms.reserve(request_.objectsUpdateData.size());
    const ObjectEditContext& editContext = *request_.editContext;
    const TZoom zoom = editContext.viewZoom();
    for (const auto& topoObject : request_.objectsUpdateData) {
        auto adjGeom = adjustToAntiMeridian(
            topoObject.geometry(),
            cfg()->editor()->system().antiMeridianGravityRadius(zoom));
        geoms.push_back(geomToPolyline(adjGeom ? (*adjGeom).geom : topoObject.geometry()));
    }

    const auto restrictions = topologyEditContext_->getRestrictions(editContext.viewZoom());

    auto intersectionsResult =
        topologyEditContext_->editor().createIntersections(geoms, restrictions);
    for (const auto& splitResult : intersectionsResult.splitEdgeIds) {
        const auto splitObjectId = splitResult.first;
        const auto& partIds = splitResult.second;
        result_->splitObjects.insert(
            {splitObjectId, context.cache().get(partIds.begin(), partIds.end())});
    }

    REQUIRE(
        intersectionsResult.adjustedGeoms.size() == request_.objectsUpdateData.size(),
        "Invalid adjusted geoms size");
    auto adjIt = intersectionsResult.adjustedGeoms.begin();
    auto objIt = request_.objectsUpdateData.begin();
    for ( ; adjIt != intersectionsResult.adjustedGeoms.end(); ++adjIt, ++objIt) {
        const auto& obj = *objIt;
        result_->adjustedObjects.push_back(
            TopoObjectData(obj.id(), obj.revision(), obj.categoryId(), polylineToGeom(*adjIt)));
    }

    if (!result_->splitObjects.empty()) {
        for (const auto& id : result_->splitObjects) {
            context.cache().getExisting(id.first)->primaryEdit(true);
        }
    }

    auto filter = [] (const GeoObject* object) -> bool
    {
        return object->isModified();
    };

    GeoObjectCollection modifiedObjects = context.cache().find(filter);
    if (modifiedObjects.empty()) {
        return false;
    }

    context.cache().saveWithContext(
        {},
        context.editContexts(),
        request_.userId(),
        common::COMMIT_PROPVAL_ACTION_SERVICE);
    return true;
}

const std::string&
CreateIntersections::taskName()
{
    return TASK_METHOD_NAME;
}

} // namespace wiki
} // namespace maps
