#include "saveobject.h"

#include "saveobject_operations.h"
#include "set_default_speed_limit.h"

#include "maps/wikimap/mapspro/services/editor/src/common.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/check_permissions.h"
#include "maps/wikimap/mapspro/services/editor/src/factory.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/moderation.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/context.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/line_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/relations_manager.h"
#include "maps/wikimap/mapspro/services/editor/src/geom.h"
#include "maps/wikimap/mapspro/services/editor/src/revisions_facade.h"
#include "maps/wikimap/mapspro/services/editor/src/tablevalues.h"
#include "maps/wikimap/mapspro/services/editor/src/actions/tools/stick_polygons.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/common.h"

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

#include <yandex/maps/wiki/configs/editor/restrictions.h>

#include <yandex/maps/wiki/common/json_helpers.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/master_role.h>
#include <yandex/maps/wiki/configs/editor/topology_groups.h>
#include <yandex/maps/wiki/topo/cache.h>
#include <yandex/maps/wiki/topo/editor.h>
#include <yandex/maps/wiki/topo/exception.h>

#include <maps/wikimap/mapspro/libs/acl/include/restricted_users.h>


#include <maps/libs/common/include/unique_ptr.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/json/include/builder.h>
#include <boost/optional/optional_io.hpp>
#include <contrib/libs/minizip/unzip.h>

namespace maps {
namespace wiki {

namespace {

const std::string TASK_METHOD_NAME = "SaveObject";

constexpr std::string_view DAE_FILE_EXT = ".dae";
constexpr std::string_view ZIP_FOURCC = "PK\003\004";

bool isZipData(std::string_view data) {
    return data.starts_with(ZIP_FOURCC);
}

bool zipContainsDaeFile(std::string_view data) {
    /*
     * The logic of this method was taken from kmlbase::ZipFile ctor.
     * Both cudos and curses go to libkml:
     * https://github.com/libkml/libkml/blob/master/src/kml/base/zip_file.cc#L89
     */
    zlib_filefunc_def api;
    auto memStream = mem_simple_create_file(
        &api,
        const_cast<void*>(static_cast<const void*>(data.data())),
        data.size()
    );
    ::maps::common::UniquePtr<void, unzClose> zfile(unzAttach(memStream, &api));
    if (!zfile) {
        return false;
    }

    char buf[1024];
    unz_file_info finfo;
    do {
        if (unzGetCurrentFileInfo(zfile.get(), &finfo, buf, sizeof(buf), 0, 0, 0, 0) != UNZ_OK) {
            continue;
        }
        if (std::string_view(buf).ends_with(DAE_FILE_EXT) &&
            finfo.uncompressed_size > 0)
        {
            return true;
        }
    } while (unzGoToNextFile(zfile.get()) == UNZ_OK);
    return false;
}

} // namespace

SaveObject::SaveObject(
        const ObserverCollection& observers,
        const Request& request,
        taskutils::TaskID asyncTaskID)
    : controller::BaseController<SaveObject>(BOOST_CURRENT_FUNCTION, asyncTaskID)
    , observers_(observers)
    , request_(request)
    , newLinearElementSourceIdGen_(0)
    , createdNewPrimaryObjects_(false)
{}

SaveObject::~SaveObject() = default;

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

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

namespace {
std::string
formatAsyncTaskMetadataJson(const UniqueId& uniqueId, bool addUUID, bool addPrimaryId)
{
    json::Builder json;
    json << [&](json::ObjectBuilder root) {
        root[STR_TASK] = TASK_METHOD_NAME;
        root[STR_GEO_OBJECTS] << [&](json::ObjectBuilder objects) {
            if (addUUID) {
                objects[STR_UUID] = uniqueId.uuid();
            }
            if (addPrimaryId && uniqueId.objectId()) {
                objects[STR_PRIMARY_OBJECT_ID] = common::idToJson(uniqueId.objectId());
            }
        };
    };
    return json.str();
}
}

controller::AsyncTaskControlData
SaveObject::asyncTaskControlData(const Request& request) {
    const auto& primaryUniqueId = request.objectsUpdateData.primaryUniqueId();
    DEBUG() << "SaveObject::asyncTaskControlData build uuid: " << primaryUniqueId;
    return primaryUniqueId && isValidUUID(primaryUniqueId->uuid())
        ? controller::AsyncTaskControlData {
            formatAsyncTaskMetadataJson(*primaryUniqueId, true, true),
            formatAsyncTaskMetadataJson(*primaryUniqueId, true, false),
            primaryUniqueId->objectId() ?
                formatAsyncTaskMetadataJson(*primaryUniqueId, false, true)
                : s_emptyString,
            request.dump() + " body:\n" + request.requestBody
        }
        : controller::AsyncTaskControlData();
}

void
SaveObject::control()
{
    auto branchCtx = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());

