#include "objects_update_union.h"


#include "maps/wikimap/mapspro/services/editor/src/collection.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "maps/wikimap/mapspro/services/editor/src/branch_helpers.h"
#include "maps/wikimap/mapspro/services/editor/src/objects_cache.h"
#include "maps/wikimap/mapspro/services/editor/src/actions/tools/stick_polygons.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/xml_parser.h"
#include "maps/wikimap/mapspro/services/editor/src/serialize/request_data.h"
#include "maps/wikimap/mapspro/services/editor/src/relations_manager.h"
#include "maps/wikimap/mapspro/services/editor/src/relations_processor.h"

#include <maps/libs/geolib/include/vector.h>
#include <geos/geom/Polygon.h>
#include <geos/operation/union/CascadedPolygonUnion.h>

namespace maps::wiki {
namespace {
const std::string TASK_METHOD_NAME = "ObjectsUpdateUnion";
const std::string ACTION_ID_UNION = "union";
const TZoom WORK_ZOOM = 23;

typedef geos::geom::Polygon* GeosPolygonPtr;

class ObjectsUpdateUnionRequestData : public RequestData
{
public:
    ObjectsUpdateUnionRequestData(common::FormatType format, const std::string& body, ObjectsCache& cache)
    {
        parse(format, body, cache);
    }

protected:
    void parseContextDataJson(const json::Value&, ObjectsCache&) override {}
    void parseContextDataXml(const maps::xml3::Node&, ObjectsCache&) override {}
};

void
moveRelations(const ObjectPtr& from, const ObjectPtr& to, ObjectsCache& cache)
{
    RelationsManager& relationsManager = cache.relationsManager();
    auto createRelation = [&](TOid masterId, TOid slaveId, const std::string& roleId) {
        if (!relationsManager.isRelationExists(masterId, slaveId, roleId)) {
            relationsManager.createRelation(masterId, slaveId, roleId);
        }
    };
    for (const auto& rel : from->masterRelations().range()) {
        createRelation(rel.id(), to->id(), rel.roleId());
    }
    for (const auto& rel : from->slaveRelations().range()) {
        createRelation(to->id(), rel.id(), rel.roleId());
    }
    RelationsProcessor processor(cache);
    processor.deleteAllRelations(from.get());
}

void
unionObjectsWithinClusters(
    GeoObjectCollection& objects,
    const std::vector<TOIds>& clusters,
    TUid userId,
    ObjectsCache& cache)
{
    for (const auto& cluster : clusters) {
        if (cluster.size() < 2) {
            continue;
        }
        GeoObjectCollection toStick;
        for (auto oid : cluster) {
            toStick.add(objects.getById(oid));
        }
        stickGeometries(toStick, std::nullopt, userId, cache.workCore(), WORK_ZOOM);
        TOid maxAreaOid = 0;
        double maxArea = 0;
        std::vector<geos::geom::Polygon*> polys;
        for (auto oid : cluster) {
            const auto& objGeom = objects.getById(oid)->geom();
            double area = objGeom->getArea();
            if (area > maxArea) {
                maxArea = area;
                maxAreaOid = oid;
            }
            polys.push_back(
                dynamic_cast<GeosPolygonPtr>(
                    const_cast<GeosGeometryPtr>(
                        objGeom.geosGeometryPtr())));
        }
        std::unique_ptr<geos::geom::Geometry> unionResult(
            geos::operation::geounion::CascadedPolygonUnion::Union(&polys));
        WIKI_REQUIRE(unionResult->getGeometryTypeId() == geos::geom::GEOS_POLYGON,
            ERR_RESULT_IS_NOT_POLYGON,
            "Union result is not single polygon object.");
        const auto& mainOnject = objects.getById(maxAreaOid);
        mainOnject->setGeometry(Geom(unionResult.release()));
        for (auto oid : cluster) {
            const auto& obj = objects.getById(oid);
            if (oid != maxAreaOid) {
                obj->setState(GeoObject::State::Deleted);
                moveRelations(obj, mainOnject, cache);
            }
        }
    }
}
} // namespace

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

std::string
ObjectsUpdateUnion::Request::dump() const
{
    std::stringstream ss;
    ss << " uid: " << userId();
    ss << " branch: " << branchId;
    ss << " body: " << body;
    return ss.str();
}

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

void
ObjectsUpdateUnion::control()
{
    auto branchContext = BranchContextFacade::acquireWrite(request_.branchId, request_.userId());
    ObjectsCache cache(
        branchContext,
        boost::none);
    ObjectsUpdateUnionRequestData requestData(request_.format, request_.body, cache);
    GeoObjectCollection objects = cache.get(requestData.oids());
    cfg()->editor()->objectsUpdateRestrictions().checkObjects(ACTION_ID_UNION, objects);
    for (const auto& oid : requestData.oids()) {
        ObjectPtr object = objects.getById(oid);
        WIKI_REQUIRE(object, ERR_MISSING_OBJECT,
            " Not found object id:" << oid);
        WIKI_REQUIRE(!object->geom().isNull(), ERR_BAD_REQUEST,
            " Object " << oid << " has no geometry");
        object->primaryEdit(true);
    }
    TOIds oidsSet(requestData.oids().begin(), requestData.oids().end());
    unionObjectsWithinClusters(objects, {oidsSet}, request_.userId(), cache);
    saveAndNotify(objects, cache);
}

void
ObjectsUpdateUnion::saveAndNotify(GeoObjectCollection& /*objectContainer*/, ObjectsCache& cache)
{
    cache.save(request_.userId(), common::COMMIT_PROPVAL_GROUP_UNITED);
    result_->taskName = taskName();
    result_->token = observers_.doCommit(cache, request_.userContext);
    result_->commit = cache.savedCommit();
}

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

} // namespace maps::wiki
