#pragma once

#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis.h>

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

#include <maps/wikimap/mapspro/services/mrc/libs/object/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/libs/object/include/loader.h>
#include <maps/wikimap/mapspro/services/mrc/libs/object/include/rd.h>

#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_hypothesis/include/directed_point.h>

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/units.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/units_literals.h>

#include <yandex/maps/wiki/social/feedback/enums.h>
#include <maps/libs/common/include/exception.h>

#include <boost/range/algorithm.hpp>
#include <boost/range/algorithm_ext.hpp>
#include <boost/range/adaptor/transformed.hpp>

#include <unordered_map>
#include <optional>
#include <utility>
#include <list>
#include <vector>

namespace maps::mrc::eye {

// Хранит внутри список геометрий. Неоходим на тот случай, если
// для поиска используются не сами геометрии, а какие-то классы
// из которых получаются, геометрии, т.е. мы не имеем готового
// контейнера с геометриями. Если такой контейнер уже у нас есть,
// то можно использовать StaticGeometrySearcher непосредственно.
template <typename Geometry, typename Value>
class GeometrySearcher {
public:
    using InternalSearcher = geolib3::StaticGeometrySearcher<Geometry, Value>;

    void insert(const Geometry& geometry, const Value& value) {
        geometries_.push_back(geometry);
        searcher_.insert(&geometries_.back(), value);
    }

    void build() {
        searcher_.build();
    }

    const typename InternalSearcher::SearchResult find(const geolib3::BoundingBox& searchBox) const {
        return searcher_.find(searchBox);
    }

private:
    InternalSearcher searcher_;
    std::list<Geometry> geometries_;
};

struct HypothesisObjectData {
    db::eye::Object object;
    db::eye::ObjectLocation location;
    db::eye::Objects slaveObjects;
    HypothesisObjectData(
        const db::eye::Object& _object,
        const db::eye::ObjectLocation& _location,
        db::eye::Objects&& _slaveObjects)
        : object(_object)
        , location(_location)
        , slaveObjects(std::move(_slaveObjects))
    {}
};

class ObjectsContext {
public:
    typedef std::vector<HypothesisObjectData> Data;

    typedef Data::value_type value_type;
    typedef Data::const_iterator const_iterator;

    ObjectsContext(
        const db::eye::Objects& objects,
        const db::eye::ObjectLocations& locations,
        const std::map<db::TId, db::TIds>& objectRelations,
        const db::eye::Objects& slaveObjects
        );

    auto begin() const { return data_.cbegin(); }

    auto end() const { return data_.cend(); }

    size_t size() const { return data_.size(); }

private:
    Data data_;
};


// Получаем таблички привязанные к объектам с id из списка objectIds
// Return:
//    first  - map ключ masterObjectId, значение slaveObjectId
//    second - список slaveObjects - табличек привязанных к объекту
std::pair<std::map<db::TId, db::TIds>, db::eye::Objects> loadSlaveObjectRelations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds);

ObjectsContext loadObjectsContext(
    pqxx::transaction_base& txn, db::eye::Objects objects);

bool hasDuplicateDefaultCheck(
    pqxx::transaction_base& txn,
    const db::eye::Hypothesis& hypothesis,
    double searchRadiusMeters);

template<typename T>
using IdToRef = std::unordered_map<object::TId, const T&>;

template<typename T>
IdToRef<T> byObjectId(const std::vector<T>& objects)
{
    IdToRef<T> objectById;
    for (const auto& object: objects) {
        objectById.emplace(object.revisionId().objectId(), object);
    }
    return objectById;
}

bool goInJunction(
        const object::RoadElement& element,
        const object::RoadJunction& junction);

bool goOutJunction(
        const object::RoadElement& element,
        const object::RoadJunction& junction);

bool areParallel(
        const object::RoadElement& element,
        const DirectedPoint& point,
        geolib3::Degrees tolerance);

bool areCodirectional(
        const object::RoadElement& element,
        const DirectedPoint& point,
        geolib3::Degrees tolerance);

// Returns direction in which element is codirectional with sign.
object::RoadElement::Direction direction(
        const object::RoadElement& element,
        const DirectedPoint& point);

template<typename T>
auto range(const IdToRef<T>& idToObjects, const object::TIds& ids)
{
    return ids | boost::adaptors::transformed(
        [&](object::TId id) -> const T& {
            return idToObjects.at(id);
        }
    );
}

geolib3::BoundingBox inArea(const geolib3::Point2& point, double toleranceMeters);

object::TIds collectConditionIds(const object::RoadJunctions& junctions);
object::TIds collectElementIds(const object::RoadJunctions& junctions);
object::TIds collectJunctionIds(const object::RoadElements& elements);

geolib3::BoundingBox makeSearchBox(const geolib3::Point2& center, double searchRadiusMeter);

} // namespace maps::mrc::eye
