#include "searchers.h"

#include <maps/libs/geolib/include/contains.h>

namespace maps {
namespace wiki {
namespace topology_fixer {

namespace gl = maps::geolib3;

namespace {

enum class SpatialRelation { Contains, Intersects };

template <class Index>
IdSet
idsByBBoxImpl(const Index& index, const gl::BoundingBox& bbox,
    SpatialRelation spatialRel)
{
    gl::BoundingBox searchBox = gl::resizeByValue(bbox, gl::EPS);
    gl::BoundingBox shrinked = gl::resizeByValue(bbox, - gl::EPS);
    auto res = index.find(searchBox);
    IdSet ids;
    for (auto it = res.first; it != res.second; ++it) {
        if (spatialRel != SpatialRelation::Contains ||
            gl::contains(shrinked, it->geometry().boundingBox()))
        {
            ids.insert(it->value());
        }
    }
    return ids;
}

const double EPS = 0.05;

} // namespace

FaceMasterSearcher::FaceMasterSearcher(const TopologyData& data)
    : data_(new Data())
{
    for (const auto& master : data.masters()) {
        boost::optional<gl::BoundingBox> masterBox;
        for (const auto& faceRel : master.second.faceRels()) {
            boost::optional<gl::BoundingBox> faceBox;
            for (auto edgeId : data.face(faceRel.first).edgeIds()) {
                const auto& edgeBox = data.edge(edgeId).boundingBox();
                faceBox = faceBox ? gl::expand(*faceBox, edgeBox) : edgeBox;
            }
            if (faceBox) {
                masterBox = masterBox ? gl::expand(*masterBox, *faceBox) : *faceBox;
            }
        }
        if (masterBox) {
            auto it = data_->bboxes.insert({master.first, *masterBox}).first;
            data_->index.insert(&it->second, master.first);
            data_->bbox = data_->bbox ? gl::expand(*masterBox, *data_->bbox) : *masterBox;
        }
    }
    data_->index.build();
}

boost::optional<geolib3::BoundingBox>
FaceMasterSearcher::bbox(MasterId masterId) const
{
    auto it = data_->bboxes.find(masterId);
    if (it == data_->bboxes.end()) {
        return boost::none;
    }
    return it->second;
}

IdSet
FaceMasterSearcher::idsByBBox(const gl::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Contains);
}

// EdgeMasterSearcher

EdgeMasterSearcher::EdgeMasterSearcher(const TopologyData& data)
    : data_(new Data())
{
    for (const auto& master : data.masters()) {
        boost::optional<gl::BoundingBox> masterBox;
        for (auto edgeId : master.second.edgeIds()) {
            const auto& edgeBox = data.edge(edgeId).boundingBox();
            masterBox = masterBox ? gl::expand(*masterBox, edgeBox) : edgeBox;
        }
        if (masterBox) {
            auto it = data_->bboxes.insert({master.first, *masterBox}).first;
            data_->index.insert(&it->second, master.first);
            data_->bbox = data_->bbox ? gl::expand(*masterBox, *data_->bbox) : *masterBox;
        }
    }
    data_->index.build();
}

boost::optional<geolib3::BoundingBox>
EdgeMasterSearcher::bbox(MasterId masterId) const
{
    auto it = data_->bboxes.find(masterId);
    if (it == data_->bboxes.end()) {
        return boost::none;
    }
    return it->second;
}

IdSet
EdgeMasterSearcher::idsByBBox(const gl::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Contains);
}

// FaceSearcher

FaceSearcher::FaceSearcher(const TopologyData& data)
    : data_(new Data())
{
    init(data, data.faceIds());
}

FaceSearcher::FaceSearcher(const TopologyData& data, const IdSet& faceIds)
    : data_(new Data())
{
    std::vector<FaceId> faceIdsList(faceIds.begin(), faceIds.end());
    init(data, faceIdsList);
}

void
FaceSearcher::init(const TopologyData& data, const std::vector<FaceId>& faceIds)
{
    for (auto faceId: faceIds) {
        const Face& face = data.face(faceId);
        boost::optional<gl::BoundingBox> bbox;
        for (auto edgeId : face.edgeIds()) {
            const auto& edge = data.edge(edgeId);
            const auto& geom = edge.linestring();
            if (geom.points().empty()) {
                continue;
            }
            bbox = bbox ? gl::expand(*bbox, edge.boundingBox()) : edge.boundingBox();
        }
        if (bbox) {
            auto it = data_->bboxes.insert({face.id(), *bbox}).first;
            data_->index.insert(&it->second, it->first);
            data_->bbox = data_->bbox ? gl::expand(*data_->bbox, *bbox) : *bbox;
        }
    }
    data_->index.build();
}

boost::optional<geolib3::BoundingBox>
FaceSearcher::bbox(FaceId faceId) const
{
    auto it = data_->bboxes.find(faceId);
    if (it == data_->bboxes.end()) {
        return boost::none;
    }
    return it->second;
}

IdSet
FaceSearcher::idsByBBox(const geolib3::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Contains);
}

IdSet
FaceSearcher::idsIntersectingBBox(const geolib3::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Intersects);
}

// StaticEdgeSearcher

StaticEdgeSearcher::StaticEdgeSearcher(const TopologyData& data)
    : data_(new Data())
{
    for (auto edgeId: data.edgeIds()) {
        const Edge& edge = data.edge(edgeId);
        auto it = data_->bboxes.insert({edgeId, edge.boundingBox()}).first;
        data_->index.insert(&it->second, it->first);
        data_->bbox = data_->bbox ? gl::expand(*data_->bbox, it->second) : it->second;
    }
    data_->index.build();
}

boost::optional<geolib3::BoundingBox>
StaticEdgeSearcher::bbox(EdgeId edgeId) const
{
    auto it = data_->bboxes.find(edgeId);
    if (it == data_->bboxes.end()) {
        return boost::none;
    }
    return it->second;
}

IdSet
StaticEdgeSearcher::idsByBBox(const geolib3::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Contains);
}

IdSet
StaticEdgeSearcher::idsIntersectingBBox(const geolib3::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(data_->index, bbox, SpatialRelation::Intersects);
}

// DynamicEdgeSearcher

DynamicEdgeSearcher::DynamicEdgeSearcher(const TopologyData& data, const std::set<EdgeId>& edgeIds)
{
    ASSERT(!edgeIds.empty());

    gl::BoundingBox grid = data.edge(*edgeIds.begin()).boundingBox();
    for (auto edgeId : edgeIds) {
        grid = gl::expand(grid, data.edge(edgeId).boundingBox());
    }

    size_t size = floor(sqrt(1.0 * edgeIds.size()));

    index_ = std::make_shared<IndexType>(gl::resizeByValue(grid, EPS), size, size);
    for (auto edgeId : edgeIds) {
        const auto& edge = data.edge(edgeId);
        insert(edge.linestring(), edge.boundingBox(), edgeId);
    }
}

void DynamicEdgeSearcher::insert(const gl::Polyline2& geom, const gl::BoundingBox& bbox, EdgeId edgeId)
{
    elements_.emplace_back(Element{geom, bbox});
    //index_->insert(&elements_.back().geom, elements_.back().bbox, edgeId);
    index_->insert(&elements_.back().geom, edgeId);
}

IdSet DynamicEdgeSearcher::edgeIdsByBBox(const gl::BoundingBox& bbox) const
{
    return idsByBBoxImpl<IndexType>(*index_, bbox, SpatialRelation::Intersects);
}

} // namespace topology_fixer
} // namespace wiki
} // namespace maps
