#include "diffalert_runner.h"
#include <maps/wikimap/mapspro/services/editor/src/configs/categories.h>
#include <maps/wikimap/mapspro/services/editor/src/configs/config.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/objects/relation_object.h>
#include <maps/wikimap/mapspro/services/editor/src/objects/category_traits.h>
#include <maps/wikimap/mapspro/services/editor/src/views/objects_query.h>
#include <maps/wikimap/mapspro/services/editor/src/commit.h>
#include <maps/wikimap/mapspro/services/editor/src/utils.h>

#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/common/string_utils.h>
#include <yandex/maps/wiki/diffalert/diff_context.h>
#include <yandex/maps/wiki/diffalert/object.h>
#include <yandex/maps/wiki/diffalert/snapshot.h>
#include <yandex/maps/wiki/diffalert/runner.h>
#include <maps/libs/common/include/profiletimer.h>

#include <sstream>

namespace maps {
namespace wiki {

namespace da = diffalert;

namespace {

const double BBOX_TOLERANCE = 0.001;

enum class SnapshotTime { Old, New };

bool
existsBeforeOrAfterCommit(const GeoObject& obj)
{
    return !obj.isDeleted() || obj.hasExistingOriginal();
}

class ObjectImplGeo : public da::Object
{
public:
    ObjectImplGeo(const GeoObject& geoObject, SnapshotTime snapshotTime)
        : geoObject_(geoObject)
        , isOld_(snapshotTime == SnapshotTime::Old)
    {
        if (isOld_) {
            ASSERT(geoObject_.hasExistingOriginal());
        } else {
            ASSERT(!geoObject_.isDeleted());
        }
    }

    da::TId id() const override { return geoObject_.id(); }
    const std::string& categoryId() const override { return geoObject_.categoryId(); }

    const GeoObject& geoObject() const
    {
        return isOld_ ? *geoObject_.original() : geoObject_;
    }

    da::AttrValue attr(const std::string& name) const override
    {
        auto hasColon = (name.find(':') != std::string::npos);
        auto attrName = hasColon ? name : categoryId() + ':' + name;

        const auto& attributes = geoObject().attributes();
        auto attr = attributes.find(attrName);
        return da::AttrValue(
                std::move(attrName),
                attr != attributes.end() ? attr->packedValues() : std::string());
    }

    const da::Geom& geom() const override
    {
        return geoObject().geom();
    }

    da::Relations loadMasterRelations() override
    {
        da::Relations masters;
        for (const auto& rel : geoObject_.masterRelations().range()) {
            masters.insert(da::Relation{
                    rel.id(), geoObject_.id(), rel.roleId(), rel.seqNum()});
        }
        if (isOld_) {
            auto diff = geoObject_.masterRelations().diff();
            for (const auto& rel : diff.added) {
                masters.erase(da::Relation{
                        rel.id(), geoObject_.id(), rel.roleId(), rel.seqNum()});
            }
            for (const auto& rel : diff.deleted) {
                masters.insert(da::Relation{
                        rel.id(), geoObject_.id(), rel.roleId(), rel.seqNum()});
            }
        }
        return masters;
    }
    da::Relations loadSlaveRelations() override
    {
        da::Relations slaves;
        for (const auto& rel : geoObject_.slaveRelations().range()) {
            slaves.insert(da::Relation{
                    geoObject_.id(), rel.id(), rel.roleId(), rel.seqNum()});
        }
        if (isOld_) {
            auto diff = geoObject_.slaveRelations().diff();
            for (const auto& rel : diff.added) {
                slaves.erase(da::Relation{
                        geoObject_.id(), rel.id(), rel.roleId(), rel.seqNum()});
            }
            for (const auto& rel : diff.deleted) {
                slaves.insert(da::Relation{
                        geoObject_.id(), rel.id(), rel.roleId(), rel.seqNum()});
            }
        }
        return slaves;
    }

    bool isFaceElement() const override
    {
        return isContourElementCategory(categoryId());
    }
    bool isFaceJunction() const override
    {
        return isContourJunctionCategory(categoryId());
    }

private:
    const GeoObject& geoObject_;
    bool isOld_;
};

class SnapshotImpl;
class ObjectImplView : public da::Object
{
public:
    ObjectImplView(views::ViewObject&& viewObject, SnapshotImpl& snapshot)
        : viewObject_(std::move(viewObject))
        , categoryId_(viewObject_.categoryId())
        , snapshot_(snapshot)
    { }

    da::TId id() const override { return viewObject_.id(); }
    const std::string& categoryId() const override
    {
        return categoryId_;
    }

