#pragma once

#include <maps/libs/geolib/include/point.h>
#include <maps/libs/road_graph/include/types.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/graph.h>
#include <maps/wikimap/mapspro/services/mrc/browser/lib/types.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/geometry.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/visibility.h>
#include <maps/wikimap/mapspro/services/mrc/long_tasks/export_gen/lib/graph.h>

#include <functional>
#include <optional>
#include <vector>

namespace maps::mrc::browser {

constexpr int MAX_FC = 10;
const Meters MAX_VISIBILITY_DISTANCE{export_gen::COVERAGE_METERS_MAX_PATH};
const Meters MAX_CONNECTIVITY_DISTANCE{50};

// This is a stateless abstraction to explore graph coverage via continuous
// coverage subpolylines (aka blue lines). This means it doesn't move across
// gaps in the graph coverage.
//
// Typical usage is:
//     GraphCoverageExplorer explorer(graph, coverageSelector);
//
//     const auto node = explorer.find(point); - find a nearest beginning of a coverage subpolyline represented by a Node instance
//
//     const auto backwardNodes = explorer.backward(node); - collect all connected nodes behind
//
//     const auto forwardNodes = explorer.forward(node);  - collect all connected nodes ahead
//
//     const auto optUturnNode = explorer.uturn(node);   - find a node on a reverse edge if any
//
// For more examples see graph_coverage_explorer_tests.cpp
class GraphCoverageExplorer {
public:
    // Graph coverage is represented as nodes on a road graph edge polylines.
    // Single node doesn't correspond to a single coverage subpolyline on an
    // edge. It represents a first subpolylines in a sequence of connected
    // subpolylines sharing the same feature ID.
    struct Node {
        road_graph::EdgeId edgeId;              // edge ID this node belongs to.
        common::geometry::SubPolyline coverage; // the coverage subpolyline on the edge.
        db::TId featureId;                      // feature ID which produced the coverage subpolyline.
        db::Ray ray;                            // beginning and direction of the subpolyline.
    };
    using OptNode = std::optional<Node>;
    using Nodes = std::vector<Node>;
    using CoverageSelector =
        std::function<const fb::TEdgeCoverage*(const fb::TEdge&)>;

    // Create graph coverage explorer.
    // graph            - MRC graph with road graph and coverage.
    // coverageSelector - defines which coverage sublayer to explore.
    GraphCoverageExplorer(
        const IGraph& graph,
        CoverageSelector coverageSelector)
        : graph_{graph}
        , coverageSelector_{coverageSelector}
    { }

    // Find the closest node to a specific geodetic point with respect to local
    // traffic direction.
    OptNode find(const geolib3::Point2& geoPoint,
                 int maxFc = MAX_FC,
                 Meters searchRadius = MAX_VISIBILITY_DISTANCE) const;

    // Find a node closest to a specific ray in terms of distance and relative
    // angle. Returns nullopt if there is no covered edge within searchRadius.
    OptNode find(const db::Ray& ray,
                 Meters searchRadius = MAX_VISIBILITY_DISTANCE) const;

    // Find a node which represents a coverage for a given feature. Returns
    // nullopt if a given feature ID not referenced in actual coverage.
    OptNode find(const db::Feature& feature) const;

    // Find uturn node which is on the same road but in a reverse direction.
    OptNode uturn(const Node& node) const;

    // Find adjacent backward nodes from which the specified node is reachable.
    std::vector<Node> backward(const Node& node,
                               Meters maxDistance = MAX_CONNECTIVITY_DISTANCE) const;

    // Find adjacent forward nodes which are reachable from the specified node.
    std::vector<Node> forward(const Node& node,
                              Meters maxDistance = MAX_CONNECTIVITY_DISTANCE) const;

private:
    const IGraph& graph_;
    CoverageSelector coverageSelector_;

private:
    bool isResolved(const Node& node) const;

    geolib3::Segment2 getSegment(road_graph::EdgeId edgeId,
                                 uint16_t segmentIdx) const;

    geolib3::Point2 pointAtEdgeStart(road_graph::EdgeId edgeId) const;

    geolib3::Point2 pointAtEdgeEnd(road_graph::EdgeId edgeId) const;

    Node toNode(road_graph::EdgeId edgeId,
                const fb::CoveredSubPolyline& from) const;

    bool edgeHasCoverage(road_graph::EdgeId edgeId) const;

    struct SnappedPoint;

    // If there is a topological reverse edge with coverage on it then
    // find out on which side of it the point is and choose the reverse edge
    // if it is codirected with local traffic on this side of road.
    std::optional<road_graph::EdgeId> reverseForTrafficDirection(
        const geolib3::Point2& point, const SnappedPoint& snappedPoint) const;

    // Snap point to a closest point on a given edge.
    std::optional<SnappedPoint> snapPointToEdge(
        road_graph::EdgeId edgeId,
        const geolib3::Point2& point,
        const std::optional<Meters>& maxDistance) const;

    // Collect covered edges within specifed distance near a given point.
    std::vector<road_graph::EdgeId> getCoveredEdgeIds(
        const geolib3::Point2& point,
        int maxFc = MAX_FC,
        Meters bboxSize = MAX_VISIBILITY_DISTANCE) const;

    // Snap a point to a covered edge with respect to local traffic direction.
    std::optional<SnappedPoint> snapPointToCoveredEdge(
        const geolib3::Point2& point, int maxFc, Meters searchRadius) const;

    // Snap a ray to a covered edge according it position and direction.
    std::optional<SnappedPoint> snapRayToCoveredEdge(
        db::Ray ray, Meters searchRadius) const;

    // Get covered edge subpolylines using a coverage selector set by a client.
    std::vector<fb::CoveredSubPolyline> getCoveredSubpolylines(
        road_graph::EdgeId edgeId) const;

    // Select a coverage which is closest to a given polyline position along
    // edge polyline.
    const fb::CoveredSubPolyline& selectNearestCoverage(
        road_graph::EdgeId edgeId,
        const common::geometry::PolylinePosition& position,
        const fb::CoveredSubPolyline& first,
        const fb::CoveredSubPolyline& second) const;

    // it is possible that coverage for a backward edge tip starts somewhere
    // on an its incoming edge. So, try resolve a node to an edge its coverage
    // starts from.
    // WARN: Any Node that is returned to a client must be resolved to its edge.
    //       Requests to utrun, backward, and forward with an unresolved node may
    //       lead to an unexpected result.
    Node resolveNode(const Node& node) const;

    // Returns a nearest node to a given positon starting form  polyline
    // position in the both directions. Returns nullopt if there is no
    // coverage on a given edge.
    OptNode findNearestNodeOnEdge(
        const geolib3::Point2& position,
        const road_graph::EdgeId edgeId,
        const common::geometry::PolylinePosition& snappedPosition) const;

    // Returns a subpolyline which completely covers an edge geometry.
    common::geometry::SubPolyline
        completeEdgeSubpolyline(road_graph::EdgeId edgeId) const;

    // Returns next connected node on the same edge in the backward direction.
    OptNode nextEdgeBackwardNode(const Node& node) const;

    // Returns next connected node on the same edge in the forward direction.
    OptNode nextEdgeForwardNode(const Node& node) const;

    // Check if a given node is on a backward edge tip.
    bool isBackwardTipNode(const Node& node) const;

    // Check if a given node is on a forward edge tip.
    bool isForwardTipNode(const Node& node) const;

    // Get reversed edge ID from the graph topology.
    std::optional<road_graph::EdgeId> reverse(road_graph::EdgeId) const;
};

} // namespace maps::mrc::browser
