#include "objects_cache.h"
#include "relations_processor.h"
#include "objects/relation_object.h"
#include "objects/linear_element.h"
#include "objects/junction.h"
#include "objects/point_object.h"
#include "objects/areal_object.h"
#include "objects/complex_object.h"
#include "objects/attr_object.h"
#include "revisions_facade.h"
#include "runtime_data.h"
#include "configs/config.h"
#include "relation_infos.h"
#include "factory.h"
#include "relations_manager.h"
#include "validator.h"
#include "commit.h"
#include "geom.h"
#include "branch_helpers.h"
#include "srv_attrs/registry.h"
#include "shadow_attributes.h"
#include "revision_meta/bbox_provider.h"

#include <maps/wikimap/mapspro/libs/acl/include/aclgateway.h>
#include <yandex/maps/wiki/configs/editor/categories.h>
#include <yandex/maps/wiki/configs/editor/slave_role.h>
#include <yandex/maps/wiki/configs/editor/topology_groups.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/commit.h>

#include <maps/libs/geolib/include/contains.h>
#include <maps/libs/geolib/include/intersection.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/polygon.h>

#include <algorithm>

namespace maps {
namespace wiki {

ObjectsCache::ObjectsCache(
        const BranchContext& branchContext,
        boost::optional<TCommitId> headCommit,
        const CachePolicy& policy)
    : branchContext_(make_unique<BranchContext>(branchContext))
    , policy_(policy)
{
    runtimeDataCalculator_ = make_unique<RuntimeDataCalculator>(*this);
    relationsManager_ = make_unique<RelationsManager>(*this);

    revisionsFacade_ = make_unique<RevisionsFacade>(
        make_unique<GeoObjectFactory>(*this),
        workCore(),
        branchContext_->branch,
        headCommit);
    bboxProvider_ = make_unique<BBoxProvider>(*this);
}

ObjectsCache::~ObjectsCache()
{}

TCommitId
ObjectsCache::headCommitId() const
{
    return revisionsFacade().headCommit();
}

RevisionsFacade&
ObjectsCache::revisionsFacade() const
{
    ASSERT(revisionsFacade_);
    return *revisionsFacade_;
}

RelationsManager&
ObjectsCache::relationsManager()
{
    ASSERT(relationsManager_);
    return *relationsManager_;
}

BBoxProvider&
ObjectsCache::bboxProvider()
{
    ASSERT(bboxProvider_);
    return *bboxProvider_;
}

TRevisionId
ObjectsCache::newObjectId()
{
    return revisionsFacade().newObjectId();
}

ObjectPtr
ObjectsCache::cacheObject(ObjectPtr obj)
{
    auto it = cachedObjects_.insert(std::make_pair(obj->id(), obj)).first;
    ASSERT(it != cachedObjects_.end());
    return it->second;
}

boost::optional<ObjectPtr>
ObjectsCache::get(TOid id)
{
    auto it = cachedObjects_.find(id);
    if (it != cachedObjects_.end()) {
        return it->second;
    }
    auto objectOpt = revisionsFacade_->load(id);
    if (!objectOpt) {
        return boost::none;
    }
    ObjectPtr loaded = cacheObject(*objectOpt);
    (*runtimeDataCalculator_)(loaded);
    //!TODO: Reimplement original using simple data structure,
    //not GeoObject
    ASSERT(loaded->original());
    loaded->original()->tableAttributes() = loaded->tableAttributes();
    return loaded;
}

ObjectPtr
ObjectsCache::getExisting(TOid id)
{
    auto objectOpt = get(id);
    WIKI_REQUIRE(objectOpt, ERR_MISSING_OBJECT,
        "Object with id: " << id << " not found in database");
    return *objectOpt;
}

GeoObjectCollection
ObjectsCache::get(const TOIds& ids)
{
    GeoObjectCollection res;
    TOIds toLoad;
    for (auto id : ids) {
        auto it = cachedObjects_.find(id);
        if (it != cachedObjects_.end()) {
            res.add(it->second);
        } else {
            toLoad.insert(id);
        }
    }
    if (!toLoad.empty()) {
        GeoObjectCollection loaded = revisionsFacade_->load(toLoad);
        DEBUG() << "Before runtime data calculator";
        for (auto objPtr : loaded) {
            cacheObject(objPtr);
        }
        (*runtimeDataCalculator_)(loaded);
        for (auto object: loaded) {
            ASSERT(object->original());
            object->original()->tableAttributes() = object->tableAttributes();
        }
        res.append(std::move(loaded));
    }
    return res;
}

ObjectPtr
ObjectsCache::add(ObjectPtr obj)
{
    auto it = cachedObjects_.find(obj->id());
    if (it == cachedObjects_.end()) {
        return cacheObject(obj);
    }
    return it->second;
}

GeoObjectCollection
ObjectsCache::add(const GeoObjectCollection& objects)
{
    GeoObjectCollection res;
    for (auto objectPtr : objects) {
        res.add(add(objectPtr));
    }
    return res;
}

GeoObjectCollection
ObjectsCache::find(const ObjectPredicate& predicate) const
{
    GeoObjectCollection res;
    for (const auto& objPair : cachedObjects_) {
        if (predicate(objPair.second.get())) {
            res.add(objPair.second);
        }
    }
    return res;
}

namespace {

void
propagateSrvAttrUpdate(GeoObjectCollection& modified, ObjectsCache& cache)
{
    GeoObjectCollection affected;
    for (auto it = modified.begin(); it != modified.end(); ++it) {
        (*it)->requestPropertiesUpdate();
        affected.append(srv_attrs::affectedDependentObjects((*it).get(), cache));
    }
    modified.append(affected);
}

bool
isJunctionCategory(const std::string& categoryId)
{
    const auto* topoGroup = cfg()->editor()->topologyGroups().findGroup(categoryId);
    return topoGroup && topoGroup->junctionsCategory() == categoryId;
}

void
deleteUnusedSlavesCascade(GeoObjectCollection&& collection, ObjectsCache& cache)
{
    RelationsProcessor relationsProcessor(cache);
    auto affectedBySplitCollection = cache.find(
        [&](const GeoObject* obj)
        {
            return
                obj->isModifiedGeom() &&
                is<LinearElement>(obj) &&
                as<LinearElement>(obj)->affectedBySplitId() &&
                as<LinearElement>(obj)->affectedBySplitId() != obj->id();
        });
    auto findFragments =
        [&](TOid id)
        {
            GeoObjectCollection fragments;
            for (const auto& obj : affectedBySplitCollection) {
                if (as<LinearElement>(obj)->affectedBySplitId() == id) {
                    fragments.add(obj);
                }
            }
            return fragments;
        };
    while (!collection.empty()) {
        GeoObjectCollection nextLevel;
        for (const auto& obj : collection) {
            for (const auto& relation :
                    obj->slaveRelations().diff().deleted) {
                auto relative = relation.relative();
                if (relative->primaryEdit()) {
                    continue;
                }
                const auto& slaveRole = obj->category().slaveRole(relation.roleId());
                const bool isJunctionCategoryRole = isJunctionCategory(slaveRole.categoryId());
                if (!slaveRole.deleteUnused() &&
                    !slaveRole.tableRow() &&
                    !isJunctionCategoryRole) {
                    continue;
                }

                auto relationsRange =
                    isJunctionCategoryRole
                    ? relative->masterRelations().range()
                    : relative->masterRelations().range(slaveRole.roleId());
                if (relationsRange.empty()) {
                    nextLevel.add(cache.getExisting(relative->id()));
                    relationsProcessor.deleteAllRelations(relative);
                    relative->setState(GeoObject::State::Deleted);
                }
                if (!is<LinearElement>(relative) || !relative->isModifiedGeom()) {
                    continue;
                }
                for (const auto& fragment : findFragments(relative->id())) {
                    if (!fragment->masterRelations().range(slaveRole.roleId()).empty()) {
                        continue;
                    }
                    nextLevel.add(cache.getExisting(fragment->id()));
                    relationsProcessor.deleteAllRelations(fragment.get());
                    fragment->setState(GeoObject::State::Deleted);
                }
            }
        }
        std::swap(collection, nextLevel);
    }
}

void processLoginAttributes(GeoObjectCollection& collection, ObjectsCache& cache)
{
    for (const auto& obj : collection) {
        for (const auto& attrDef : obj->category().attrDefs()) {
            if (attrDef->valueType() != ValueType::Login) {
                continue;
            }
            const auto& loginAttrId = attrDef->id();
            const auto& login = obj->attributes().value(loginAttrId);
            const auto& uidAttrId = loginAttrId + ATTR_UID_SUFFIX;
            std::string uidValue;
            if (!login.empty()) {
                acl::ACLGateway gw(cache.workCore());
                auto user = gw.user(login);
                uidValue = std::to_string(user.uid());
            }
            if (obj->attributes().isDefined(uidAttrId)) {
                if (uidValue != obj->attributes().value(uidAttrId)) {
                    obj->attributes().setValue(uidAttrId, uidValue);
                    obj->setModifiedAttr();
                }
            }
        }
    }
}

struct PreparedCollections
{
    GeoObjectCollection modified;
    GeoObjectCollection requireEditNotes;
};

PreparedCollections
prepareCollectionToSave(
    TUid uid,
    const ObjectsEditContextsPtrMap& editContexts,
    ObjectsCache& cache,
    const std::map<TOid, ObjectPtr>& cachedObjects)
{
    resetShadowAttributes(cache);
    deleteUnusedSlavesCascade(
        cache.find([](const GeoObject* obj) { return obj->isModifiedLinksToSlaves(); })
        , cache);

    PreparedCollections preparedCollections;
    for (const auto& objPtr : cachedObjects) {
        auto& obj = objPtr.second;
        obj->calcModified();
        if (obj->isModified()) {
            preparedCollections.modified.add(obj);
            preparedCollections.requireEditNotes.add(obj);
        } else if (obj->primaryEdit()) {
            preparedCollections.requireEditNotes.add(obj);
        }
    }

    GeoObjectCollection modifiedObjects = preparedCollections.modified;

    propagateSrvAttrUpdate(preparedCollections.modified, cache);
    Validator(uid, cache, modifiedObjects, editContexts).validate();
    processLoginAttributes(modifiedObjects, cache);
    DEBUG() << BOOST_CURRENT_FUNCTION
        << " Modified objects collection size: " << preparedCollections.modified.size();
    return preparedCollections;
}

} // namespace

void
ObjectsCache::saveWithContext(
    const ObjectsDataMap& objectsUpdateData,
    const ObjectsEditContextsPtrMap& editContexts,
    TUid user,
    const std::string& commitActionString,
    const StringMap& actionNotes)
{
    auto preparedCollections = prepareCollectionToSave(
        user, editContexts, *this, cachedObjects_);
    auto commitAttributes = createCommitNotes(commitActionString,
        actionNotes, preparedCollections.requireEditNotes, *this);
    setSavedCommit(revisionsFacade_->save(preparedCollections.modified, user,
        commitAttributes, objectsUpdateData, RevisionsFacade::CheckPermissionsPolicy::Check));
}

void
ObjectsCache::saveWithContextSkipPermissionsCheck(
    const ObjectsEditContextsPtrMap& editContexts,
    TUid user,
    const std::string& commitActionString,
    const StringMap& actionNotes)
{
    auto preparedCollections = prepareCollectionToSave(
        user, editContexts, *this, cachedObjects_);
    auto commitAttributes = createCommitNotes(commitActionString,
        actionNotes, preparedCollections.requireEditNotes, *this);
    setSavedCommit(revisionsFacade_->save(preparedCollections.modified, user,
        commitAttributes, {}, RevisionsFacade::CheckPermissionsPolicy::Skip));
}

Token
ObjectsCache::commit()
{
    auto branchId = branchContext_->branch.id();
    auto savedCommitId = savedCommit().id();

    auto token = branchContext_->commit();

    branchContext_.reset(); // release connections into pgpool
    revisionsFacade_.reset();

    branchContext_ = make_unique<BranchContext>(
        BranchContextFacade::acquireRead(branchId, token));

    revisionsFacade_ = make_unique<RevisionsFacade>(
        make_unique<GeoObjectFactory>(*this),
        branchContext_->txnCore(),
        branchContext_->branch,
        savedCommitId);

    return token;
}

const revision::Commit&
ObjectsCache::savedCommit() const
{
    REQUIRE(savedCommit_, "Cache is not saved yet");
    return *savedCommit_;
}

void
ObjectsCache::setSavedCommit(revision::Commit commit)
{
    savedCommit_ = std::make_unique<revision::Commit>(std::move(commit));
}

} // namespace wiki
} // namespace maps