    da::AttrValue attr(const std::string& name) const override
    {
        auto hasColon = (name.find(':') != std::string::npos);
        auto attrName = hasColon ? name : categoryId() + ':' + name;

        const auto& attributes = viewObject_.domainAttrs();
        auto attrIt = attributes.find(attrName);
        return da::AttrValue(
                std::move(attrName),
                attrIt != attributes.end() ? attrIt->second : std::string());
    }

    const da::Geom& geom() const override
    {
        return viewObject_.geom();
    }

    da::Relations loadMasterRelations() override;
    da::Relations loadSlaveRelations() override;

    bool isFaceElement() const override
    {
        return isContourElementCategory(categoryId());
    }
    bool isFaceJunction() const override
    {
        return isContourJunctionCategory(categoryId());
    }

private:
    views::ViewObject viewObject_;
    std::string categoryId_;
    SnapshotImpl& snapshot_;
};


class SnapshotImpl : public da::Snapshot
{
public:
    SnapshotImpl(ObjectsCache& cache, SnapshotTime snapshotTime)
        : cache_(cache)
        , isOld_(snapshotTime == SnapshotTime::Old)
    { }

    std::vector<ObjectPtr>
    objectsByIds(const da::TIds& ids) override
    {
        std::vector<ObjectPtr> objects;
        for (const auto& geoObj: cache_.get(ids)) {
            if (isOld_ && geoObj->hasExistingOriginal()) {
                objects.emplace_back(new ObjectImplGeo(*geoObj, SnapshotTime::Old));
            } else if (!geoObj->isDeleted()) {
                objects.emplace_back(new ObjectImplGeo(*geoObj, SnapshotTime::New));
            }
        }
        return objects;
    }

    std::vector<ObjectPtr>
    primitivesByEnvelope(
            const da::Envelope& envelope_,
            da::GeometryType geomType,
            const std::vector<std::string>& categoryIds) override
    {
        std::vector<ObjectPtr> objects;

        if (envelope_.isNull()) {
            return objects;
        }
        auto envelope = envelope_;
        envelope.expandBy(BBOX_TOLERANCE); // avoid problems with degenerate envelopes

        // View is already synchronized at the moment, because of this
        // we might need to exclude some objects.
        TOIds processedOids;
        if (isOld_) {
            auto envelopeGeomPtr = createGeom(envelope, SpatialRefSystem::Mercator);
            ASSERT(envelopeGeomPtr);
            auto filter = [&](const GeoObject* object)
            {
                auto changedViewGeometry =
                    object->isModifiedState() || object->isModifiedGeom();
                const auto& categoryId = object->categoryId();

                return changedViewGeometry &&
                    (categoryIds.empty() ||
                        std::find(
                            std::begin(categoryIds), std::end(categoryIds),
                            categoryId) != std::end(categoryIds));
            };
            for (const auto& object : cache_.find(filter)) {
                const auto& original = object->original();
                if (object->hasExistingOriginal() &&
                        original &&
                        !original->geom().isNull() &&
                        original->geom()->intersects(envelopeGeomPtr.get())) {
                    objects.emplace_back(new ObjectImplGeo(*object, SnapshotTime::Old));
                }
                processedOids.insert(object->id());
            }
        }

        auto& work = cache_.workView();
        views::ObjectsQuery query;
        query.addCondition(
            views::EnvelopeGeometryCondition(std::move(envelope)));
        if (!categoryIds.empty()) {
            query.addCondition(
                views::CategoriesCondition(work, {categoryIds.begin(), categoryIds.end()}));
        }

        auto branchId = cache_.branchContext().branch.id();
        for (auto&& viewObj : query.exec(work, branchId)) {
            if (!processedOids.count(viewObj.id())) {
                objects.emplace_back(new ObjectImplView(std::move(viewObj), *this));
            }
        }

        // TODO: perform filtering while fetching objects
        if (geomType != da::GeometryType::All) {
            std::string expectedGeomTypeName;
            switch (geomType) {
                case da::GeometryType::Point:
                    expectedGeomTypeName = Geom::geomTypeNamePoint;
                    break;
                case da::GeometryType::LineString:
                    expectedGeomTypeName = Geom::geomTypeNameLine;
                    break;
                case da::GeometryType::Polygon:
                    expectedGeomTypeName = Geom::geomTypeNamePolygon;
                    break;
                case da::GeometryType::All:
                    ASSERT(geomType != da::GeometryType::All);
            }

            objects.erase(
                std::remove_if(
                    objects.begin(), objects.end(),
                    [&](const ObjectPtr& p)
                    { return p->geom().geometryTypeName() != expectedGeomTypeName; }),
                objects.end());
        }

        return objects;
    }

private:
    ObjectsCache& cache_;
    bool isOld_;
};

class DiffContextImpl : public da::DiffContext
{
public:
    DiffContextImpl(
            TId oid, SnapshotImpl& oldSnapshot, SnapshotImpl& newSnapshot)
        : oid_(oid)
        , oldSnapshot_(oldSnapshot)
        , newSnapshot_(newSnapshot)
        , categoryChanged_(false)
        , attrsChanged_(false)
        , geomChanged_(false)
    {}

