#include "geom_diff.h"

#include "maps/wikimap/mapspro/services/editor/src/configs/config.h"
#include "maps/wikimap/mapspro/services/editor/src/objects/category_traits.h"
#include "maps/wikimap/mapspro/services/editor/src/commit.h"
#include "commit_diff_data_provider.h"

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

namespace maps {
namespace wiki {

namespace {

const std::unordered_map<std::string, DiffScope> ACTION_DIFF_SCOPE
{
    {common::COMMIT_PROPVAL_GROUP_MOVED, DiffScope::Commit},
    {common::COMMIT_PROPVAL_GROUP_DELETED, DiffScope::Commit},
    {common::COMMIT_PROPVAL_GROUP_UNITED, DiffScope::Commit},
    {common::COMMIT_PROPVAL_GROUP_MODIFIED_ATTRIBUTES, DiffScope::GroupUpdateAttributes},
    {common::COMMIT_PROPVAL_GROUP_MODIFIED_RELATION, DiffScope::GroupUpdateRelation},
    {common::COMMIT_PROPVAL_GROUP_MODIFIED_RELATIONS, DiffScope::GroupUpdateRelations},
};

bool isAllGeomsScope(DiffScope scope)
{
    return
        scope == DiffScope::GroupUpdateAttributes ||
        scope == DiffScope::GroupUpdateRelations;
}

bool isPoiUpdateRelations(
    DiffScope scope,
    const TOIds& allCommitedObjectIds,
    const CommitDiffDataProvider& commitDataProvider)
{
    if (!common::isIn(
            scope,
            {DiffScope::GroupUpdateRelations, DiffScope::Object}))
    {
        return false;
    }
    for (auto geomOid : allCommitedObjectIds) {
        if (!isPoi(commitDataProvider.objectCategory(geomOid).id())) {
            return false;
        }
    }
    return true;
}

} // namespace

DiffScope
commitDiffScope(const revision::Commit& commit, TOid objectId)
{
    const auto& commitAction = commit.action();
    if (commitAction == common::COMMIT_PROPVAL_GROUP_MODIFIED_RELATION) {
        return DiffScope::GroupUpdateRelation;
    }

    auto diffScope = DiffScope::Object;
    if (hasEditNotes(commit)) {
        if (common::COMMIT_NON_GROUP_ACTIONS.count(commitAction)) {
            diffScope = DiffScope::Commit;
        } else if (!objectId) {
            auto it = ACTION_DIFF_SCOPE.find(commitAction);
            if (it != ACTION_DIFF_SCOPE.end()) {
                diffScope = it->second;
            }
        }
    }
    return diffScope;
}

class GeomDiffBuilder
{
public:
    explicit GeomDiffBuilder(const CommitDiffDataProvider& commitDataProvider)
        : commitDataProvider_(commitDataProvider)
    {};

    GeomDiff geometryUpdates(
        TOid oid,
        const TOIds& allCommitedObjectIds,
        DiffScope scope) const;

    GeomDiff geometryUpdates(
        TOid oid,
        const TOIds& allCommitedObjectIds,
        DiffScope scope,
        const geos::geom::Envelope& viewPort,
        size_t maxDiffElements) const;

private:
    bool isSimpleGeomObject(TOid oid) const;
    bool isJunctionObject(TOid oid) const;
    bool isLinearElementObject(TOid oid) const;
    bool isDirectGeomMasterObject(TOid oid) const;
    bool isIndirectGeomMasterObject(TOid oid) const;

    void geometryUpdates(
        GeomDiff& geomDiff,
        TOid oid,
        const TOIds& allCommitedObjectIds,
        DiffScope scope) const;

