#include "revisions_facade.h"
#include "factory.h"
#include "objectclassinfo.h"
#include "objects/relation_object.h"
#include "objects/category_traits.h"
#include "check_permissions.h"
#include "shadow_attributes.h"

#include <sstream>
#include <functional>
#include <algorithm>

namespace maps {
namespace wiki {

namespace {

const size_t OBJECT_IDS_BULK_SIZE = 1000;

DECLARE_ERR_CODE( ERR_DATA_HAS_NO_CHANGES );

} // namespace

RevisionsFacade::RevisionsFacade(
        std::unique_ptr<GeoObjectFactory> factory,
        Transaction& work,
        const revision::Branch& branch,
        boost::optional<TCommitId> headCommit)
    : factory_(std::move(factory))
    , gateway_(work, branch)
    , snapshot_(gateway_.snapshot(
        headCommit ? *headCommit : gateway_.headCommitId()))
{}

RevisionsFacade::~RevisionsFacade()
{}

boost::optional<ObjectPtr>
RevisionsFacade::load(TOid oid)
{
    auto result = snapshot_.objectRevision(oid);
    if (!result) {
        return boost::none;
    }
    ObjectPtr object = loadRevision(*result);
    if (object) {
        return object;
    }
    return boost::none;
}

revision::Commit
RevisionsFacade::loadHeadCommit()
{
    return revision::Commit::load(gateway_.work(), snapshot_.maxCommitId());
}

revision::CommitDiff
RevisionsFacade::headCommitDiff()
{
    return gateway_.reader().commitDiff(snapshot_.maxCommitId());
}

TOIds
RevisionsFacade::getIdsByBBox(
    const revision::Snapshot& snapshot,
    const geolib3::BoundingBox& bbox,
    const StringSet& categoryIds,
    revision::filters::GeomFilterExpr::Operation op)
{
    if (categoryIds.empty()) {
        return TOIds();
    }

    namespace rf = revision::filters;

    auto filter =
        rf::Attr::definedAny(plainCategoryIdsToCanonical(categoryIds)) &&
        rf::GeomFilterExpr(op, bbox.minX(), bbox.minY(), bbox.maxX(), bbox.maxY()) &&
        rf::ObjRevAttr::isNotRelation() &&
        rf::ObjRevAttr::isNotDeleted();

    TOIds ids;
    for (auto revId : snapshot.revisionIdsByFilter(filter)) {
        ids.insert(revId.objectId());
    }
    return ids;
}

namespace {

template <typename Cont>
std::string toString(const Cont& objects)
{
    std::ostringstream os;
    bool first = true;
    for (auto object : objects) {
        if (!first) {
            os << " ";
        }
        first = false;
        os << object;
    }
    return os.str();
}

template <typename T>
const revision::ObjectRevision& convertToObjectRevision(const T& value);

template <>
const revision::ObjectRevision&
convertToObjectRevision(const RevisionsFacade::RevisionsList::value_type& value)
{
    return value;
}

template <>
const revision::ObjectRevision&
convertToObjectRevision(const RevisionsFacade::RevisionsMap::value_type& value)
{
    return value.second;
}

} // namespace

GeoObjectCollection
RevisionsFacade::loadRelations(RelationType type, const TOIds& objectIds)
{
    DEBUG() << BOOST_CURRENT_FUNCTION
        << " Objects: " << toString(objectIds)
        << ", roles: all roles";
    if (objectIds.empty()) {
        return GeoObjectCollection();
    }
    auto revs = type == RelationType::Master
        ? snapshot_.loadMasterRelations(objectIds)
        : snapshot_.loadSlaveRelations(objectIds);
    return collectResult(revs);
}

revision::filters::ProxyFilterExpr
RevisionsFacade::relationsFilter(
    RelationType type, const TOIds& ids, const StringSet& roleIds) const
{
    namespace rf = revision::filters;

    auto idsExpr = type == RelationType::Master
        ? rf::ObjRevAttr::slaveObjectId()
        : rf::ObjRevAttr::masterObjectId();

    rf::ProxyFilterExpr filter(idsExpr.in(ids));
    filter &= rf::ObjRevAttr::isNotDeleted() && rf::ObjRevAttr::isRelation();
    if (!roleIds.empty()) {
        filter &= rf::Attr(ATTR_REL_ROLE).in(roleIds);
    }
    return filter;
}

GeoObjectCollection
RevisionsFacade::loadRelations(RelationType type, const TOIds& objectIds, const StringSet& roleIds)
{
    DEBUG() << BOOST_CURRENT_FUNCTION
        << " Objects: " << toString(objectIds)
        << ", roles: " << toString(roleIds);
    if (objectIds.empty() || roleIds.empty()) {
        return GeoObjectCollection();
    }

    revision::Revisions result;
    TOIds ids;
    auto loadBatch = [&]()
    {
        auto filter = relationsFilter(type, ids, roleIds);
        auto revs = snapshot_.relationsByFilter(filter);
        result.splice(result.end(), revs);
    };
    for (auto id : objectIds) {
        ids.insert(id);
        if (ids.size() >= OBJECT_IDS_BULK_SIZE) {
            loadBatch();
            ids.clear();
        }
    }
    if (!ids.empty()) {
        loadBatch();
    }
    return collectResult(result);
}

boost::optional<GeoObjectCollection>
RevisionsFacade::loadRelations(RelationType type, TOid id, const StringSet& roleIds, size_t limit)
{
    REQUIRE(!roleIds.empty(), "Empty role ids list");
    auto filter = relationsFilter(type, TOIds{id}, roleIds);
    auto revs = snapshot_.tryLoadRelationsByFilter(filter, limit);
    if (!revs) {
        return boost::none;
    }
    return collectResult(*revs);
}

size_t
RevisionsFacade::relativesForRoleCount(
    RelationType type,
    TOid id,
    const std::string& roleId,
    const std::string& relativeCategoryId)
{
    revision::filters::ProxyFilterExpr filter(relationsFilter(type, TOIds {id}, StringSet {roleId}) &&
                  revision::filters::ObjRevAttr::isRelation());
    if (!relativeCategoryId.empty()) {
        filter &= revision::filters::Attr(
            type == RelationType::Master
            ? ATTR_REL_MASTER
            : ATTR_REL_SLAVE).equals(relativeCategoryId);
    }
    auto revIds = snapshot_.revisionIdsByFilter(filter);
    DEBUG() << BOOST_CURRENT_FUNCTION <<
        "Object id: " << id <<
        ", slave role: " << roleId <<
        ", slaves count: " << revIds.size();
    return revIds.size();
}

std::vector<ObjectPtr>
RevisionsFacade::findRelation(TOid masterId, TOid slaveId,
    const std::string& roleId,
    const std::string& masterCategoryId,
    const std::string& slaveCategoryId)
{
    namespace rf = revision::filters;

    auto filter =
        rf::ObjRevAttr::isNotDeleted() &&
        rf::ObjRevAttr::masterObjectId() == masterId &&
        rf::ObjRevAttr::slaveObjectId() == slaveId &&
        rf::Attr(ATTR_REL_ROLE).equals(roleId) &&
        rf::Attr(ATTR_REL_MASTER).equals(masterCategoryId) &&
        rf::Attr(ATTR_REL_SLAVE).equals(slaveCategoryId);

    std::vector<ObjectPtr> relations;
    auto revs = snapshot_.relationsByFilter(filter);
    relations.reserve(revs.size());
    for (const auto& rev : revs) {
        ObjectPtr object = loadRevision(rev);
        if (object) {
            relations.push_back(object);
        }
    }
    return relations;
}

RelativesIdsDiff
RevisionsFacade::headCommitRelativesDiff(RelationType type, TOid id)
{
    using namespace revision;
    auto filter = relationsFilter(type, TOIds {id}, {});
    auto curRelRevs = snapshot_.relationsByFilter(filter);
    auto headCommit = snapshot_.maxCommitId();
    auto prevRelRevs = (headCommit > 1)
        ? gateway_.snapshot(headCommit - 1).relationsByFilter(filter)
        : RevisionsList();
    return relationsRevisionsToDiff(type, curRelRevs, prevRelRevs);
}

RelativesIdsMap
RevisionsFacade::headCommitRelativesDiff(RelationType type, const TOIds& ids)
{
    RelativesIdsMap res;
    for (auto id : ids) {
        res.insert(std::make_pair(id, headCommitRelativesDiff(type, id)));
    }
    return res;
}

RoleToRelativesIdsDiff
RevisionsFacade::headCommitRelativesDiff(RelationType type, TOid id, const StringSet& roleIds)
{
    if (roleIds.empty()) {
        return {};
    }
    using namespace revision;
    auto filter = relationsFilter(type, TOIds {id}, roleIds);
    auto curRelRevs = snapshot_.relationsByFilter(filter);
    auto headCommit = snapshot_.maxCommitId();
    auto prevRelRevs = (headCommit > 1)
        ? gateway_.snapshot(headCommit - 1).relationsByFilter(filter)
        : RevisionsList();
    return relationsRevisionsToRoleRelDiff(type, curRelRevs, prevRelRevs);
}

RoleToRelativesIdsDiff
RevisionsFacade::headCommitRelativesDiff(RelationType type)
{
    auto commitDiff = revision::commitDiff(work(), snapshot_.maxCommitId(), 0);
    std::set<revision::RevisionID> prevRelRevsIds;
    std::set<revision::RevisionID> curRelRevsIds;
    for (const auto& [_, diff] : commitDiff) {
        const auto& diffData = diff.data();
        if (!diffData.newRelationData && !diffData.oldRelationData) {
            continue;
        }
        if (diffData.deleted) {
            prevRelRevsIds.insert(diff.oldId());
        } else {
            curRelRevsIds.insert(diff.newId());
        }
    }
    return relationsRevisionsToRoleRelDiff(type,
        gateway_.reader().loadRevisions(curRelRevsIds),
        gateway_.reader().loadRevisions(prevRelRevsIds));
}


namespace {
struct RelativeRole
{
    std::string roleId;
    TOid relativeId;
};

bool operator < (const RelativeRole& x, const RelativeRole& y)
{
    if (x.roleId < y.roleId)
        return true;
    if (x.roleId > y.roleId)
        return false;
    return x.relativeId < y.relativeId;
}

typedef std::set<RelativeRole> RelativesRoles;

RelativesRoles
relationsRevisionsToRelativesRoles(RelationType type,
    const RevisionsFacade::RevisionsList& relationsRevisions)
{
    RelativesRoles relativesRoles;
    for (const auto& curRelRev : relationsRevisions) {
        const auto& relData = curRelRev.data().relationData;
        REQUIRE(relData, "Relation data not set for relation object");
        const auto& relAttr = curRelRev.data().attributes;
        REQUIRE(relAttr, "No attributes for relation object");
        auto it = relAttr->find("rel:role");
        REQUIRE(it != relAttr->end(), "Role not set for relation object");
        relativesRoles.insert(
            {
                it->second,
                type == RelationType::Master
                    ? relData->masterObjectId()
                    : relData->slaveObjectId()
            });
    }
    return relativesRoles;
}

TOIds
relationsRevisionsToRelativesIds(RelationType type,
    const RevisionsFacade::RevisionsList& relationsRevisions)
{
    TOIds relativesIds;
    for (const auto& curRelRev : relationsRevisions) {
        const auto& relData = curRelRev.data().relationData;
        REQUIRE(relData, "Relation data not set for relation object");
        relativesIds.insert(type == RelationType::Master
            ? relData->masterObjectId()
            : relData->slaveObjectId());
    }
    return relativesIds;
}

} // namespace

RelativesIdsDiff
RevisionsFacade::relationsRevisionsToDiff(
    RelationType type,
    const RevisionsList& curRelations,
    const RevisionsList& prevRelations)
{
    TOIds curRelativesIds = relationsRevisionsToRelativesIds(type, curRelations);
    TOIds prevRelativesIds = relationsRevisionsToRelativesIds(type, prevRelations);
    RelativesIdsDiff res;
    std::set_difference(curRelativesIds.begin(), curRelativesIds.end(),
        prevRelativesIds.begin(), prevRelativesIds.end(),
        std::inserter<TOIds>(res.added, res.added.begin()));
    std::set_difference(prevRelativesIds.begin(), prevRelativesIds.end(),
        curRelativesIds.begin(), curRelativesIds.end(),
        std::inserter<TOIds>(res.deleted, res.deleted.begin()));
    DEBUG() << BOOST_CURRENT_FUNCTION;
    for (auto id : res.added) {
        DEBUG() << "Added relation to: " << id;
    }
    for (auto id : res.deleted) {
        DEBUG() << "Deleted relation to: " << id;
    }
    return res;
}

RoleToRelativesIdsDiff
RevisionsFacade::relationsRevisionsToRoleRelDiff(
    RelationType type,
    const RevisionsList& curRelations,
    const RevisionsList& prevRelations)
{
    auto cur = relationsRevisionsToRelativesRoles(type, curRelations);
    auto prev = relationsRevisionsToRelativesRoles(type, prevRelations);
    RelativesRoles added;
    RelativesRoles deleted;
    std::set_difference(cur.begin(), cur.end(),
        prev.begin(), prev.end(),
        std::inserter<RelativesRoles>(added, added.begin()));
    std::set_difference(prev.begin(), prev.end(),
        cur.begin(), cur.end(),
        std::inserter<RelativesRoles>(deleted, deleted.begin()));
    RoleToRelativesIdsDiff res;
    for (const auto& relRole : added) {
        res[relRole.roleId].added.insert(relRole.relativeId);
    }
    for (const auto& relRole : deleted) {
        res[relRole.roleId].deleted.insert(relRole.relativeId);
    }
    return res;
}


namespace {

TOIds
computeUnfetchedIds(
    const RevisionsFacade::RevisionsMap& revisionsMap, const TOIds& requestedIds)
{
    TOIds unfetchedIds;
    for (auto id : requestedIds) {
        if (!revisionsMap.count(id)) {
            unfetchedIds.insert(id);
        }
    }
    return unfetchedIds;
}

} // namespace

GeoObjectCollection
RevisionsFacade::load(const TOIds& objectIds)
{
    if (objectIds.empty()) {
        return GeoObjectCollection();
    }

    auto revisionsMap = snapshot_.objectRevisions(objectIds);

    if (revisionsMap.size() != objectIds.size()) {
        DEBUG() << BOOST_CURRENT_FUNCTION << " heads not fetched: " <<
            toString(computeUnfetchedIds(revisionsMap, objectIds));
    }

    return collectResult(revisionsMap);
}

namespace {
StringMap
fixCategory(const Attributes& attrs, const std::string& categoryId)
{
    StringMap ret;
    for (const auto& attr : attrs) {
        ret.insert(std::make_pair(attr.name(), attr.packedValues()));
    }
    if (categoryId != CATEGORY_RELATION) {
       ret[plainCategoryIdToCanonical(categoryId)] = "1";
    }
    return ret;
}

bool
objectNeedsSave(const GeoObject* obj)
{
    return obj->isCreated() ||
                (obj->original() &&
                    (obj->isModifiedState() ||
                     obj->isModifiedAttr() ||
                     obj->isModifiedGeom() ||
                     obj->isModifiedRichContent() ||
                     obj->isModifiedSysAttr())
                );
}

} // namespace

revision::Commit
RevisionsFacade::save(const GeoObjectCollection& col,
    TUid user,
    const StringMap& commitAttributes,
    const ObjectsDataMap& objectsUpdateData,
    CheckPermissionsPolicy checkPermissionsPolicy)
{
    std::unique_ptr<CheckPermissions> checkPermissions;
    if (CheckPermissionsPolicy::Check == checkPermissionsPolicy) {
        checkPermissions = make_unique<CheckPermissions>(user, work(), objectsUpdateData);
    }
    std::vector<revision::RevisionsGateway::NewRevisionData> newData;
    newData.reserve(col.size());

    std::vector<GeoObject*> savedObjects;
    savedObjects.reserve(col.size());
    for (const auto& obj : col) {
        bool needSave = objectNeedsSave(obj.get());
        if (checkPermissions &&
            (needSave ||
            obj->isModifiedTableAttrs() ||
            obj->primaryEdit() ||
            (needCheckRelationsPermissions(obj->categoryId())
                && obj->isModifiedLinksToSlaves())
            ))
        {
           (*checkPermissions)(obj.get());
        }
        if (!needSave) {
            DEBUG() << BOOST_CURRENT_FUNCTION << " NOT Saving: " <<
                obj->revision() <<
                " m: " << obj->isModified() << " d: " << obj->isDeleted();
            continue;
        }
        DEBUG() << BOOST_CURRENT_FUNCTION << " Saving: " << obj->id() <<
            " d: " << obj->isDeleted() << " cr:" << obj->revision().commitId();

        revision::RevisionsGateway::NewRevisionData newObjData;
        newObjData.second.deleted = obj->isDeleted();
        newObjData.first = obj->revision();
        if (!newObjData.second.deleted) {
            if (!obj->geom().isNull() && obj->isModifiedGeom()) {
                newObjData.second.geometry = obj->geom().wkb();
            }
            if (obj->isModifiedAttr() || obj->isModifiedSysAttr()) {
                newObjData.second.attributes =
                    clearShadowAttributes(fixCategory(obj->attributes(), obj->categoryId()));
            }
            if (obj->isModifiedRichContent()) {
                //TODO: std::optional richContent
                const auto& richContent = obj->richContent();
                if (richContent) {
                    newObjData.second.description = *richContent;
                }
            }
            if (is<RelationObject>(obj)) {
                RelationObject* rel = as<RelationObject>(obj);
                newObjData.second.relationData =
                    revision::RelationData(rel->masterId(), rel->slaveId());
            }
        }
        savedObjects.push_back(obj.get());
        newData.push_back(std::move(newObjData));
    }

    if (newData.empty()) {
        THROW_WIKI_LOGIC_ERROR(ERR_DATA_HAS_NO_CHANGES, "Data has no changes.");
    }

    revision::Commit result = gateway().createCommit(
        newData,
        revision::UserID(user),
        revision::Attributes(commitAttributes));

    auto commitId = result.id();
    for (GeoObject* obj : savedObjects) {
        obj->setRevisionCommit(commitId);
    }
    return result;
}

Transaction&
RevisionsFacade::work()
{
    return gateway_.work();
}

TRevisionId
RevisionsFacade::newObjectId()
{
    return gateway_.acquireObjectId();
}

ObjectPtr
RevisionsFacade::loadRevision(const revision::ObjectRevision& objectRev)
{
    try {
        const auto& objectRevData = objectRev.data();
        const auto& objectClass = GeoObjectFactory::objectClass(
            categoryFromAttributes(
                    objectRevData.attributes
                        ? *objectRevData.attributes
                        : StringMap()));
        return factory_->createObject(objectRev, objectClass);
    } catch (const UnknownObjectCategoryException& ex) {
        INFO() << BOOST_CURRENT_FUNCTION << " revision " << objectRev.id() <<
            " was fetched but could not be loaded: " << ex.what();
        return ObjectPtr();
    }
}

template <typename RevisionsContainer>
GeoObjectCollection
RevisionsFacade::collectResult(const RevisionsContainer& revisions)
{
    GeoObjectCollection result;
    for (const auto& rev : revisions) {
        const auto& revisionRef = convertToObjectRevision(rev);
        ObjectPtr object = loadRevision(revisionRef);
        if (object) {
            result.add(object);
        }
    }
    return result;
}

} // namespace wiki
} // namespace maps
