#include "objects_deleter.h"

#include "maps/wikimap/mapspro/services/editor/src/objectvisitor.h"
#include "maps/wikimap/mapspro/services/editor/src/edit_options.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/objects/junction.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/linear_element.h"
#include "maps/wikimap/mapspro/services/editor/src/topo_storage.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/delete_junction_callback.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/delete_linear_element_callback.h"
#include "maps/wikimap/mapspro/services/editor/src/topological/topological_callbacks.h"
#include "maps/wikimap/mapspro/services/editor/src/relations_processor.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/attr_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/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 <yandex/maps/wiki/topo/editor.h>
#include <yandex/maps/wiki/topo/cache.h>

namespace maps {
namespace wiki {

class ObjectsDeleter::Impl : public ObjectProcessor
{
public:
    Impl(ObjectsCache& cache)
        : cache_(cache)
        , relationsProcessor_(cache)
    {}

    ObjectsCache& cache() const { return cache_; }

    void processGeoObject(GeoObject* obj) override;
    void processArealObject(ArealObject* obj) override;
    void processJunction(Junction* obj) override;
    void processLinearElement(LinearElement* obj) override;
    void processLineObject(LineObject* obj) override;
    void processPointObject(PointObject* obj) override;
    void processComplexObject(ComplexObject* obj) override;
    void processAttrObject(AttrObject* obj) override;
    void processRelationObject(RelationObject* obj) override;
    void processModel3dObject(Model3dObject* obj) override
    {
        processComplexObject(obj);
    }

private:
    struct TopoEditorHolder
    {
        TopoEditorHolder(const TopologyGroup& topoGroup, ObjectsCache& cache)
            : storage(topoGroup, cache)
            , topoCache(storage, CALCULATION_TOLERANCE)
            , editor(topoCache.editor())
        {
            editor->registerTopoCallback(
                std::unique_ptr<topo::TopoDeleteEdgeCallback>(
                    new TopoDeleteLinearElementCallback(cache)));
            editor->registerTopoCallback(
                std::unique_ptr<topo::TopoMoveEdgeCallback>(
                    new TopoMoveLinearElementCallback(cache)));
            editor->registerCallback(
                std::unique_ptr<topo::MergeEdgesCallback>(
                    new DeleteJunctionCallback(cache)));
            editor->registerCallback(
                std::unique_ptr<topo::DeleteEdgeCallback>(
                    new DeleteLinearElementCallback(cache, topoCache)));
        }

        TopoStorage storage;
        topo::Cache topoCache;
        std::unique_ptr<topo::Editor> editor;
    };

    topo::Editor& topoEditor(const std::string& categoryId)
    {
        auto it = categoryToTopoEditorHolder_.find(categoryId);
        if (it != categoryToTopoEditorHolder_.end()) {
            return *it->second->editor;
        }
        auto topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId);
        REQUIRE(topoGroup, "Topology group is not set, category id " << categoryId);
        topoEditorHolder_.emplace_back(*topoGroup, cache_);
        TopoEditorHolder* topoEditorHolder = &topoEditorHolder_.back();
        categoryToTopoEditorHolder_.insert({topoGroup->junctionsCategory(), topoEditorHolder});
        for (const auto& elementCat : topoGroup->linearElementsCategories()) {
            categoryToTopoEditorHolder_.insert({elementCat, topoEditorHolder});
        }
        return *(topoEditorHolder->editor);
    }

    ObjectsCache& cache_;
    RelationsProcessor relationsProcessor_;
    std::list<TopoEditorHolder> topoEditorHolder_;
    std::unordered_map<std::string, TopoEditorHolder*> categoryToTopoEditorHolder_;
};


void
ObjectsDeleter::Impl::processGeoObject(GeoObject* obj)
{
    relationsProcessor_.deleteAllRelations(obj);
    obj->setState(GeoObject::State::Deleted);
}

void
ObjectsDeleter::Impl::processArealObject(ArealObject* obj)
{
    processGeoObject(obj);
}

void
ObjectsDeleter::Impl::processAttrObject(AttrObject* obj)
{
    WIKI_REQUIRE(!obj->primaryEdit(), ERR_FORBIDDEN,
        "Can't delete attributes object directly oid: " << obj->id());
    processGeoObject(obj);
}

void
ObjectsDeleter::Impl::processRelationObject(RelationObject* obj)
{
    THROW_WIKI_LOGIC_ERROR(ERR_FORBIDDEN, "Can't delete relation directly oid: " << obj->id());
}

void
ObjectsDeleter::Impl::processLineObject(LineObject* obj)
{
    processGeoObject(obj);
}

void
ObjectsDeleter::Impl::processPointObject(PointObject* obj)
{
    processGeoObject(obj);
}

void
ObjectsDeleter::Impl::processJunction(Junction* obj)
{
    if (obj->isDeleted()) {
        return;
    }
    if (!obj->valency()) {
        processGeoObject(obj);
    } else {
        topoEditor(obj->categoryId()).mergeEdges(obj->id());
    }
}

void
ObjectsDeleter::Impl::processLinearElement(LinearElement* obj)
{
    if (!obj->isDeleted()) {
        topoEditor(obj->categoryId()).deleteEdge(obj->id());
    }
}

void
ObjectsDeleter::Impl::processComplexObject(ComplexObject* obj)
{
    processGeoObject(obj);
}


ObjectsDeleter::ObjectsDeleter(ObjectsCache& cache)
    : impl_(new Impl(cache))
{}

ObjectsDeleter::~ObjectsDeleter()
{}

bool
ObjectsDeleter::changeState(TUid uid, const TOIds& objectIds)
{
    auto objects = impl_->cache().get(objectIds);
    for (auto& obj : objects) {
        obj->primaryEdit(true);
    }

    bool stateChanged = false;
    for (auto& obj : objects) {
        WIKI_REQUIRE(
            canDelete(obj.get(), uid),
            ERR_INVALID_OPERATION,
            "User " << uid << " not allowed to delete object " << obj->id());
        if (!obj->isDeleted()) {
            obj->applyProcessor(*impl_);
            stateChanged = true;
        }
    }
    return stateChanged;
}

} // namespace wiki
} // namespace maps