    WIKI_REQUIRE(
        !acl::isUserRestricted(branchCtx.txnCore(), request_.userId()),
        ERR_FORBIDDEN,
        "User " << request_.userId() << " is restricted");

    const auto& user = request_.userContext.aclUser(branchCtx);
    user.checkActiveStatus();

    const auto& moderationStatus = request_.userContext.moderationStatus(branchCtx);

    cfg()->rateLimiter().checkUserActivityAndRestrict(
        branchCtx,
        request_.userId(),
        social::ActivityType::Edits,
        moderationStatus);

    ASSERT(result_);
    result_->userId = request_.userId();
    if (request_.objectsUpdateData.isMultisave()) {
        auto resetController = [&]() {
            result_->collection.clear();
            newObjectsData_.clear();
            newLinearElementsData_.clear();
            modifiedGeomObjectIds_.clear();
            topologyEditContext_.reset();
        };
        std::vector<std::list<Observer::ContextDataPtr>> observersData;
        for (const auto& object : request_.objectsUpdateData) {
            const auto contextIt = request_.objectsEditContexts.find(object.id());
            curObjectsUpdateData_ = ObjectsUpdateDataCollection(object);
            curObjectsEditContexts_ = ObjectsEditContextsPtrMap {*contextIt};
            resetController();
            CommitContext commitContext {
                request_.feedbackTaskId
            };
            doEditAndSave(branchCtx, commitContext);
            observersData.push_back(observers_.beforeCommit(*result_->cache, request_.userContext, commitContext));
        }
        result_->token = result_->cache->commit();
        for (auto& observerData : observersData) {
            observers_.afterCommit(observerData, request_.branchId);
        }
    } else {
        curObjectsUpdateData_ = std::move(request_.objectsUpdateData);
        curObjectsEditContexts_ = std::move(request_.objectsEditContexts);
        CommitContext commitContext {
            request_.feedbackTaskId
        };
        doEditAndSave(branchCtx, commitContext);
        result_->token = observers_.doCommit(*result_->cache, request_.userContext, commitContext);
    }
}

void
SaveObject::doEditAndSave(
    maps::wiki::BranchContext& branchCtx,
    CommitContext& commitContext)
{
    std::make_shared<ObjectsCache>(branchCtx, boost::none).swap(result_->cache);
    Context context(*result_->cache, request_.userId());
    context.forceSave(request_.force);
    save(context);

    for (const auto& [uuid, contextPtr] : context.editContexts()) {
        if (!contextPtr->diffAlertsData().empty()) {
            commitContext.predefinedDiffAlerts.emplace(
                uuid.objectId(),
                contextPtr->diffAlertsData());
        }
    }
}

void
SaveObject::preliminaryGabaritsCheck(Context& context)
{
    CheckPermissions permissionsChecker(request_.userId(), context.cache().workCore());
    const auto& categories = cfg()->editor()->categories();
    for (const auto& objectUpdateData : curObjectsUpdateData_) {
        auto categoryId = objectUpdateData.categoryId();
        if (categoryId.empty()) {
            auto existingObject =
                result_->collection.getById(objectUpdateData.id().objectId());
            WIKI_REQUIRE(existingObject,
                ERR_MISSING_OBJECT,
                "Updating missing object.");
           categoryId = existingObject->categoryId();
        }
        const auto& category = categories[categoryId];
        if (objectUpdateData.isGeometryDefined() &&
            isLinearElement(categoryId))
        {
            const auto& restrictions = category.restrictions();
            const auto& gabarits = restrictions.gabarits();
            if (!gabarits || !gabarits->length()) {
                return;
            }
            const double newLen = objectUpdateData.geometry().realLength();
            WIKI_REQUIRE(
                newLen < *gabarits->length() * restrictions.maxCrossedElements() ||
                permissionsChecker.allowToIgnoreRestrictions(category),
                ERR_FORBIDDEN,
                "User uid: " << request_.userId() << " is not allowed to edit huge " << categoryId);
        }
    }
}

void
SaveObject::prepareObjectsDataForUpdate(Context& context)
{
    checkUpdateRevisionIds(curObjectsUpdateData_);
    loadExistingObjects(context);
    preliminaryGabaritsCheck(context);
    createNewObjects(context);
    auto objectIdToParentIdsMap =
        reversedObjectRelationsGraph(curObjectsUpdateData_);
    prepareObjectEditContexts(context, objectIdToParentIdsMap);
    collectNewLinearElementsData();
    for (const auto& objectUpdateData : curObjectsUpdateData_) {
        const auto& id = objectUpdateData.id();
        const auto& objectId = id.objectId();
        if (!objectId) {
            auto it = std::find_if(
                newLinearElementsData_.begin(),
                newLinearElementsData_.end(),
                [id] (const NewLinearElementsDataMap::value_type& v)
                    { return v.second.updateData->id() == id; });
            REQUIRE(
                it != newLinearElementsData_.end(),
                "Only new linear elements haven't got real object ids at this stage");
        } else {
            ObjectPtr object = result_->collection.getById(objectId);
            REQUIRE(object, "All object except new linear elements must be in update collection");
            object->primaryEdit(
                curObjectsUpdateData_.isPrimary(objectUpdateData.id()));
            newObjectsData_.insert({objectId, &objectUpdateData});
        }
    }
}

void
SaveObject::checkUpdateRevisionIds(
    const ObjectsUpdateDataCollection& objectsUpdateData)
{
    std::set<TRevisionId> revisionIds;
    for (const auto& objectUpdateData : objectsUpdateData) {
        const auto& revisionId = objectUpdateData.revision();
        if (revisionId.valid()) {
            revisionIds.insert(revisionId);
        }
        // objects from relatives diff are also present in collection
    }
    result_->cache->revisionsFacade().gateway().checkConflicts(revisionIds);
}

void
SaveObject::loadExistingObjects(Context& context)
{
    const ObjectsUpdateDataCollection& newData = curObjectsUpdateData_;
    TOIds objectIdsToLoad;
    for (const auto& objectUpdateData : newData) {
        const auto& revisionId = objectUpdateData.revision();
        if (revisionId.valid()) {
            objectIdsToLoad.insert(revisionId.objectId());
        }
    }
    auto collection = context.cache().get(objectIdsToLoad);
    WIKI_REQUIRE(
        collection.size() == objectIdsToLoad.size(),
        ERR_MISSING_OBJECT,
        "Some objects were not loaded");
    for (const auto& object : collection) {
        result_->collection.add(object);
    }
}

void
SaveObject::createNewObjects(Context& context)
{
    GeoObjectFactory factory(context.cache());
    ObjectsUpdateDataCollection& newData = curObjectsUpdateData_;
    std::map<UniqueId, UniqueId> oldToNewIdsMapping;
    for (const auto& objectUpdateData : newData) {
        if (objectUpdateData.revision().valid()) {
            continue;
        }
        WIKI_REQUIRE(
            !objectUpdateData.categoryId().empty(),
            ERR_BAD_DATA,
            "Category id not set for new object, id " << objectUpdateData.id());
        const auto& objectClass = GeoObjectFactory::objectClass(objectUpdateData.categoryId());
        if (objectClass.className == ObjectsClassInfos::linearElementClassInfo.className) {
            newLinearElementsData_.insert(
                {++newLinearElementSourceIdGen_,
                    {&objectUpdateData,
                     {},
                     curObjectsUpdateData_.isPrimary(objectUpdateData.id())}});
        } else {
            ObjectPtr object = factory.createNewObject(
                objectClass, objectUpdateData.categoryId());
            object->setAllModified();
            context.cache().add(object);
            result_->collection.add(object);
            oldToNewIdsMapping.insert(
                {objectUpdateData.id(), UniqueId(object->id())});
        }
        createdNewPrimaryObjects_ |= curObjectsUpdateData_.isPrimary(objectUpdateData.id());
    }
    // remap object ids
    for (const auto& idsPair : oldToNewIdsMapping) {
        newData.replaceId(idsPair.first, idsPair.second);
    }
    // remap object edit contexts
    for (const auto& idsPair : oldToNewIdsMapping) {
        auto contextIt = curObjectsEditContexts_.find(idsPair.first);
        if (contextIt == curObjectsEditContexts_.end()) {
            continue;
        }
        auto context = std::move(contextIt->second);
        curObjectsEditContexts_.erase(contextIt);
        curObjectsEditContexts_.insert({idsPair.second, std::move(context)});
    }
}

namespace {

bool
objectGeometryUpdated(const Geom& geom, const GeoObject* obj)
{
    return !obj || !obj->original() ||
        obj->original()->geom().isNull() ||
        !geom.equal(
            obj->original()->geom(),
            CALCULATION_TOLERANCE);
}

bool
containsUpdatedGeom(
    const ObjectUpdateData& objectUpdateData,
    const GeoObject* obj,
    const ObjectEditContext* editContext)
{
    if (!objectUpdateData.isGeometryDefined()) {
        return false;
    }
    return (editContext && (!editContext->splitPoints().empty()
            || !editContext->splitLines().empty()))
            || objectGeometryUpdated(objectUpdateData.geometry(), obj);
}
} // namespace

void
SaveObject::prepareObjectEditContexts(
    Context& context,
    const ObjectIdToParentIdsMap& objectIdToParentIdsMap)
{
    auto& editContexts = curObjectsEditContexts_;
    const ObjectsUpdateDataCollection& newData = curObjectsUpdateData_;
    for (const auto& objectUpdateData : newData) {
        UniqueId objId = objectUpdateData.id();
        ObjectEditContextPtr editContext = nullptr;
        auto contextIt = editContexts.find(objId);
        if (contextIt != editContexts.end()) {
            context.addEditContext(objId, contextIt->second);
            editContext = contextIt->second;
        } else {
            editContext = editContextFromParents(
                objId, objectIdToParentIdsMap, curObjectsEditContexts_);
            if (editContext) {
                context.addEditContext(objId, editContext);
            }
        }
        ObjectPtr obj = objId.objectId()
            ? result_->collection.getById(objId.objectId())
            : ObjectPtr();
        if (containsUpdatedGeom(objectUpdateData, obj.get(), editContext.get())) {
            if (objId.objectId()) {
                modifiedGeomObjectIds_.insert(objId.objectId());
            }
            WIKI_REQUIRE(
                editContext,
                ERR_BAD_DATA,
                "Edit context for geometry object " << objId
                    << " is not set neither for object nor for any of its parents");
            const auto& categoryId = objectUpdateData.categoryId();
            auto topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId);
            if (!topoGroup) {
                continue;
            }
            if (topologyEditContext_) {
                REQUIRE(
                    topologyEditContext_->topoGroup().isInGroup(categoryId),
                    "Geometry object of one topology group can only be edited at a time");
            } else {
                topologyEditContext_ = make_unique<TopologyEditContext>(
                    *topoGroup, context.cache(), newLinearElementsData_);
            }
        }
    }
}