    const CommitDiffDataProvider& commitDataProvider_;
};

GeomDiff::GeomDiff()
    : maxDiffElementsNum_(DEFAULT_GEOM_DIFF_PARTS_LIMIT)
    , limitExceeded_(false)
{
}

GeomDiff::GeomDiff(size_t maxDiffElementsNum, const geos::geom::Envelope& viewPort)
    : maxDiffElementsNum_(maxDiffElementsNum)
    , limitExceeded_(false)
    , viewPort_(viewPort)
{
}

size_t
GeomDiff::numPoints() const
{
    size_t numPointsInAllGeoms = 0;
    auto sum = [&](const std::vector<Geom>& geoms) {
        for (const auto& geom : geoms) {
            numPointsInAllGeoms += geom->getNumPoints();
        }
    };
    sum(before_);
    sum(after_);
    return numPointsInAllGeoms;
}

size_t
GeomDiff::simplify(double tolerance)
{
    size_t numPointsInAllGeoms = 0;
    auto simplifyAll = [&](std::vector<Geom>& geoms) {
        for (auto& geom : geoms) {
            geom.simplify(tolerance);
            numPointsInAllGeoms += geom->getNumPoints();
        }
    };
    simplifyAll(before_);
    simplifyAll(after_);
    return numPointsInAllGeoms;
}

bool
GeomDiffBuilder::isSimpleGeomObject(TOid oid) const
{
    return isSimpleGeomCategory(commitDataProvider_.objectCategory(oid));
}

bool
GeomDiffBuilder::isJunctionObject(TOid oid) const
{
    return isJunction(commitDataProvider_.objectCategory(oid).id());
}

bool
GeomDiffBuilder::isLinearElementObject(TOid oid) const
{
    return isLinearElement(commitDataProvider_.objectCategory(oid).id());
}

bool
GeomDiffBuilder::isDirectGeomMasterObject(TOid oid) const
{
    return isDirectGeomMasterCategory(commitDataProvider_.objectCategory(oid));
}

bool
GeomDiffBuilder::isIndirectGeomMasterObject(TOid oid) const
{
    return isIndirectGeomMasterCategory(commitDataProvider_.objectCategory(oid));
}

GeomDiff
GeomDiffBuilder::geometryUpdates(
    TOid oid,
    const TOIds& allCommitedObjectIds,
    DiffScope scope) const
{
    GeomDiff diff;
    try {
        geometryUpdates(diff, oid, allCommitedObjectIds, scope);
    } catch (const DiffLimitExceeded& ex) {
        diff.setLimitExceeded();
    }
    return diff;
}

GeomDiff
GeomDiffBuilder::geometryUpdates(
    TOid oid,
    const TOIds& allCommitedObjectIds,
    DiffScope scope,
    const geos::geom::Envelope& viewPort,
    size_t maxDiffElements) const
{
    GeomDiff diff(maxDiffElements, viewPort);
    try {
        geometryUpdates(diff, oid, allCommitedObjectIds, scope);
    } catch (const DiffLimitExceeded& ex) {
        diff.setLimitExceeded();
    }
    return diff;
}

void
GeomDiffBuilder::geometryUpdates(
    GeomDiff& diff,
    TOid requestOid,
    const TOIds& allCommitedObjectIds,
    DiffScope scope) const
{
    TOIds addedObjectsToBefore;
    TOIds addedObjectsToAfter;

    auto addObjectGeoms = [&](TOid oid) {
        if (!addedObjectsToBefore.count(oid)) {
            diff.tryAddBefore(commitDataProvider_.oldGeometry(oid));
            addedObjectsToBefore.insert(oid);
        }
        if (!addedObjectsToAfter.count(oid)) {
            diff.tryAddAfter(commitDataProvider_.newGeometry(oid));
            addedObjectsToAfter.insert(oid);
        }
    };

    auto addGeomByRelation = [&](const RelData& relation, TOid geomOid) {
        if (relation.removed) {
            if (!addedObjectsToBefore.count(geomOid)) {
                diff.tryAddBefore(commitDataProvider_.oldGeometry(geomOid));
                addedObjectsToBefore.insert(geomOid);
            }
        } else {
            if (!addedObjectsToAfter.count(geomOid)) {
                diff.tryAddAfter(commitDataProvider_.newGeometry(geomOid));
                addedObjectsToAfter.insert(geomOid);
            }
        }
    };

    if (isPoiUpdateRelations(scope, allCommitedObjectIds, commitDataProvider_)) {
        if (!requestOid) {
            // Unchanged master objects must come first
            for (const auto& relation : commitDataProvider_.modifiedRelations()) {
                if (isSimpleGeomObject(relation.masterId)) {
                    addObjectGeoms(relation.masterId);
                }
            }
            for (const auto& relation : commitDataProvider_.modifiedRelations()) {
                if (isSimpleGeomObject(relation.slaveId)) {
                    addGeomByRelation(relation, relation.slaveId);
                }
            }
            return;
        }

        addObjectGeoms(requestOid);
        auto catId = commitDataProvider_.objectCategory(requestOid).id();
        for (const auto& relation : commitDataProvider_.modifiedRelations()) {
            auto geomOid =
                (catId == CATEGORY_ENTRANCE)
                    ? relation.masterId
                    : relation.slaveId;
            if (isSimpleGeomObject(geomOid)) {
                addGeomByRelation(relation, geomOid);
            }
        }
        return;
    }

    const auto& geomModifiedObjects = commitDataProvider_.geomModifiedObjects();
    // Commit scope - all add modified geometries
    if (scope == DiffScope::Commit) {
        if (requestOid && isSimpleGeomObject(requestOid) &&
            !(isJunctionObject(requestOid) || isLinearElementObject(requestOid)))
        {
            addObjectGeoms(requestOid);
            if (isPoi(commitDataProvider_.objectCategory(requestOid).id())) {
                for (auto geomOid : geomModifiedObjects) {
                    addObjectGeoms(geomOid);
                }
                for (const auto& relation : commitDataProvider_.modifiedRelations()) {
                    if (isSimpleGeomObject(relation.slaveId)) {
                        addGeomByRelation(relation, relation.slaveId);
                    }
                }
            }
        } else {
            for (auto geomOid : geomModifiedObjects) {
                addObjectGeoms(geomOid);
            }
        }
    // Some GroupUpdate Actions scope - take all (unchanged) geometries
    } else if (isAllGeomsScope(scope)) {
        for (auto geomOid : allCommitedObjectIds) {
            addObjectGeoms(geomOid);
        }
    }

    // We've gone from feed while observing group operation
    if (!requestOid || scope == DiffScope::GroupUpdateRelation) {
        // Unchanged master object for ObjectsUpdateRelation must come first
        if (requestOid) {
            addObjectGeoms(requestOid);
        }
        const auto& modifiedRelations = commitDataProvider_.modifiedRelations();
        for (const auto& relation : modifiedRelations) {
            if (isSimpleGeomObject(relation.slaveId)) {
                addGeomByRelation(relation, relation.slaveId);
            }
        }
        return;
    }

    // Simple object with own geometry - just take it
    if (isSimpleGeomObject(requestOid)) {
        addObjectGeoms(requestOid);
        return;
    }

    // Check all objects with modified geometries if they are
    // direct parts of target requestOid (linear, contour, turn condition)
    // or indirectly contained by requestOid object (contour object)
    // and store modified geoms in respective containers
    bool requestOidIsDirectGeomMasterObject = isDirectGeomMasterObject(requestOid);
    bool requestOidIsIndirectGeomMasterObject = isIndirectGeomMasterObject(requestOid);
    if (scope == DiffScope::Object) {
        for (auto geomOid : geomModifiedObjects) {
            if ((requestOidIsDirectGeomMasterObject && commitDataProvider_.isSlaveOf(geomOid, requestOid))
                ||
                (requestOidIsIndirectGeomMasterObject && commitDataProvider_.isIndirectSlaveOf(geomOid, requestOid)))
            {
                addObjectGeoms(geomOid);
            }
        }
    }
    const auto& modifiedRelations = commitDataProvider_.modifiedRelations();
    // Check modified relations if they add/remove geometry objects to target
    // (linear, contour, turn condition, and contour belonging to contour object)
    for (const auto& relation : modifiedRelations) {
        if (
            ((relation.masterId == requestOid && requestOidIsDirectGeomMasterObject)
                ||
                (requestOidIsIndirectGeomMasterObject &&
                    commitDataProvider_.isSlaveOf(relation.masterId, requestOid)
                ))
            && isSimpleGeomObject(relation.slaveId))
        {
            addGeomByRelation(relation, relation.slaveId);
        }
    }
    if (requestOidIsIndirectGeomMasterObject) {
        // Check modified relations if they add/remove contours to target contour object
        for (auto contourRelation : modifiedRelations) {
            if (contourRelation.masterId != requestOid || !isDirectGeomMasterObject(contourRelation.slaveId)) {
                continue;
            }
            // Scan relation updates if they update added/removed contour contents
            for (auto elementRelation : modifiedRelations) {
                if (contourRelation.slaveId == elementRelation.masterId && isSimpleGeomObject(elementRelation.slaveId)) {
                    addGeomByRelation(elementRelation, elementRelation.slaveId);
                }
            }
            // For contour add it's entire contents to the changes of master object
            for (auto elementOid : commitDataProvider_.objectGeomSlaves(contourRelation.slaveId)) {
                addGeomByRelation(contourRelation, elementOid);
            }
        }
    }
}

GeomDiff
objectGeomDiff(
    const std::unique_ptr<ObjectsCache>& prevCache,
    ObjectsCache& curCache,
    const boost::optional<revision::CommitDiff>& commitDiff,
    TOid objectId,
    const TOIds& allCommitedObjectIds,
    DiffScope scope,
    const geos::geom::Envelope& viewPort,
    size_t maxDiffElements)
{
    const CommitDiffDataProvider commitDataProvider(prevCache, curCache, commitDiff);
    GeomDiffBuilder builder(commitDataProvider);
    return builder.geometryUpdates(objectId, allCommitedObjectIds, scope, viewPort, maxDiffElements);
}

GeomDiff
objectGeomDiff(
    const std::unique_ptr<ObjectsCache>& prevCache,
    ObjectsCache& curCache,
    const boost::optional<revision::CommitDiff>& commitDiff,
    TOid objectId,
    const TOIds& allCommitedObjectIds,
    DiffScope scope)
{
    const CommitDiffDataProvider commitDataProvider(prevCache, curCache, commitDiff);
    GeomDiffBuilder builder(commitDataProvider);
    return builder.geometryUpdates(objectId, allCommitedObjectIds, scope);
}

geos::geom::Envelope changesEnvelope(const GeomDiff& geomDiff)
{
    geos::geom::Envelope envelope;
    for (const auto& geom : geomDiff.before()) {
        envelope.expandToInclude(geom->getEnvelopeInternal());
    }
    for (const auto& geom : geomDiff.after()) {
        envelope.expandToInclude(geom->getEnvelopeInternal());
    }
    return envelope;
}

geos::geom::Envelope changesEnvelope(const GeoObject* fromObj, const GeoObject* toObj)
{
    geos::geom::Envelope envelope;
    if (fromObj) {
        const auto objEnv = fromObj->envelope();
        envelope.expandToInclude(&objEnv);
    }
    if (toObj) {
        const auto objEnv = toObj->envelope();
        envelope.expandToInclude(&objEnv);
    }
    return envelope;
}

} // namespace wiki
} // namespace maps