    TId oid_;
    std::unique_ptr<da::Object> oldObject_;
    std::unique_ptr<da::Object> newObject_;

    SnapshotImpl& oldSnapshot_;
    SnapshotImpl& newSnapshot_;

    bool categoryChanged_;
    bool attrsChanged_;
    bool geomChanged_;

    da::Relations relationsAdded_;
    da::Relations relationsDeleted_;

    da::Relations tableAttrsAdded_;
    da::Relations tableAttrsDeleted_;

    bool isLoaded() const { return oldObject_ || newObject_; }

    da::TId objectId() const override { return oid_; }
    const std::string& categoryId() const override
    {
        if (newObject_) {
            return newObject_->categoryId();
        }
        if (oldObject_) {
            return oldObject_->categoryId();
        }
        throw InternalErrorException()
            << "invalid diff context for object id: " << oid_;
    }

    da::OptionalObject oldObject() const override
    {
        return da::OptionalObject(oldObject_.get());
    }
    da::OptionalObject newObject() const override
    {
        return da::OptionalObject(newObject_.get());
    }

    da::Snapshot& oldSnapshot() const override { return oldSnapshot_; }
    da::Snapshot& newSnapshot() const override { return newSnapshot_; }

    bool categoryChanged() const override { return categoryChanged_; }
    bool attrsChanged() const override { return attrsChanged_; }
    bool geomChanged() const override { return geomChanged_; }

    const da::Relations& relationsAdded() const override
    {
        return relationsAdded_;
    }
    const da::Relations& relationsDeleted() const override
    {
        return relationsDeleted_;
    }

    const da::Relations& tableAttrsAdded() const override
    {
        return tableAttrsAdded_;
    }
    const da::Relations& tableAttrsDeleted() const override
    {
        return tableAttrsDeleted_;
    }

};

class DiffContextsBuilder
{
public:
    DiffContextsBuilder(
            const StringSet& geomPartCategories,
            const StringSet& geomPartRoles,
            ObjectsCache& cache,
            const GeoObjectCollection& modifiedObjects)
        : geomPartCategories_(geomPartCategories)
        , geomPartRoles_(geomPartRoles)
        , cache_(cache)
        , oldSnapshot_(cache_, SnapshotTime::Old)
        , newSnapshot_(cache_, SnapshotTime::New)
    {
        auto modified = modifiedObjects.find(
            [](const GeoObject* obj) {
                return existsBeforeOrAfterCommit(*obj);
            });
        for (const auto& obj : modified) {
            addModifiedObject(obj);
        }
        propagateGeomChanges();
    }

    const std::unordered_map<TId, DiffContextImpl>& contexts() const
    {
        return contextById_;
    }

private:
    DiffContextImpl& contextById(TId id);
    DiffContextImpl& addObjectContext(const GeoObject& object);

    void addModifiedObject(const ObjectPtr& obj);
    void propagateGeomChanges();

private:
    const StringSet& geomPartCategories_;
    const StringSet& geomPartRoles_;
    ObjectsCache& cache_;
    SnapshotImpl oldSnapshot_;
    SnapshotImpl newSnapshot_;