ObjectEditContextPtr
SaveObject::editContextFromParents(
    const UniqueId& objectId,
    const ObjectIdToParentIdsMap& objectIdToParentIdsMap,
    const ObjectsEditContextsPtrMap& editContexts)
{
    UniqueIdSet visitedObjectIds = {objectId};
    UniqueIdSet currentLevelIds = {objectId};
    while (!currentLevelIds.empty()) {
        UniqueIdSet nextLevelIds;
        for (const auto& id : currentLevelIds) {
            for (const auto& parentId : objectIdToParentIdsMap.at(id)) {
                if (!visitedObjectIds.insert(parentId).second) {
                    continue;
                }
                auto it = editContexts.find(parentId);
                if (it != editContexts.end()) {
                    return it->second;
                }
                nextLevelIds.insert(parentId);
            }
        }
        currentLevelIds = std::move(nextLevelIds);
    }
    return nullptr;
}

void
SaveObject::collectNewLinearElementsData()
{
    for (const auto& objectUpdateData : curObjectsUpdateData_) {
        const auto& objectId = objectUpdateData.id().objectId();
        if (!objectId) {
            continue;
        }
        for (const auto& roleDiff : objectUpdateData.slavesDiff()) {
            for (auto id : roleDiff.second.added()) {
                auto it = std::find_if(
                    newLinearElementsData_.begin(),
                    newLinearElementsData_.end(),
                    [id] (const NewLinearElementsDataMap::value_type& v)
                        { return v.second.updateData->id() == id; });
                if (it != newLinearElementsData_.end()) {
                    it->second.mastersDiff[roleDiff.first].insert(objectId);
                }
            }
        }
    }
    for (auto& newLEDataPair : newLinearElementsData_) {
        NewLinearElementData& data = newLEDataPair.second;
        for (const auto& roleDiff : data.updateData->mastersDiff()) {
            const auto& roleId = roleDiff.first;
            const RelativesDiff& diffIds = roleDiff.second;
            for (auto addedId : diffIds.added()) {
                const auto& id = addedId.objectId();
                REQUIRE(id, "All LE master objects should have already been processed");
                data.mastersDiff[roleId].insert(id);
            }
        }
    }
}

