#pragma once

#include "geometry.h"

#include <maps/libs/edge_persistent_index/include/persistent_index.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/point.h>
#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/geolib/include/direction.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/spatial_relation.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/deprecated/localeutils/include/locale.h>

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

#include <functional>
#include <unordered_map>
#include <set>
#include <unordered_set>
#include <cmath>

// All coordinates are (lon, lat)

namespace maps::mrc::gen_targets {

using Id = int64_t;
using EdgeId = Id;
using AdId = Id;
using LoopId = Id;

using Seconds = double;
using Meters = double;

struct Edge {
    EdgeId id;

    road_graph::LongEdgeId persistentId{};
    geolib3::Polyline2 geom;
    int fc;
    Meters length; // edge length in meters
    Seconds time; // = length / (average speed for such fc)
    bool isUTurn; // true if edge is a U turn connector
    bool isToll; // true if driver should pay for this road
    bool isJunction;
    bool endsWithBarrier; // true if this edge has barrier deadend in
                          // the original static_graph
    bool isTarget;

    std::vector<EdgeId> inEdges;
    std::vector<EdgeId> outEdges;
    std::vector<Seconds> outEdgesPenalties; // outEdges[i] has penalty
                                            // = outEdgesPenalties[i]

    geolib3::Direction2 incomingDirection() const {
        return geolib3::Direction2(geom.points()[1] - geom.points()[0]);
    }
    geolib3::Direction2 incomingDirectionReverse() const {
        return geolib3::Direction2(geom.points()[0] - geom.points()[1]);
    }
    geolib3::Direction2 outgoingDirection() const {
        return geolib3::Direction2((geom.points().back()) - *(geom.points().end()-2));
    }
    geolib3::Direction2 outGoingDirectionReverse() const {
        return geolib3::Direction2(*(geom.points().end()-2) - geom.points().back());
    }

    geolib3::Point2 middlePoint() const;
};
using Edges = std::unordered_map<EdgeId, Edge>;

struct LoopEdge {
    EdgeId edgeId;
    LoopId loopId;
    bool operator==(const LoopEdge& other) const {
        return edgeId == other.edgeId && loopId == other.loopId;
    }
};

using LoopsPath = std::vector<LoopEdge>;
using LoopsPaths = std::vector<LoopsPath>;

// @brief one edge from a connected route
struct PathEdge {
    EdgeId edgeId;
    bool isUsefulAsTarget; // some edges from a task path are not useful
                           // as targets and used only
                           // to keep the path connected
};

// @brief the same as PathEdge, but contains edges data instead of pointers
struct PathEdgeWithData {
    Edge edge;
    bool isUsefulAsTarget; // some edges from a task path are not useful
                           // as targets and used only
                           // to keep the path connected
};

struct DistrictName {
    std::string name;
    Locale locale;
};

struct MultiDistrict {
    geolib3::MultiPolygon2 area;
    DistrictName name;
};

struct District {
    geolib3::Polygon2 area;
    DistrictName name;
};

using Path = std::vector<PathEdge>;
using Paths = std::vector<Path>;

using PathWithData = std::vector<PathEdgeWithData>;
using PathsWithData = std::vector<PathWithData>;

using EdgeCRef = std::reference_wrapper<const Edge>;
using EdgeCRefVec = std::vector<EdgeCRef>;

class RoadNetworkData {
public:
    RoadNetworkData(Edges&& edges);

    boost::iterator_range<Edges::const_iterator> edges() const;
    const Edge& edge(EdgeId id) const;
    const Edges& getEdges() const { return edges_; };
    std::pair<EdgeCRefVec, std::vector<Seconds>> getOutEdgesAndPenalties(EdgeId edgeId) const;

    void deleteEdge(EdgeId edgeId);
    void deleteEdges(const std::unordered_set<EdgeId>& edgeIds);

    // @brief Marks edges as targets.
    // Returns selected roads lengths sum.
    // Uses only allowedEdges if provided.
    // Doesn't mark alreadyUsedEdges.
    Meters selectTargetsAsPolygon(
        const geolib3::Polygon2& polygon,
        int maxRoadFC,
        const std::unordered_set<EdgeId>& prohibitedEdges);

    void addTarget(EdgeId edgeId) { edges_.at(edgeId).isTarget = true; }
    void removeTarget(EdgeId edgeId) { edges_.at(edgeId).isTarget = false; }
    void eraseTargets() {
        for (auto& it : edges_) {
            it.second.isTarget = false;
        }
    }

    std::vector<EdgeId> getEdgesWithinPolygon(
        const geolib3::Polygon2& polygon,
        int minRoadFC,
        int maxRoadFC,
        const std::unordered_set<EdgeId>& prohibitedEdges) const;

    Meters getPathLength(
        Path path,
        const std::unordered_set<EdgeId>& usedEdges,
        const std::unordered_multiset<EdgeId>& probablyUsedEdges) const{
        Meters length = 0;
        for (auto& pathEdge : path) {
            if (!edges_.at(pathEdge.edgeId).isTarget
                || usedEdges.count(pathEdge.edgeId)
                || probablyUsedEdges.count(pathEdge.edgeId)) {
                length += edge(pathEdge.edgeId).length;
            }
        }
        return length;
    }

    // returns true if edges are connected and the maneuver is allowed
    bool edgesAreConnected(EdgeId edge1, EdgeId edge2) const;

    Seconds getManeuverPenalty(EdgeId from, EdgeId to) const;

    // additional penalties. In this class they are always 0
    virtual Seconds getUnrecommendedManeuversPenalty(EdgeId /* from */, EdgeId /* to */) const { return 0; }

    std::unordered_set<EdgeId> getSetOfEdgeIds() const;

protected:
    void calculateOutputPenalties(Edge& edge);

protected:
    Edges edges_;
};

PathWithData convertPathToPathWithData(const RoadNetworkData& roadNetworkData,
                                       const Path& path);

// @brief split MultiDistrict[] districts into District[] and return
// only the districts which belong to the masked area
std::vector<District> filterDistricts(std::vector<MultiDistrict>& districts,
                                      const boost::optional<MultiDistrict>& mask);

Meters getPathLength(const RoadNetworkData& roadNetwork,
                     const LoopsPath& path);
Meters getPathLength(const RoadNetworkData& roadNetwork,
                     const std::vector<EdgeId>& path);


struct EdgesGeometryFilter {
    EdgesGeometryFilter(std::vector<geolib3::Polygon2> geoms, std::vector<int> fcs_)
        : geodeticGeoms(std::move(geoms))
        , fcs(std::move(fcs_))
    {}

    std::vector<geolib3::Polygon2> geodeticGeoms;
    std::vector<int> fcs;
};

} // namespace maps::mrc::gen_targets
