#ifndef OSRM_ENGINE_GUIDANCE_COLLAPSING_UTILITY_HPP_
#define OSRM_ENGINE_GUIDANCE_COLLAPSING_UTILITY_HPP_

#include "guidance/turn_instruction.hpp"
#include "engine/guidance/route_step.hpp"
#include "util/bearing.hpp"
#include "util/guidance/name_announcements.hpp"

#include <boost/range/algorithm_ext/erase.hpp>
#include <cstddef>

namespace osrm::engine::guidance
{

using RouteSteps = std::vector<RouteStep>;
using RouteStepIterator = typename RouteSteps::iterator;
const constexpr std::size_t MIN_END_OF_ROAD_INTERSECTIONS = std::size_t{2};
const constexpr double MAX_COLLAPSE_DISTANCE = 30.0;
// a bit larger than 100 to avoid oscillation in tests
const constexpr double NAME_SEGMENT_CUTOFF_LENGTH = 105.0;
const double constexpr STRAIGHT_ANGLE = 180.;

// check if a step is completely without turn type
inline bool hasTurnType(const RouteStep &step)
{
    return step.maneuver.instruction.type != osrm::guidance::TurnType::NoTurn;
}
inline bool hasWaypointType(const RouteStep &step)
{
    return step.maneuver.waypoint_type != WaypointType::None;
}

// skip backwards through possibly disabled turns until we find a turn type (or the first step)
inline RouteStepIterator findPreviousTurn(RouteStepIterator current_step)
{
    BOOST_ASSERT(!hasWaypointType(*current_step));
    // find the first element preceding the current step that has an actual turn type (not
    // necessarily announced)
    do
    {
        // safety to do this loop is asserted in collapseTurnInstructions
        --current_step;
    } while (!hasTurnType(*current_step) && !hasWaypointType(*current_step));
    return current_step;
}

// skip forwards over possible NoTurn entries (e.g. ferries) until we find the next instruction with
// a turn type
inline RouteStepIterator findNextTurn(RouteStepIterator current_step)
{
    BOOST_ASSERT(!hasWaypointType(*current_step));
    // find the first element preceding the current step that has an actual turn type (not
    // necessarily announced)
    do
    {
        // safety to do this loop is asserted in collapseTurnInstructions
        ++current_step;
    } while (!hasTurnType(*current_step) && !hasWaypointType(*current_step));
    return current_step;
}

// alias for comparisons
inline bool hasTurnType(const RouteStep &step, const osrm::guidance::TurnType::Enum type)
{
    return type == step.maneuver.instruction.type;
}
// alias for comparisons
inline bool hasModifier(const RouteStep &step,
                        const osrm::guidance::DirectionModifier::Enum modifier)
{
    return modifier == step.maneuver.instruction.direction_modifier;
}
inline bool hasLanes(const RouteStep &step)
{
    return step.intersections.front().lanes.lanes_in_turn != 0;
}

// alias for detectors, gives the number of connected roads
inline std::size_t numberOfAvailableTurns(const RouteStep &step)
{
    return step.intersections.front().entry.size();
}
// alias for detectors, counts only the allowed turns
inline std::size_t numberOfAllowedTurns(const RouteStep &step)
{
    return std::count(
        step.intersections.front().entry.begin(), step.intersections.front().entry.end(), true);
}
// traffic lights are very specifically modelled. Sometimes we need to skip them. All checks need to
// fulfill:
inline bool isTrafficLightStep(const RouteStep &step)
{
    return hasTurnType(step, osrm::guidance::TurnType::Suppressed) &&
           numberOfAvailableTurns(step) == 2 && numberOfAllowedTurns(step) == 1;
}

// alias for readability
inline void setInstructionType(RouteStep &step, const osrm::guidance::TurnType::Enum type)
{
    step.maneuver.instruction.type = type;
}

// alias for readability
inline void setModifier(RouteStep &step, const osrm::guidance::DirectionModifier::Enum modifier)
{
    step.maneuver.instruction.direction_modifier = modifier;
}

// alias for readability
inline bool haveSameMode(const RouteStep &lhs, const RouteStep &rhs)
{
    return lhs.mode == rhs.mode;
}

// alias for readability
inline bool haveSameMode(const RouteStep &first, const RouteStep &second, const RouteStep &third)
{
    return haveSameMode(first, second) && haveSameMode(second, third);
}

// alias for readability
inline bool haveSameName(const RouteStep &lhs, const RouteStep &rhs)
{
    const auto has_name_or_ref = [](auto const &step)
    { return !step.name.empty() || !step.ref.empty(); };

    // make sure empty is not involved
    if (!has_name_or_ref(lhs) || !has_name_or_ref(rhs))
    {
        return false;
    }

    // easy check to not go over the strings if not necessary
    else if (lhs.name_id == rhs.name_id)
        return true;

    // ok, bite the sour grape and check the strings already
    else
        return !util::guidance::requiresNameAnnounced(lhs.name,
                                                      lhs.ref,
                                                      lhs.pronunciation,
                                                      lhs.exits,
                                                      rhs.name,
                                                      rhs.ref,
                                                      rhs.pronunciation,
                                                      rhs.exits);
}

// alias for readability, both turn right | left
inline bool areSameSide(const RouteStep &lhs, const RouteStep &rhs)
{
    const auto is_left = [](const RouteStep &step)
    {
        return hasModifier(step, osrm::guidance::DirectionModifier::Straight) ||
               hasLeftModifier(step.maneuver.instruction);
    };

    const auto is_right = [](const RouteStep &step)
    {
        return hasModifier(step, osrm::guidance::DirectionModifier::Straight) ||
               hasRightModifier(step.maneuver.instruction);
    };

    return (is_left(lhs) && is_left(rhs)) || (is_right(lhs) && is_right(rhs));
}

// do this after invalidating any steps to compress the step array again
[[nodiscard]] inline std::vector<RouteStep> removeNoTurnInstructions(std::vector<RouteStep> steps)
{
    // finally clean up the post-processed instructions.
    // Remove all invalid instructions from the set of instructions.
    // An instruction is invalid, if its NO_TURN and has WaypointType::None.
    // Two valid NO_TURNs exist in each leg in the form of Depart/Arrive

    // keep valid instructions
    const auto not_is_valid = [](const RouteStep &step)
    {
        return step.maneuver.instruction == osrm::guidance::TurnInstruction::NO_TURN() &&
               step.maneuver.waypoint_type == WaypointType::None;
    };

    steps.erase(std::remove_if(std::begin(steps), std::end(steps), not_is_valid), std::end(steps));

    // the steps should still include depart and arrive at least
    BOOST_ASSERT(steps.size() >= 2);

    BOOST_ASSERT(steps.front().intersections.size() >= 1);
    BOOST_ASSERT(steps.front().intersections.front().bearings.size() == 1);
    BOOST_ASSERT(steps.front().intersections.front().entry.size() == 1);
    BOOST_ASSERT(steps.front().maneuver.waypoint_type == WaypointType::Depart);

    BOOST_ASSERT(steps.back().intersections.size() == 1);
    BOOST_ASSERT(steps.back().intersections.front().bearings.size() == 1);
    BOOST_ASSERT(steps.back().intersections.front().entry.size() == 1);
    BOOST_ASSERT(steps.back().maneuver.waypoint_type == WaypointType::Arrive);

    return steps;
}

inline double totalTurnAngle(const RouteStep &entry_step, const RouteStep &exit_step)
{
    if (entry_step.geometry_begin > exit_step.geometry_begin)
        return totalTurnAngle(exit_step, entry_step);

    const auto &exit_intersection = exit_step.intersections.front();
    const auto &entry_intersection = entry_step.intersections.front();
    if ((exit_intersection.out >= exit_intersection.bearings.size()) ||
        (entry_intersection.in >= entry_intersection.bearings.size()))
        return entry_intersection.bearings[entry_intersection.out];

    const auto exit_step_exit_bearing = exit_intersection.bearings[exit_intersection.out];
    const auto entry_step_entry_bearing =
        util::bearing::reverse(entry_intersection.bearings[entry_intersection.in]);

    const double total_angle =
        util::bearing::angleBetween(entry_step_entry_bearing, exit_step_exit_bearing);

    return total_angle;
}

// check bearings for u-turns.
// since bearings are wrapped around at 0 (we only support 0,360), we need to do some minor math to
// check if bearings `a` and `b` go in opposite directions. In general we accept some minor
// deviations for u-turns.
inline bool bearingsAreReversed(const double bearing_in, const double bearing_out)
{
    // Nearly perfectly reversed angles have a difference close to 180 degrees (straight)
    const double left_turn_angle = [&]()
    {
        if (0 <= bearing_out && bearing_out <= bearing_in)
            return bearing_in - bearing_out;
        return bearing_in + 360 - bearing_out;
    }();
    return util::angularDeviation(left_turn_angle, 180) <= 35;
}

// Returns true if the specified step has only one intersection
inline bool hasSingleIntersection(const RouteStep &step)
{
    return (step.intersections.size() == 1);
}

// Returns true if the specified angle is a wider straight turn
inline bool isWiderStraight(const double angle) { return (angle >= 125 && angle <= 235); }

// Returns the straightest intersecting edge turn for the specified step
inline double getStraightestIntersectingEdgeTurn(const RouteStep &step)
{
    const auto &intersection = step.intersections.front();
    const double bearing_in = util::bearing::reverse(intersection.bearings[intersection.in]);
    double staightest_turn = 360.;
    double staightest_delta = 360.;

    for (std::size_t i = 0; i < intersection.bearings.size(); ++i)
    {
        // Skip the in, out, and non-traversable edges
        if ((i == intersection.in) || (i == intersection.out) || !intersection.entry.at(i))
            continue;

        double intersecting_turn =
            util::bearing::angleBetween(bearing_in, intersection.bearings.at(i));
        double straight_delta = util::angularDeviation(intersecting_turn, STRAIGHT_ANGLE);

        if (straight_delta < staightest_delta)
        {
            staightest_delta = straight_delta;
            staightest_turn = intersecting_turn;
        }
    }
    return staightest_turn;
}

// Returns true if the specified step has the straightest turn as compared to
// the intersecting edges
inline bool hasStraightestTurn(const RouteStep &step)
{
    const auto &intersection = step.intersections.front();
    const double path_turn =
        util::bearing::angleBetween(util::bearing::reverse(intersection.bearings[intersection.in]),
                                    intersection.bearings[intersection.out]);

    // Path turn must be a wider straight
    if (isWiderStraight(path_turn))
    {
        const double straightest_intersecting_turn = getStraightestIntersectingEdgeTurn(step);
        const double path_straight_delta = util::angularDeviation(path_turn, STRAIGHT_ANGLE);
        const double intersecting_straight_delta =
            util::angularDeviation(straightest_intersecting_turn, STRAIGHT_ANGLE);
        const double path_intersecting_delta =
            util::angularDeviation(path_turn, straightest_intersecting_turn);

        // Add some fuzz - the delta between the path and intersecting turn must be greater
        // than 10 in order to consider using the intersecting turn as the straightest
        return ((path_intersecting_delta > 10.)
                    ? (path_straight_delta <= intersecting_straight_delta)
                    : true);
    }

    return false;
}

} // namespace osrm::engine::guidance

#endif /* OSRM_ENGINE_GUIDANCE_COLLAPSING_UTILITY_HPP_ */