std::string dump(std::map<unsigned long, const maps::wiki::ObjectUpdateData *> newObjectsData)
{
    return
        common::join(newObjectsData,
            [](const std::pair<unsigned long, const maps::wiki::ObjectUpdateData*> p) {
                return std::to_string(p.first);
            }, ",");
}

void
SaveObject::save(Context& context)
{
    CheckPermissions checkPermissions(request_.userId(), context.cache().workCore());
    for (const auto& [_, contextPtr] : context.editContexts()) {
        if(contextPtr->allowInvalidContours()) {
            checkPermissions.checkAccessToEditorMode();
            break;
        }
    }

    prepareObjectsDataForUpdate(context);
    ObjectSaver processor(newObjectsData_, context, topologyEditContext_.get());
    GeoObjectCollection objectsToSave;
    for (const auto& obj : result_->collection) {
        auto it = newObjectsData_.find(obj->id());
        REQUIRE(it != newObjectsData_.end(),
            "Object not found in update collection, id: " << obj->id());
        if (!it->second->revision().valid() || !it->second->isEmpty()) {
            objectsToSave.add(obj);
        }
    }
    result_->collection = std::move(objectsToSave);

    /// all objects
    for (auto& objPtr : result_->collection) {
        if (!is<Junction>(objPtr) && !is<LinearElement>(objPtr)) {
            objPtr->applyProcessor(processor);
        }
    }

    saveTopologicalObjects(processor);

    splitArealObjects(context);
    splitLineObjects(context);
    if (request_.stickPolygonsPolicy == StickPolygonsPolicy::On &&
        checkPermissions.isUserHasAccessToStickPolygons())
    {
        stickGeometries(context.cache(), request_.userId(), context.maxViewZoom());
    }

    createDefaultRelations(context, result_->collection);
    setDefaultSpeedLimit(context);
    resetPoiImportSourceWithPermalink(result_->collection);
    if (request_.isLocalPolicy == SaveObject::IsLocalPolicy::Manual) {
        checkPermissions.checkUserHasAccessToIsLocalManualPolicy();
    } else if (request_.branchId == revision::TRUNK_BRANCH_ID) {
        calculateNmIsLocalAndLang(context, result_->collection);
    }

    result_->collection.clear();
    result_->collection.append(
        context.cache().find(
            [] (const GeoObject* obj) -> bool { return obj->primaryEdit(); }));

    context.cache().saveWithContext(
        newObjectsData_,
        context.editContexts(),
        request_.userId(),
        createdNewPrimaryObjects_
            ? common::COMMIT_PROPVAL_OBJECT_CREATED
            : common::COMMIT_PROPVAL_OBJECT_MODIFIED,
        request_.commitAttributes);
}

