#include <maps/wikimap/mapspro/services/mrc/eye/lib/generate_hypothesis/include/common.h>
#include <maps/wikimap/mapspro/services/mrc/eye/lib/common/include/id.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/hypothesis_gateway.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/eye/object_gateway.h>

using namespace maps::geolib3::literals;

namespace maps::mrc::eye {

namespace {

object::TIds elementIds(const object::RoadJunction& junction) { return junction.elementIds(); }
object::TIds conditionIds(const object::RoadJunction& junction) { return junction.conditionIds(); }

template<typename TObject, typename Get>
object::TIds collectIds(const std::vector<TObject>& objects, Get get)
{
    object::TIds result;

    for (const auto& object: objects) {
        const auto ids = get(object);
        result.insert(result.end(), ids.begin(), ids.end());
    }

    boost::sort(result);
    boost::unique(result);

    return result;
}

} // namespace

geolib3::BoundingBox makeSearchBox(const geolib3::Point2& center, double searchRadiusMeter) {
    const double adjustedSearchRadius = geolib3::toMercatorUnits(searchRadiusMeter, center);
    return geolib3::BoundingBox(
        center,
        2 * adjustedSearchRadius,
        2 * adjustedSearchRadius
    );
}

ObjectsContext::ObjectsContext(
    const db::eye::Objects& objects,
    const db::eye::ObjectLocations& locations,
    const std::map<db::TId, db::TIds>& objectRelations,
    const db::eye::Objects& allSlaveObjects)
{
    std::unordered_map<db::TId, db::eye::ObjectLocation> locationByObjectId;
    for (const auto& location : locations) {
        locationByObjectId.emplace(location.objectId(), location);
    }

    std::unordered_map<db::TId, db::eye::Object> slaveObjectByObjectId;
    for (const auto& object : allSlaveObjects) {
        slaveObjectByObjectId.emplace(object.id(), object);
    }

    for (const auto& object : objects) {
        auto itRelations = objectRelations.find(object.id());
        db::eye::Objects slaveObjects;
        if (itRelations != objectRelations.end()) {
            const db::TIds& slaveIds = itRelations->second;
            for (size_t i = 0; i < slaveIds.size(); i++) {
                auto itSlave = slaveObjectByObjectId.find(slaveIds[i]);
                REQUIRE(itSlave != slaveObjectByObjectId.end(),
                    "There is not slave object with id "<< slaveIds[i] << " for object " << object.id());
                slaveObjects.push_back(itSlave->second);
            }
        }
        auto itLocation = locationByObjectId.find(object.id());
        REQUIRE(itLocation != locationByObjectId.end(),
                "There is not location for object " << object.id());
        data_.emplace_back(object, itLocation->second, std::move(slaveObjects));
    }
}

bool hasDuplicateDefaultCheck(
    pqxx::transaction_base& txn,
    const db::eye::Hypothesis& hypothesis,
    double searchRadiusMeters)
{
    const geolib3::BoundingBox searchBox = makeSearchBox(
        hypothesis.mercatorPos(),
        searchRadiusMeters
    );
    db::eye::Hypotheses dbHypotheses = db::eye::HypothesisGateway(txn).load(
        db::eye::table::Hypothesis::position.intersects(searchBox) &&
        db::eye::table::Hypothesis::type.equals(hypothesis.type())
    );

    return std::any_of(dbHypotheses.begin(), dbHypotheses.end(),
        [&](const db::eye::Hypothesis& dbHypothesis) {
            return hypothesis.hasEqualAttrs(dbHypothesis);
        }
    );
}

ObjectsContext loadObjectsContext(
    pqxx::transaction_base& txn, db::eye::Objects objects)
{
    db::TIds filteredObjectIds = collectIds(objects);
    for (const auto& object: objects) {
        filteredObjectIds.push_back(object.id());
    }

    db::eye::ObjectLocations locations =
        db::eye::ObjectLocationGateway(txn).load(
            db::eye::table::ObjectLocation::objectId.in(filteredObjectIds));
    std::map<db::TId, db::TIds> relationMap;
    db::eye::Objects slaveObjects;
    std::tie(relationMap, slaveObjects) = loadSlaveObjectRelations(txn, filteredObjectIds);
    return ObjectsContext(std::move(objects), std::move(locations),
        std::move(relationMap), std::move(slaveObjects));
}

std::pair<std::map<db::TId, db::TIds>, db::eye::Objects> loadSlaveObjectRelations(
    pqxx::transaction_base& txn,
    const db::TIds& objectIds)
{
    db::eye::ObjectRelations relations = db::eye::ObjectRelationGateway(txn).load(
        db::eye::table::ObjectRelation::masterObjectId.in(objectIds) &&
        ! db::eye::table::ObjectRelation::deleted
    );

    std::map<db::TId, db::TIds> relationMap;
    db::TIds slaveObjectIds;
    for (size_t i = 0; i < relations.size(); i++) {
        const db::eye::ObjectRelation& relation = relations[i];
        const db::TId slaveObjectId = relation.slaveObjectId();
        relationMap[relation.masterObjectId()].push_back(slaveObjectId);
        slaveObjectIds.push_back(slaveObjectId);
    }

    db::eye::Objects slaveObjects =  db::eye::ObjectGateway(txn).load(
        db::eye::table::Object::id.in(slaveObjectIds)
    );
    return {relationMap, slaveObjects};
}

bool goInJunction(const object::RoadElement& element, const object::RoadJunction& junction)
{
    const object::TId junctionId = junction.revisionId().objectId();
    if (junctionId == element.startJunctionId()) {
        return !!(element.direction() & object::RoadElement::Direction::Backward);
    }
    if (junctionId == element.endJunctionId()) {
        return !!(element.direction() & object::RoadElement::Direction::Forward);
    }
    throw RuntimeError() << "element " << element.revisionId()
        << "has no incidence with junction " << junction.revisionId();
}

bool goOutJunction(const object::RoadElement& element, const object::RoadJunction& junction)
{
    const object::TId junctionId = junction.revisionId().objectId();
    if (junctionId == element.startJunctionId()) {
        return !!(element.direction() & object::RoadElement::Direction::Forward);
    }
    if (junctionId == element.endJunctionId()) {
        return !!(element.direction() & object::RoadElement::Direction::Backward);
    }
    throw RuntimeError() << "element " << element.revisionId()
        << "has no incidence with junction " << junction.revisionId();
}

bool areCodirectional(
        const object::RoadElement& element,
        const DirectedPoint& point,
        geolib3::Degrees tolerance)
{
    const geolib3::Polyline2& geom = element.geom();

    const auto closestSegment = geom.segmentAt(geom.closestPointSegmentIndex(point.mercator()));
    const auto toleranceInRadians = geolib3::toRadians(tolerance);

    const geolib3::Direction2 direction(point.heading());
    const geolib3::Direction2 segmentDirection(closestSegment);

    return (
        !!(element.direction() & object::RoadElement::Direction::Forward)
            && (geolib3::angleBetween(segmentDirection, direction) < toleranceInRadians)
    ) || (
        !!(element.direction() & object::RoadElement::Direction::Backward)
            && (geolib3::angleBetween(-segmentDirection, direction) < toleranceInRadians)
    );
}

bool areParallel(
        const object::RoadElement& element,
        const DirectedPoint& point,
        geolib3::Degrees tolerance)
{
    ASSERT(tolerance < 90_deg);

    const geolib3::Polyline2& geom = element.geom();

    const auto closestSegment = geom.segmentAt(geom.closestPointSegmentIndex(point.mercator()));
    const auto toleranceInRadians = geolib3::toRadians(tolerance);

    const geolib3::Direction2 direction(point.heading());
    const geolib3::Direction2 segmentDirection(closestSegment);

    return geolib3::angleBetween(direction, segmentDirection) < toleranceInRadians
        || geolib3::angleBetween(-direction, segmentDirection) < toleranceInRadians;
}

object::RoadElement::Direction direction(
        const object::RoadElement& element,
        const DirectedPoint& point)
{
    const geolib3::Polyline2& geom = element.geom();

    const auto closestSegment = geom.segmentAt(geom.closestPointSegmentIndex(point.mercator()));

    static constexpr auto toleranceInRadians = geolib3::toRadians(90_deg);

    const geolib3::Direction2 direction(point.heading());
    const geolib3::Direction2 segmentDirection(closestSegment);

    return geolib3::angleBetween(segmentDirection, direction) <= toleranceInRadians
        ? object::RoadElement::Direction::Forward
        : object::RoadElement::Direction::Backward;
}

geolib3::BoundingBox inArea(const geolib3::Point2& mercatorPoint, double toleranceMeters)
{
    const double radius = geolib3::toMercatorUnits(toleranceMeters, mercatorPoint);
    const double size = 2 * radius;
    return geolib3::BoundingBox(mercatorPoint, size, size);
}

object::TIds collectJunctionIds(const object::RoadElements& elements)
{
    object::TIds ids;

    for (const auto& element: elements) {
        ids.push_back(element.startJunctionId());
        ids.push_back(element.endJunctionId());
    }

    boost::sort(ids);
    boost::unique(ids);

    return ids;
}

object::TIds collectConditionIds(const object::RoadJunctions& junctions)
{
    return collectIds(junctions, conditionIds);
}

object::TIds collectElementIds(const object::RoadJunctions& junctions)
{
    return collectIds(junctions, elementIds);
}

} // namespace maps::mrc::eye
