#include "object_loader.h"
#include "geometry_filter.h"
#include "relation_data.h"
#include "utils/geom.h"

#include <yandex/maps/wiki/groupedit/object.h>
#include <yandex/maps/wiki/groupedit/relation.h>

#include <yandex/maps/wiki/revision/branch_manager.h>
#include <yandex/maps/wiki/revision/filters.h>
#include <yandex/maps/wiki/revision/revisionid.h>
#include <yandex/maps/wiki/revision/snapshot.h>

#include <maps/libs/common/include/exception.h>
#include <maps/libs/geolib/include/bounding_box.h>

#include <boost/range/iterator_range.hpp>
#include <iterator>
#include <unordered_map>

namespace maps {
namespace wiki {

namespace rf = revision::filters;

namespace groupedit {

namespace {

const size_t REVISION_LOAD_BATCH_SIZE = 10000;

typedef std::unordered_map<TObjectId, std::shared_ptr<RelationData>>
    RelDataIdentityMap;

template <typename TRange>
void loadBatch(
        const revision::Snapshot& snapshot,
        const GeometryFilter& geomFilter,
        const TRange& revisionIds,
        RelDataIdentityMap& relationMap,
        BatchCallback batchCallback)
{
    // TODO: make relations loading optional
    std::vector<TObjectId> objectIds;
    std::transform(
        std::begin(revisionIds), std::end(revisionIds),
        std::back_inserter(objectIds),
        [](const revision::RevisionID& revId) { return revId.objectId(); });

    std::unordered_map<TObjectId, std::list<Relation>> relationLists;
    for (auto& masterRel : snapshot.loadMasterRelations(objectIds)) {
        TObjectId relId = masterRel.id().objectId();
        auto relDataPtr = std::make_shared<RelationData>(std::move(masterRel));

        TObjectId objId = relDataPtr->slaveId;
        relationLists[objId].emplace_back(
            objId,
            relationMap.insert({relId, relDataPtr}).first->second);
    }
    for (auto& slaveRel : snapshot.loadSlaveRelations(objectIds)) {
        TObjectId relId = slaveRel.id().objectId();
        auto relDataPtr = std::make_shared<RelationData>(std::move(slaveRel));

        TObjectId objId = relDataPtr->masterId;
        relationLists[objId].emplace_back(
            objId,
            relationMap.insert({relId, relDataPtr}).first->second);
    }

    std::vector<Object> result;
    for (auto& revision : snapshot.reader().loadRevisions(
            revisionIds, rf::ObjRevAttr::isNotRelation())) {
        TObjectId objId = revision.id().objectId();
        if (geomFilter.apply(revision.data().geometry)) {
            REQUIRE(revision.type() == revision::RevisionType::RegularObject,
                    "Incorrect revision type");

            result.emplace_back(
                    std::move(revision.id()),
                    std::move(revision.data()),
                    std::move(relationLists[objId]));
        }
    }

    batchCallback(std::move(result));
}

} // namespace

void loadObjects(
        const revision::Snapshot& snapshot,
        const std::vector<revision::RevisionID>& revisionIds,
        const GeometryFilter& geomFilter,
        BatchCallback batchCallback)
{
    RelDataIdentityMap relationMap;

    for (size_t iBegin = 0;
            iBegin < revisionIds.size();
            iBegin += REVISION_LOAD_BATCH_SIZE)
    {
        size_t iEnd = std::min(
            iBegin + REVISION_LOAD_BATCH_SIZE,
            revisionIds.size());

        typedef boost::iterator_range<decltype(std::begin(revisionIds))>
            TRange;

        TRange currentRange(
            std::begin(revisionIds) + iBegin,
            std::begin(revisionIds) + iEnd);

        loadBatch(
            snapshot, geomFilter, currentRange, relationMap, batchCallback);
    }
}

} // namespace groupedit
} // namespace wiki
} // namespace maps