namespace {
topo::Editor::EdgeData
edgeDataForLinearElement(
    const Geom& geom, TOid id, bool exists, const ObjectEditContext& editContext)
{
    const TZoom zoom = editContext.viewZoom();

    const auto antiMeridianGravityRadius =
        cfg()->editor()->system().antiMeridianGravityRadius(zoom);
    auto adjGeom = adjustToAntiMeridian(geom, antiMeridianGravityRadius);

    geolib3::PointsVector spoints(
        editContext.splitPoints().begin(), editContext.splitPoints().end());

    if (adjGeom) {
        for (auto& point : spoints) {
            point = adjustToAntiMeridian(adjGeom->id, point, antiMeridianGravityRadius);
        }
    }
    return {
        topo::SourceEdgeID{id, exists},
        spoints,
        geomToPolyline(adjGeom ? (*adjGeom).geom : geom)};
}

topo::Editor::NodeData
nodeDataForJunction(
    const GeoObject* jc, const Geom& newGeom, const ObjectEditContext& editContext)
{
    const TZoom zoom = editContext.viewZoom();
    geolib3::Point2 newPos = geomToPoint(newGeom);
    if (jc->original()) {
        newPos = adjustToAntiMeridian(
            geomToPoint(jc->geom()), newPos,
            cfg()->editor()->system().antiMeridianGravityRadius(zoom));
    }
    return {jc->id(), newPos};
}

} // namespace