    std::unordered_map<TId, DiffContextImpl> contextById_;
};

DiffContextImpl&
DiffContextsBuilder::contextById(TId id)
{
    auto it = contextById_.find(id);
    if (it == contextById_.end()) {
        DiffContextImpl dd(id, oldSnapshot_, newSnapshot_);
        it = contextById_.insert(std::make_pair(id, std::move(dd))).first;
    }
    return it->second;
}

DiffContextImpl&
DiffContextsBuilder::addObjectContext(const GeoObject& obj)
{
    auto& context = contextById(obj.id());
    if (context.isLoaded()) {
        return context;
    }
    if (obj.hasExistingOriginal()) {
        context.oldObject_ = make_unique<ObjectImplGeo>(obj, SnapshotTime::Old);
    }
    if (!obj.isDeleted()) {
        context.newObject_ = make_unique<ObjectImplGeo>(obj, SnapshotTime::New);
    }
    return context;
}

void
DiffContextsBuilder::addModifiedObject(const ObjectPtr& obj)
{
    if (is<RelationObject>(obj)) {
        const auto& relObj = as<RelationObject>(obj);
        const auto& masterCat =
            cfg()->editor()->categories()[relObj->masterCategoryId()];
        const auto& role = masterCat.slaveRole(relObj->role());

        da::Relation relation{
            relObj->masterId(), relObj->slaveId(), relObj->role(), relObj->seqNum()};

        if (role.tableRow()) {
            if (!relObj->isDeleted()) {
                contextById(relObj->masterId()).tableAttrsAdded_.insert(relation);
            } else {
                contextById(relObj->masterId()).tableAttrsDeleted_.insert(relation);
            }
        } else {
            if (!relObj->isDeleted()) {
                contextById(relObj->masterId()).relationsAdded_.insert(relation);
                contextById(relObj->slaveId()).relationsAdded_.insert(relation);
            } else {
                contextById(relObj->masterId()).relationsDeleted_.insert(relation);
                contextById(relObj->slaveId()).relationsDeleted_.insert(relation);
            }

            if (role.geomPart()) {
                contextById(relObj->masterId()).geomChanged_ = true;
            }
        }
    } else if (!obj->category().system()) {
        auto& diffContext = addObjectContext(*obj);

        if (obj->isModifiedCategory() || diffContext.stateChanged()) {
            diffContext.categoryChanged_ = true;
        }

        if (obj->isModifiedAttr() || diffContext.stateChanged()) {
            diffContext.attrsChanged_ = true;
        }
        if ((obj->isModifiedGeom() || diffContext.stateChanged())
            && !obj->geom().isNull()) {
            diffContext.geomChanged_ = true;
        }
    }
}

void
DiffContextsBuilder::propagateGeomChanges()
{
    TOIds curGeomChanged;
    for (const auto& pair: contextById_) {
        const auto& diffContext = pair.second;
        const auto& catId = diffContext.categoryId();

        if ((diffContext.geomChanged() && geomPartCategories_.count(catId))
            || (diffContext.changed() && isContourCategory(catId))) {
            curGeomChanged.insert(diffContext.objectId());
        }
    }
    while (!curGeomChanged.empty()) {
        cache_.relationsManager().loadRelations(
                RelationType::Master, curGeomChanged, geomPartRoles_);
        auto coll = cache_.get(curGeomChanged);
        curGeomChanged.clear();

        for (const auto& obj : coll) {
            for (const auto& relInfo : obj->masterRelations().range(geomPartRoles_)) {
                const auto& masterCat =
                    cfg()->editor()->categories()[relInfo.categoryId()];
                const auto& role = masterCat.slaveRole(relInfo.roleId());
                if (role.geomPart()
                      && existsBeforeOrAfterCommit(*relInfo.relative())) {
                    auto& relDiffContext = addObjectContext(*relInfo.relative());
                    if (!relDiffContext.geomChanged()) {
                        relDiffContext.geomChanged_ = true;
                        curGeomChanged.insert(relInfo.id());
                    }
                }
            }
        }
    }
}

da::Relations ObjectImplView::loadMasterRelations()
{
    auto objects = snapshot_.objectsByIds({id()});
    if (objects.empty()) {
        return {};
    }
    return objects.front()->loadMasterRelations();
}

da::Relations ObjectImplView::loadSlaveRelations()
{
    auto objects = snapshot_.objectsByIds({id()});
    if (objects.empty()) {
        return {};
    }
    return objects.front()->loadSlaveRelations();
}

} // namespace

DiffAlertRunner::DiffAlertRunner()
{
    for (const auto& cat : cfg()->editor()->categories()) {
        for (const auto& role : cat.second.slavesRoles()) {
            if (role.geomPart()) {
                geomPartCategories_.insert(role.categoryId());
                geomPartRoles_.insert(role.roleId());
            }
        }
    }
}

std::vector<da::Message>
DiffAlertRunner::run(ObjectsCache& cache, const GeoObjectCollection& modifiedObjects) const
{
    ProfileTimer pt;
    DEBUG() << "diffalert: starting check...";

    DiffContextsBuilder contextsBuilder(geomPartCategories_, geomPartRoles_, cache, modifiedObjects);
    DEBUG() << "diffalert: contexts built in: " << pt.getElapsedTime();

    std::vector<da::Message> messages;
    for (auto& pair : contextsBuilder.contexts()) {
        auto& diffContext = pair.second;
        REQUIRE(diffContext.isLoaded(),
                "bad DiffContext for oid: " << diffContext.oid_);
        if (diffContext.changed()) {
            DEBUG() << "object id: " << diffContext.objectId()
                    << " category id: " << diffContext.categoryId()
                    << " changed, summary: " << da::ChangesPrinter{diffContext};

            auto objectMessages = runEditorChecks(diffContext);
            messages.reserve(messages.size() + objectMessages.size());
            for (auto& message : objectMessages) {
                messages.push_back(std::move(message));
            }
        }
    }

    DEBUG() << "diffalert: check ended in: " << pt.getElapsedTime();
    return messages;
}

} // namespace wiki
} // namespace maps