void
SaveObject::saveTopologicalObjects(ObjectSaver& processor)
{
    topo::Editor::TopologyData updateData;
    const ObjectEditContext* commonEditContext = nullptr;

    auto getEditContext = [&] (const ObjectUpdateData& objectUpdateData) {
        const ObjectEditContext* editContext =
            processor.context().editContext(objectUpdateData.id());
        REQUIRE(editContext, "Edit context not set for " << objectUpdateData.id());
        if (commonEditContext) {
            REQUIRE(commonEditContext == editContext,
                "All edited objects must share one editContext");
        } else {
            commonEditContext = editContext;
        }
        return *editContext;
    };

    for (auto& objPtr : result_->collection) {
        if (!is<Junction>(objPtr)) {
            continue;
        }
        processor.processGeoObject(objPtr.get());
        if (!modifiedGeomObjectIds_.count(objPtr->id())) {
            continue;
        }
        const ObjectUpdateData& objectUpdateData = *(newObjectsData_.at(objPtr->id()));
        const auto& editContext = getEditContext(objectUpdateData);
        updateData.nodesData.push_back(
            nodeDataForJunction(objPtr.get(), objectUpdateData.geometry(), editContext));
    }

    for (auto& objPtr : result_->collection) {
        if (!is<LinearElement>(objPtr)) {
            continue;
        }
        processor.processGeoObject(objPtr.get());
        if (!modifiedGeomObjectIds_.count(objPtr->id())) {
            continue;
        }
        const ObjectUpdateData& objectUpdateData = *(newObjectsData_.at(objPtr->id()));
        const auto& editContext = getEditContext(objectUpdateData);
        updateData.edgesData.push_back(
            edgeDataForLinearElement(objectUpdateData.geometry(), objPtr->id(), true, editContext));
    }

    for (const auto& newLinearElementData : newLinearElementsData_) {
        const ObjectUpdateData& objectUpdateData = *newLinearElementData.second.updateData;
        const auto id = newLinearElementData.first;
        const auto& editContext = getEditContext(objectUpdateData);
        updateData.edgesData.push_back(
            edgeDataForLinearElement(objectUpdateData.geometry(), id, false, editContext));
    }

    if (updateData.nodesData.empty() && updateData.edgesData.empty()) {
        return;
    }

    REQUIRE(topologyEditContext_, "No topology edit context set");
    REQUIRE(commonEditContext, "No edit context set");
    TopologyEditContext* topoContext = topologyEditContext_.get();

    auto restrictions = topoContext->getRestrictions(commonEditContext->viewZoom());

    if (updateData.nodesData.size() == 1 && updateData.edgesData.empty()) {

        const topo::Editor::NodeData& nodeData = updateData.nodesData.front();

        restrictions.setMaxIntersectionsWithEdge(size_t(0));
        restrictions.setMaxIntersectedEdges(size_t(0));

        try {
            topoContext->editor().saveNode(nodeData, restrictions);
        } catch (const topo::GeomProcessingError& ex) {
            if (ex.errorCode() == topo::ErrorCode::TooManyIntersectionsWithElement ||
                ex.errorCode() == topo::ErrorCode::TooManyIntersectionsWithNetwork)
            {
                THROW_WIKI_LOGIC_ERROR(ERR_TOPO_CREATE_INTERSECTION_IS_NOT_PERMITTED,
                    "Attempt to create new intersections by moving junction " << nodeData.id);
            }
            throw;
        }
    } else if (updateData.nodesData.empty() && updateData.edgesData.size() == 1) {
        topoContext->editor().saveEdge(updateData.edgesData.front(), restrictions);
    } else {
        topoContext->editor().saveObjects(updateData, restrictions);
    }
}

namespace {
void
createObjectParts(const ObjectPtr& obj, const std::vector<Geom>& partsGeoms, ObjectsCache& cache)
{
    GeoObjectFactory factory(cache);
    const auto& category = obj->category();
    const auto& objectClass = GeoObjectFactory::objectClass(category.id());

    TOIds partsIds;
    for (const auto& geom : partsGeoms) {
        ObjectPtr newObject = factory.createNewObject(objectClass, category.id());
        newObject->cloneAttributes(&(*obj));
        newObject->setGeometry(geom);
        newObject->setAllModified();
        cache.add(newObject);
        partsIds.insert(newObject->id());
    }
    auto& relMan = cache.relationsManager();
    const auto& categories = cfg()->editor()->categories();
    for (const auto& masterRole : category.masterRoles()) {
        const auto& masterCat = categories[masterRole.categoryId()];
        if (!masterCat.slaveRole(masterRole.roleId()).keepAll()) {
            continue;
        }
        for (const auto& master : obj->masterRelations().range(masterRole.roleId())) {
            if (master.categoryId() !=  masterRole.categoryId()) {
                continue;
            }
            for (const auto partId : partsIds) {
                relMan.createRelation(master.id(), partId, masterRole.roleId());
            }
        }
    }
}
}//namespace

void
SaveObject::splitArealObjects(Context& context)
{
    for (auto& obj : result_->collection) {
        const auto it = newObjectsData_.find(obj->id());
        REQUIRE(it != newObjectsData_.end(),
                "Object not found in update collection, id: " << obj->id());
        if (!context.hasEditContext(it->second->id())) {
            continue;
        }
        const auto* editContext = context.editContext(it->second->id());
        if (editContext->splitLines().empty()) {
            continue;
        }
        WIKI_REQUIRE(!obj->geom().isNull(),
                     ERR_BAD_REQUEST,
                     "Object " << obj->id() << " has no geometry");
        WIKI_REQUIRE(is<ArealObject>(obj),
                     ERR_BAD_REQUEST,
                     "Object " << obj->id() << " is not areal");

        auto model3dRelations = obj->masterRelations().range(ROLE_ASSOCIATED_WITH);
        WIKI_REQUIRE(model3dRelations.empty(),
                     ERR_FORBIDDEN,
                     "Object " << obj->id() << " has a connected 3d model");

        std::vector<Geom> resultPolygons = obj->geom().splitPolyByLines(editContext->splitLines());
        REQUIRE(!resultPolygons.empty(), "Zero polygons after split");

        if (resultPolygons.size() < 2) {
            continue;
        }
        auto itrMaxArea = std::max_element(resultPolygons.begin(), resultPolygons.end(),
            [](const Geom& lhs, const Geom& rhs) -> bool
            {
                return lhs->getArea() < rhs->getArea();
            });

        obj->setGeometry(*itrMaxArea);
        obj->calcModified();
        resultPolygons.erase(itrMaxArea);
        createObjectParts(obj, resultPolygons, context.cache());
    }
}

void
SaveObject::splitLineObjects(Context& context)
{
    for (auto& obj : result_->collection) {
        if (!is<LineObject>(obj)) {
            continue;
        }
        const auto it = newObjectsData_.find(obj->id());
        REQUIRE(it != newObjectsData_.end(),
                "Object not found in update collection, id: " << obj->id());
        if (!context.hasEditContext(it->second->id())) {
            continue;
        }
        const auto* editContext = context.editContext(it->second->id());
        if (editContext->splitPoints().empty()) {
            continue;
        }
        WIKI_REQUIRE(!obj->geom().isNull(),
                     ERR_BAD_REQUEST,
                     "Object " << obj->id() << " has no geometry");

        std::vector<Geom> splitedGeoms = obj->geom().splitLineStringByPoints(
            editContext->splitPoints(),
            obj->geometryCompareTolerance());
        if (splitedGeoms.size() < 2) {
            continue;
        }
        auto itrMaxLen = std::max_element(splitedGeoms.begin(), splitedGeoms.end(),
            [](const Geom& lhs, const Geom& rhs) -> bool
            {
                return lhs->getLength() < rhs->getLength();
            });

        obj->setGeometry(*itrMaxLen);
        obj->calcModified();
        splitedGeoms.erase(itrMaxLen);
        createObjectParts(obj, splitedGeoms, context.cache());
    }
}

void
SaveObject::ObjectSaver::processGeoObject(GeoObject* obj)
{
    auto it = newObjects_.find(obj->id());
    REQUIRE(it != newObjects_.end(),
        "Object not found in update collection, id: " << obj->id());
    const ObjectUpdateData& objectUpdateData = *it->second;
    std::vector<RelationObject*> slaveRelationsToCopy;
    std::vector<RelationObject*> masterRelationsToCopy;
    if (obj->categoryId() != objectUpdateData.categoryId()) {
        DEBUG() << " Old categoryId: " << obj->categoryId()
                << " new categoryId: " << objectUpdateData.categoryId();
        const auto& oldCategory = obj->category();
        const auto& categories = cfg()->editor()->categories();
        WIKI_REQUIRE(
            categories.defined(objectUpdateData.categoryId()) &&
            categories[objectUpdateData.categoryId()].kind() == oldCategory.kind() &&
            !oldCategory.kind().empty(),
            ERR_WRONG_CATEGORY_SWITCHING,
            "Can't change object: " << obj->id() << " to category: " <<  objectUpdateData.categoryId());
        const auto& newCategory = categories[objectUpdateData.categoryId()];
        const auto slaveRoles = oldCategory.slaveRoleIds(roles::filters::IsNotTable);
        for (auto& rel : obj->slaveRelations().range(slaveRoles)) {
            if (newCategory.isSlaveRoleDefined(rel.roleId())) {
                slaveRelationsToCopy.push_back(rel.relation());
            }
        }
        const auto& newCategoryMasterRoles = newCategory.masterRoles();
        for (auto& rel : obj->masterRelations().range()) {
            for (const auto& newCategoryMasterRole : newCategoryMasterRoles) {
                if (newCategoryMasterRole.roleId() == rel.roleId()) {
                    masterRelationsToCopy.push_back(rel.relation());
                    break;
                }
            }
        }
        RelationsProcessor(context_.cache()).deleteAllRelations(obj);
        obj->attributes().clearValues();
    }
    obj->initAttributes(objectUpdateData.categoryId(),
        objectUpdateData.attributes(), objectUpdateData.tableAttributes());

    //Copy relations
    RelationsManager& relationsManager = context_.cache().relationsManager();
    for (const auto& slaveRel : slaveRelationsToCopy) {
        relationsManager.createRelation(obj->id(), slaveRel->slaveId(), slaveRel->role());
    }
    for (const auto& masterRel : masterRelationsToCopy) {
        relationsManager.createRelation(masterRel->masterId(), obj->id(), masterRel->role());
    }

    const auto& category = cfg()->editor()->categories()[obj->categoryId()];
    obj->setRichContent(objectUpdateData.richContent());
    if (obj->hasRichContent()) {
        WIKI_REQUIRE(
            !category.richContentType().empty(),
            ERR_BAD_REQUEST,
            "RichContentType isn't set for category " << obj->categoryId()
        );
    }

    writeTableAttributesToSlaveInfos(context_.cache(), obj->id());

    saveObjectRelations(obj, objectUpdateData.mastersDiff(), RelationType::Master);
    saveObjectRelations(obj, objectUpdateData.slavesDiff(), RelationType::Slave);
}

void
SaveObject::ObjectSaver::processArealObject(ArealObject* obj)
{
    processGeoObject(obj);
    saveObjectGeometry(obj);
}

void
SaveObject::ObjectSaver::processLineObject(LineObject* obj)
{
    processGeoObject(obj);
    saveObjectGeometry(obj);
}

void
SaveObject::ObjectSaver::processPointObject(PointObject* obj)
{
    processGeoObject(obj);
    saveObjectGeometry(obj);
}

void
SaveObject::ObjectSaver::processModel3dObject(Model3dObject* obj)
{
    //thegeorg@TODO: if any new context types are to be added,
    //it will be great to remove any rich-content dependent logic from this file
    //Rich Content type validation should be placed in validator

    //calling processComplexObject to initialize obj with received data
    //and process object relations (both slave and master)
    //
    //will set object richContent from XML
    processComplexObject(obj);

    WIKI_REQUIRE(
        obj->hasRichContent(),
        ERR_BAD_RICH_CONTENT,
        "Couldn't save model3d object without rich content");

    if (!obj->isModifiedRichContent()) {
        return;
    }

    std::string decodedData;
    try {
        decodedData = decodeBase64(*obj->richContent());
    } catch (...) {
        THROW_WIKI_LOGIC_ERROR(ERR_BAD_REQUEST, "Can't decode rich-content from base64");
    }

    WIKI_REQUIRE(
        isZipData(decodedData),
        ERR_BAD_RICH_CONTENT,
        "Received data isn't a kmz archive");

    WIKI_REQUIRE(
        zipContainsDaeFile(decodedData),
        ERR_BAD_RICH_CONTENT,
        "Can't extract dae file from kmz archive");
}

/**
 * Assumes only complex objects have table attributes.
 */
void
SaveObject::ObjectSaver::processComplexObject(ComplexObject* obj)
{
    processGeoObject(obj);
    // only complex objects may have topological slaves
    TopoRelationsProcessor topoProcessor(context_.cache());
    topoProcessor.updateSeqNums(obj);
}

void
SaveObject::ObjectSaver::processAttrObject(AttrObject* obj)
{
    processGeoObject(obj);
}

void
SaveObject::ObjectSaver::processRelationObject(RelationObject* obj)
{
    processGeoObject(obj);
}

void
SaveObject::ObjectSaver::saveObjectGeometry(GeoObject* obj)
{
    auto it = newObjects_.find(obj->id());
    REQUIRE(it != newObjects_.end(), "Object not found in update collection, id: " << obj->id());
    const ObjectUpdateData& objectUpdateData = *it->second;
    if (!objectUpdateData.isGeometryDefined()) {
        return;
    }
    const auto& systemCfg = cfg()->editor()->system();
    double antiMeridianGravity =
        systemCfg.antiMeridianGravityRadius(context_.editContext(objectUpdateData.id())->viewZoom());
    auto adjGeom = adjustToAntiMeridian(objectUpdateData.geometry(), antiMeridianGravity);
    const auto& geom = adjGeom ? (*adjGeom).geom : objectUpdateData.geometry();
    if (objectGeometryUpdated(geom, obj)) {
        obj->setGeometry(geom);
    }
}

void
SaveObject::ObjectSaver::saveObjectRelations(
    GeoObject* obj,
    const RelativesDiffByRoleMap& diff,
    RelationType relationType)
{
    RelationsManager& relationsManager = context_.cache().relationsManager();
    for (const auto& [roleId, diffIds] : diff) {
        for (auto addedId : diffIds.added()) {
            const auto& id = addedId.objectId();
            if (!id) {
                continue;
            }
            relationType == RelationType::Master
                ? relationsManager.createRelation(id, obj->id(), roleId)
                : relationsManager.createRelation(obj->id(), id, roleId);
        }
        for (const auto& removedId : diffIds.removed()) {
            const auto& id = removedId.objectId();
            if (!id) {
                continue;
            }
            relationType == RelationType::Master
                ? relationsManager.deleteRelation(id, obj->id(), roleId)
                : relationsManager.deleteRelation(obj->id(), id, roleId);
        }
    }
}

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

} // namespace wiki
} // namespace maps
