#include "guidance/turn_handler.hpp"
#include "guidance/constants.hpp"

#include "util/bearing.hpp"
#include "util/guidance/name_announcements.hpp"

#include <algorithm>
#include <limits>
#include <optional>
#include <utility>

#include <boost/assert.hpp>

using osrm::util::angularDeviation;

namespace
{

using namespace osrm::guidance;
// given two adjacent roads in clockwise order and `road1` being a candidate for a fork,
// return false, if next road `road2` is also a fork candidate or
// return true, if `road2` is not a suitable fork candidate and thus, `road1` the outermost fork
bool isOutermostForkCandidate(const ConnectedRoad &road1, const ConnectedRoad &road2)
{
    const auto angle_between_next_road_and_straight = angularDeviation(road2.angle, STRAIGHT_ANGLE);
    const auto angle_between_prev_road_and_next = angularDeviation(road1.angle, road2.angle);
    const auto angle_between_prev_road_and_straight = angularDeviation(road1.angle, STRAIGHT_ANGLE);

    // a road is a fork candidate if it is close to straight or
    // close to a street that goes close to straight
    // (reverse to find fork non-candidate)
    if (angle_between_next_road_and_straight > NARROW_TURN_ANGLE)
    {
        if (angle_between_prev_road_and_next > NARROW_TURN_ANGLE ||
            angle_between_prev_road_and_straight > GROUP_ANGLE)
        {
            return true;
        }
    }
    return false;
}

bool isEndOfRoad(const ConnectedRoad &,
                 const ConnectedRoad &possible_right_turn,
                 const ConnectedRoad &possible_left_turn)
{
    return angularDeviation(possible_right_turn.angle, 90) < NARROW_TURN_ANGLE &&
           angularDeviation(possible_left_turn.angle, 270) < NARROW_TURN_ANGLE &&
           angularDeviation(possible_right_turn.angle, possible_left_turn.angle) >
               2 * NARROW_TURN_ANGLE;
}

template <typename InputIt>
InputIt findOutermostForkCandidate(const InputIt &begin, const InputIt &end)
{
    static_assert(std::is_base_of<std::input_iterator_tag,
                                  typename std::iterator_traits<InputIt>::iterator_category>::value,
                  "findOutermostForkCandidate() only accepts input iterators");
    const auto outermost = std::adjacent_find(begin, end, isOutermostForkCandidate);
    if (outermost != end)
    {
        return outermost;
    }
    // if all roads are part of a fork, set `candidate` to the last road
    else
    {
        return outermost - 1;
    }
}
} // namespace

namespace osrm::guidance
{

// a wrapper to handle road indices of forks at intersections
TurnHandler::Fork::Fork(const Intersection::iterator intersection_base,
                        const Intersection::iterator begin,
                        const Intersection::iterator end)
    : intersection_base(intersection_base), begin(begin), end(end), size(std::distance(begin, end))
{
    BOOST_ASSERT(begin < end);
    BOOST_ASSERT(size == 2 || size == 3);
}

ConnectedRoad &TurnHandler::Fork::getRight() { return *begin; }
ConnectedRoad &TurnHandler::Fork::getLeft() { return *(end - 1); }
ConnectedRoad &TurnHandler::Fork::getMiddle()
{
    BOOST_ASSERT(size == 3);
    return *(begin + 1);
}
ConnectedRoad &TurnHandler::Fork::getRight() const { return *begin; }
ConnectedRoad &TurnHandler::Fork::getLeft() const { return *(end - 1); }
ConnectedRoad &TurnHandler::Fork::getMiddle() const
{
    BOOST_ASSERT(size == 3);
    return *(begin + 1);
}
std::size_t TurnHandler::Fork::getRightIndex() const
{
    return std::distance(intersection_base, begin);
}
std::size_t TurnHandler::Fork::getLeftIndex() const
{
    return std::distance(intersection_base, end) - 1;
}

TurnHandler::TurnHandler(const util::NodeBasedDynamicGraph &node_based_graph,
                         const extractor::EdgeBasedNodeDataContainer &node_data_container,
                         const std::vector<util::Coordinate> &coordinates,
                         const extractor::CompressedEdgeContainer &compressed_geometries,
                         const extractor::RestrictionMap &node_restriction_map,
                         const extractor::ObstacleMap &obstacle_nodes,
                         const extractor::TurnLanesIndexedArray &turn_lanes_data,
                         const extractor::StringTable &string_table,
                         const extractor::SuffixTable &street_name_suffix_table)
    : IntersectionHandler(node_based_graph,
                          node_data_container,
                          coordinates,
                          compressed_geometries,
                          node_restriction_map,
                          obstacle_nodes,
                          turn_lanes_data,
                          string_table,
                          street_name_suffix_table)
{
}

bool TurnHandler::canProcess(const NodeID, const EdgeID, const Intersection &) const
{
    return true;
}

// Handles and processes possible turns
// Input parameters describe an intersection as described in
// #IntersectionExplanation@intersection_handler.hpp
Intersection
TurnHandler::operator()(const NodeID, const EdgeID via_edge, Intersection intersection) const
{
    if (intersection.size() == 1)
        return handleOneWayTurn(std::move(intersection));

    // if u-turn is allowed, set the turn type of intersection[0] to its basic type and u-turn
    if (intersection[0].entry_allowed)
    {
        intersection[0].instruction = {findBasicTurnType(via_edge, intersection[0]),
                                       DirectionModifier::UTurn};
    }

    if (intersection.size() == 2)
        return handleTwoWayTurn(via_edge, std::move(intersection));

    if (intersection.size() == 3)
        return handleThreeWayTurn(via_edge, std::move(intersection));

    return handleComplexTurn(via_edge, std::move(intersection));
}

Intersection TurnHandler::handleOneWayTurn(Intersection intersection) const
{
    BOOST_ASSERT(intersection[0].angle < 0.001);
    return intersection;
}

Intersection TurnHandler::handleTwoWayTurn(const EdgeID via_edge, Intersection intersection) const
{
    BOOST_ASSERT(intersection[0].angle < 0.001);
    intersection[1].instruction =
        getInstructionForObvious(intersection.size(), via_edge, false, intersection[1]);

    return intersection;
}

// checks whether it is obvious to turn on `road` coming from `via_edge` while there is an`other`
// road at the same intersection
bool TurnHandler::isObviousOfTwo(const EdgeID via_edge,
                                 const ConnectedRoad &road,
                                 const ConnectedRoad &other) const
{
    const auto &via_data =
        node_data_container.GetAnnotation(node_based_graph.GetEdgeData(via_edge).annotation_data);
    const auto &road_data =
        node_data_container.GetAnnotation(node_based_graph.GetEdgeData(road.eid).annotation_data);
    const auto &via_classification =
        node_based_graph.GetEdgeData(via_edge).flags.road_classification;
    const auto &road_classification =
        node_based_graph.GetEdgeData(road.eid).flags.road_classification;
    const auto &other_classification =
        node_based_graph.GetEdgeData(other.eid).flags.road_classification;

    // if one of the given roads is obvious by class, obviousness is trivial
    if (obviousByRoadClass(via_classification, road_classification, other_classification))
    {
        return true;
    }
    else if (obviousByRoadClass(via_classification, other_classification, road_classification))
    {
        return false;
    }

    const bool turn_is_perfectly_straight =
        angularDeviation(road.angle, STRAIGHT_ANGLE) < std::numeric_limits<double>::epsilon();
    const auto &via_name_empty = string_table.GetNameForID(via_data.string_view_id).empty();
    if (!via_name_empty)
    {
        const auto same_name = !util::guidance::requiresNameAnnounced(via_data.string_view_id,
                                                                      road_data.string_view_id,
                                                                      string_table,
                                                                      street_name_suffix_table);
        if (turn_is_perfectly_straight && same_name)
        {
            return true;
        }
    }

    const bool is_much_narrower_than_other =
        angularDeviation(other.angle, STRAIGHT_ANGLE) /
                angularDeviation(road.angle, STRAIGHT_ANGLE) >
            INCREASES_BY_FOURTY_PERCENT &&
        angularDeviation(angularDeviation(other.angle, STRAIGHT_ANGLE),
                         angularDeviation(road.angle, STRAIGHT_ANGLE)) > FUZZY_ANGLE_DIFFERENCE;

    return is_much_narrower_than_other;
}

bool TurnHandler::hasObvious(const EdgeID &via_edge, const Fork &fork) const
{
    auto obvious_road = std::adjacent_find(fork.begin,
                                           fork.end,
                                           [&, this](const auto &a, const auto &b) {
                                               return this->isObviousOfTwo(via_edge, a, b) ||
                                                      this->isObviousOfTwo(via_edge, b, a);
                                           });
    // return whether an obvious road was found
    return obvious_road != fork.end;
}

// handles a turn at a three-way intersection _coming from_ `via_edge`
// with `intersection` as described as in #IntersectionExplanation@intersection_handler.hpp
Intersection TurnHandler::handleThreeWayTurn(const EdgeID via_edge, Intersection intersection) const
{
    BOOST_ASSERT(intersection.size() == 3);
    const auto obvious_index = findObviousTurn(via_edge, intersection);
    BOOST_ASSERT(intersection[0].angle < 0.001);
    /* Two nearly straight turns -> FORK
               OOOOOOO
             /
      IIIIII
             \
               OOOOOOO
     */

    const auto all_links = std::all_of(
        intersection.begin(),
        intersection.end(),
        [this](const auto &road)
        { return node_based_graph.GetEdgeData(road.eid).flags.road_classification.IsLinkClass(); });

    auto fork = findFork(via_edge, intersection);
    if (fork && (all_links || obvious_index == 0))
    {
        assignFork(via_edge, fork->getLeft(), fork->getRight());
    }

    /*  T Intersection

        OOOOOOO T OOOOOOOO
                I
                I
                I
     */
    else if (isEndOfRoad(intersection[0], intersection[1], intersection[2]) && obvious_index == 0)
    {
        if (intersection[1].entry_allowed)
        {
            if (TurnType::OnRamp != findBasicTurnType(via_edge, intersection[1]))
                intersection[1].instruction = {TurnType::EndOfRoad, DirectionModifier::Right};
            else
                intersection[1].instruction = {TurnType::OnRamp, DirectionModifier::Right};
        }
        if (intersection[2].entry_allowed)
        {
            if (TurnType::OnRamp != findBasicTurnType(via_edge, intersection[2]))
                intersection[2].instruction = {TurnType::EndOfRoad, DirectionModifier::Left};
            else
                intersection[2].instruction = {TurnType::OnRamp, DirectionModifier::Left};
        }
    }
    else if (obvious_index != 0) // has an obvious continuing road/obvious turn
    {
        const auto direction_at_one = getTurnDirection(intersection[1].angle);
        const auto direction_at_two = getTurnDirection(intersection[2].angle);
        if (obvious_index == 1)
        {
            intersection[1].instruction =
                getInstructionForObvious(3,
                                         via_edge,
                                         isThroughStreet(1,
                                                         intersection,
                                                         node_based_graph,
                                                         node_data_container,
                                                         string_table,
                                                         street_name_suffix_table),
                                         intersection[1]);
            const auto second_direction = (direction_at_one == direction_at_two &&
                                           direction_at_two == DirectionModifier::Straight)
                                              ? DirectionModifier::SlightLeft
                                              : direction_at_two;

            intersection[2].instruction = {findBasicTurnType(via_edge, intersection[2]),
                                           second_direction};
        }
        else
        {
            BOOST_ASSERT(obvious_index == 2);
            intersection[2].instruction =
                getInstructionForObvious(3,
                                         via_edge,
                                         isThroughStreet(2,
                                                         intersection,
                                                         node_based_graph,
                                                         node_data_container,
                                                         string_table,
                                                         street_name_suffix_table),
                                         intersection[2]);
            const auto first_direction = (direction_at_one == direction_at_two &&
                                          direction_at_one == DirectionModifier::Straight)
                                             ? DirectionModifier::SlightRight
                                             : direction_at_one;

            intersection[1].instruction = {findBasicTurnType(via_edge, intersection[1]),
                                           first_direction};
        }
    }
    else // basic turn assignment
    {
        intersection[1].instruction = {findBasicTurnType(via_edge, intersection[1]),
                                       getTurnDirection(intersection[1].angle)};
        intersection[2].instruction = {findBasicTurnType(via_edge, intersection[2]),
                                       getTurnDirection(intersection[2].angle)};
    }
    return intersection;
}

Intersection TurnHandler::handleComplexTurn(const EdgeID via_edge, Intersection intersection) const
{
    const std::size_t obvious_index = findObviousTurn(via_edge, intersection);
    const auto fork = findFork(via_edge, intersection);

    const auto straightmost = intersection.findClosestTurn(STRAIGHT_ANGLE);
    const auto straightmost_index = std::distance(intersection.begin(), straightmost);
    const auto straightmost_angle_dev = angularDeviation(straightmost->angle, STRAIGHT_ANGLE);

    // check whether the obvious choice is actually a through street
    if (obvious_index != 0)
    {
        intersection[obvious_index].instruction =
            getInstructionForObvious(intersection.size(),
                                     via_edge,
                                     isThroughStreet(obvious_index,
                                                     intersection,
                                                     node_based_graph,
                                                     node_data_container,
                                                     string_table,
                                                     street_name_suffix_table),
                                     intersection[obvious_index]);

        // assign left/right turns
        intersection = assignLeftTurns(via_edge, std::move(intersection), obvious_index);
        intersection = assignRightTurns(via_edge, std::move(intersection), obvious_index);
    }
    else if (fork) // found fork
    {
        if (fork->size == 2)
        {
            const auto left_classification =
                node_based_graph.GetEdgeData(fork->getLeft().eid).flags.road_classification;
            const auto right_classification =
                node_based_graph.GetEdgeData(fork->getRight().eid).flags.road_classification;
            if (canBeSeenAsFork(left_classification, right_classification))
            {
                assignFork(via_edge, fork->getLeft(), fork->getRight());
            }
            else if (left_classification.GetPriority() > right_classification.GetPriority())
            {
                fork->getRight().instruction = getInstructionForObvious(
                    intersection.size(), via_edge, false, fork->getRight());
                fork->getLeft().instruction = {findBasicTurnType(via_edge, fork->getLeft()),
                                               DirectionModifier::SlightLeft};
            }
            else
            {
                fork->getLeft().instruction =
                    getInstructionForObvious(intersection.size(), via_edge, false, fork->getLeft());
                fork->getRight().instruction = {findBasicTurnType(via_edge, fork->getRight()),
                                                DirectionModifier::SlightRight};
            }
        }
        else if (fork->size == 3)
        {
            assignFork(via_edge,
                       fork->getLeft(),
                       // middle fork road
                       fork->getMiddle(),
                       fork->getRight());
        }

        intersection = assignLeftTurns(via_edge, std::move(intersection), fork->getLeftIndex());
        intersection = assignRightTurns(via_edge, std::move(intersection), fork->getRightIndex());
    }
    else if (straightmost_angle_dev < FUZZY_ANGLE_DIFFERENCE && !straightmost->entry_allowed)
    {
        // invalid straight turn
        intersection = assignLeftTurns(via_edge, std::move(intersection), straightmost_index);
        intersection = assignRightTurns(via_edge, std::move(intersection), straightmost_index);
    }
    // no straight turn
    else if (straightmost->angle > 180)
    {
        // at most three turns on either side
        intersection = assignLeftTurns(via_edge, std::move(intersection), straightmost_index - 1);
        intersection = assignRightTurns(via_edge, std::move(intersection), straightmost_index);
    }
    else if (straightmost->angle < 180)
    {
        intersection = assignLeftTurns(via_edge, std::move(intersection), straightmost_index);
        intersection = assignRightTurns(via_edge, std::move(intersection), straightmost_index + 1);
    }
    else
    {
        assignTrivialTurns(via_edge, intersection, 1, intersection.size());
    }

    return intersection;
}

// Assignment of left turns hands of to right turns.
// To do so, we mirror every road segment and reverse the order.
// After the mirror and reversal / we assign right turns and
// mirror again and restore the original order.
Intersection TurnHandler::assignLeftTurns(const EdgeID via_edge,
                                          Intersection intersection,
                                          const std::size_t starting_at) const
{
    BOOST_ASSERT(starting_at < intersection.size());
    const auto switch_left_and_right = [](Intersection &intersection)
    {
        BOOST_ASSERT(!intersection.empty());

        for (auto &road : intersection)
            road.mirror();

        std::reverse(intersection.begin() + 1, intersection.end());
    };

    switch_left_and_right(intersection);
    // account for the u-turn in the beginning
    const auto count = intersection.size() - starting_at;
    intersection = assignRightTurns(via_edge, std::move(intersection), count);
    switch_left_and_right(intersection);

    return intersection;
}

// can only assign three turns
Intersection TurnHandler::assignRightTurns(const EdgeID via_edge,
                                           Intersection intersection,
                                           const std::size_t up_to) const
{
    BOOST_ASSERT(up_to <= intersection.size());
    const auto count_valid = [&intersection, up_to]()
    {
        std::size_t count = 0;
        for (std::size_t i = 1; i < up_to; ++i)
            if (intersection[i].entry_allowed)
                ++count;
        return count;
    };
    if (up_to <= 1 || count_valid() == 0)
        return intersection;
    // handle single turn
    if (up_to == 2)
    {
        assignTrivialTurns(via_edge, intersection, 1, up_to);
    }
    // Handle Turns 1-3
    else if (up_to == 3)
    {
        const auto first_direction = getTurnDirection(intersection[1].angle);
        const auto second_direction = getTurnDirection(intersection[2].angle);
        if (first_direction == second_direction)
        {
            // conflict
            handleDistinctConflict(via_edge, intersection[2], intersection[1]);
        }
        else
        {
            assignTrivialTurns(via_edge, intersection, 1, up_to);
        }
    }
    // Handle Turns 1-4
    else if (up_to == 4)
    {
        const auto first_direction = getTurnDirection(intersection[1].angle);
        const auto second_direction = getTurnDirection(intersection[2].angle);
        const auto third_direction = getTurnDirection(intersection[3].angle);
        if (first_direction != second_direction && second_direction != third_direction)
        {
            // due to the circular order, the turn directions are unique
            // first_direction != third_direction is implied
            BOOST_ASSERT(first_direction != third_direction);
            assignTrivialTurns(via_edge, intersection, 1, up_to);
        }
        else if (2 >= (intersection[1].entry_allowed + intersection[2].entry_allowed +
                       intersection[3].entry_allowed))
        {
            // at least a single invalid
            if (!intersection[3].entry_allowed)
            {
                handleDistinctConflict(via_edge, intersection[2], intersection[1]);
            }
            else if (!intersection[1].entry_allowed)
            {
                handleDistinctConflict(via_edge, intersection[3], intersection[2]);
            }
            else // handles one-valid as well as two valid (1,3)
            {
                handleDistinctConflict(via_edge, intersection[3], intersection[1]);
            }
        }
        // From here on out, intersection[1-3].entry_allowed has to be true (Otherwise we would have
        // triggered 2>= ...)
        //
        // Conflicting Turns, but at least farther than what we call a narrow turn
        else if (angularDeviation(intersection[1].angle, intersection[2].angle) >=
                     NARROW_TURN_ANGLE &&
                 angularDeviation(intersection[2].angle, intersection[3].angle) >=
                     NARROW_TURN_ANGLE)
        {
            BOOST_ASSERT(intersection[1].entry_allowed && intersection[2].entry_allowed &&
                         intersection[3].entry_allowed);

            intersection[1].instruction = {findBasicTurnType(via_edge, intersection[1]),
                                           DirectionModifier::SharpRight};
            intersection[2].instruction = {findBasicTurnType(via_edge, intersection[2]),
                                           DirectionModifier::Right};
            intersection[3].instruction = {findBasicTurnType(via_edge, intersection[3]),
                                           DirectionModifier::SlightRight};
        }
        else if (((first_direction == second_direction && second_direction == third_direction) ||
                  (first_direction == second_direction &&
                   angularDeviation(intersection[2].angle, intersection[3].angle) < GROUP_ANGLE) ||
                  (second_direction == third_direction &&
                   angularDeviation(intersection[1].angle, intersection[2].angle) < GROUP_ANGLE)))
        {
            BOOST_ASSERT(intersection[1].entry_allowed && intersection[2].entry_allowed &&
                         intersection[3].entry_allowed);
            // count backwards from the slightest turn
            assignTrivialTurns(via_edge, intersection, 1, up_to);
        }
        else if (((first_direction == second_direction &&
                   angularDeviation(intersection[2].angle, intersection[3].angle) >= GROUP_ANGLE) ||
                  (second_direction == third_direction &&
                   angularDeviation(intersection[1].angle, intersection[2].angle) >= GROUP_ANGLE)))
        {
            BOOST_ASSERT(intersection[1].entry_allowed && intersection[2].entry_allowed &&
                         intersection[3].entry_allowed);

            if (angularDeviation(intersection[2].angle, intersection[3].angle) >= GROUP_ANGLE)
            {
                handleDistinctConflict(via_edge, intersection[2], intersection[1]);
                intersection[3].instruction = {findBasicTurnType(via_edge, intersection[3]),
                                               third_direction};
            }
            else
            {
                intersection[1].instruction = {findBasicTurnType(via_edge, intersection[1]),
                                               first_direction};
                handleDistinctConflict(via_edge, intersection[3], intersection[2]);
            }
        }
        else
        {
            assignTrivialTurns(via_edge, intersection, 1, up_to);
        }
    }
    else
    {
        assignTrivialTurns(via_edge, intersection, 1, up_to);
    }
    return intersection;
}

// finds a fork candidate by just looking at the geometry and angle of an intersection
std::optional<TurnHandler::Fork>
TurnHandler::findForkCandidatesByGeometry(Intersection &intersection) const
{
    if (intersection.size() >= 3)
    {
        const auto straightmost = intersection.findClosestTurn(STRAIGHT_ANGLE);
        const auto straightmost_index = std::distance(intersection.begin(), straightmost);
        const auto straightmost_angle_dev = angularDeviation(straightmost->angle, STRAIGHT_ANGLE);

        // Forks can only happen when two or more roads have a pretty narrow angle between each
        // other and are close to going straight
        //
        //
        //     left   right          left   right
        //        \   /                 \ | /
        //         \ /                   \|/
        //          |                     |
        //          |                     |
        //          |                     |
        //
        //   possibly a fork         possibly a fork
        //
        //
        //           left             left
        //            /                 \ 
        //           /____ right         \ ______ right
        //          |                     |
        //          |                     |
        //          |                     |
        //
        //   not a fork cause      not a fork cause
        //    it's not going       angle is too wide
        //     straigthish
        //
        //
        // left and right will be indices of the leftmost and rightmost connected roads that are
        // fork candidates

        if (straightmost_angle_dev <= NARROW_TURN_ANGLE)
        {
            // find the rightmost road that might be part of a fork
            const auto right = findOutermostForkCandidate(
                intersection.rend() - straightmost_index - 1, intersection.rend());
            const std::size_t right_index = intersection.rend() - right - 1;
            const auto forward_right = intersection.begin() + right_index;
            // find the leftmost road that might be part of a fork
            const auto left = findOutermostForkCandidate(straightmost, intersection.end());

            // if the leftmost and rightmost roads with the conditions above are the same
            // or if there are more than three fork candidates
            // they cannot be fork candidates
            if (forward_right < left && left - forward_right < 3)
            {
                return Fork(intersection.begin(), forward_right, left + 1);
            }
        }
    }
    return std::nullopt;
}

// check if the fork candidates (all roads between left and right) and the
// incoming edge are compatible by class
bool TurnHandler::isCompatibleByRoadClass(const Intersection &intersection, const Fork fork) const
{
    const auto via_class =
        node_based_graph.GetEdgeData(intersection[0].eid).flags.road_classification;

    // if any of the considered roads is a link road, it cannot be a fork
    // except if rightmost fork candidate is also a link road
    const auto is_right_link_class =
        node_based_graph.GetEdgeData(fork.getRight().eid).flags.road_classification.IsLinkClass();
    if (!std::all_of(fork.begin + 1,
                     fork.end,
                     [&](ConnectedRoad &road)
                     {
                         return is_right_link_class == node_based_graph.GetEdgeData(road.eid)
                                                           .flags.road_classification.IsLinkClass();
                     }))
    {
        return false;
    }

    return std::all_of(
        fork.begin,
        fork.end,
        [&](ConnectedRoad &base)
        {
            const auto base_class =
                node_based_graph.GetEdgeData(base.eid).flags.road_classification;
            // check that there is no turn obvious == check that all turns are non-onvious
            return std::all_of(
                fork.begin,
                fork.end,
                [&](ConnectedRoad &compare)
                {
                    const auto compare_class =
                        node_based_graph.GetEdgeData(compare.eid).flags.road_classification;
                    return compare.eid == base.eid ||
                           !(obviousByRoadClass(via_class, base_class, compare_class));
                });
        });
}

// Checks whether a three-way-intersection coming from `via_edge` is a fork
// with `intersection` as described as in #IntersectionExplanation@intersection_handler.hpp
std::optional<TurnHandler::Fork> TurnHandler::findFork(const EdgeID via_edge,
                                                       Intersection &intersection) const
{
    auto fork = findForkCandidatesByGeometry(intersection);
    if (fork)
    {
        // makes sure that the fork is isolated from other neighbouring streets on the left and
        // right side
        const auto next = fork->end == intersection.end() ? intersection.begin() : (fork->end);
        const bool separated_at_left_side =
            angularDeviation(fork->getLeft().angle, next->angle) >= GROUP_ANGLE;
        BOOST_ASSERT((fork->begin - 1) >= intersection.begin());
        const bool separated_at_right_side =
            angularDeviation(fork->getRight().angle, (fork->begin - 1)->angle) >= GROUP_ANGLE;

        // check whether there is an obvious turn to take; forks are never obvious - if there is an
        // obvious turn, it's not a fork
        const bool has_obvious = hasObvious(via_edge, *fork);

        // A fork can only happen between edges of similar types where none of the ones is obvious
        const bool has_compatible_classes = isCompatibleByRoadClass(intersection, *fork);

        // check if all entries in the fork range allow entry
        const bool only_valid_entries = intersection.hasAllValidEntries(fork->begin, fork->end);

        const auto has_compatible_modes = std::all_of(
            fork->begin,
            fork->end,
            [&](const auto &road)
            {
                return node_data_container
                           .GetAnnotation(node_based_graph.GetEdgeData(road.eid).annotation_data)
                           .travel_mode ==
                       node_data_container
                           .GetAnnotation(node_based_graph.GetEdgeData(via_edge).annotation_data)
                           .travel_mode;
            });

        if (separated_at_left_side && separated_at_right_side && !has_obvious &&
            has_compatible_classes && only_valid_entries && has_compatible_modes)
        {
            return fork;
        }
    }

    return std::nullopt;
}

void TurnHandler::handleDistinctConflict(const EdgeID via_edge,
                                         ConnectedRoad &left,
                                         ConnectedRoad &right) const
{
    // single turn of both is valid (don't change the valid one)
    // or multiple identical angles -> bad OSM intersection
    if ((!left.entry_allowed || !right.entry_allowed) || (left.angle == right.angle))
    {
        if (left.entry_allowed)
            left.instruction = {findBasicTurnType(via_edge, left), getTurnDirection(left.angle)};
        if (right.entry_allowed)
            right.instruction = {findBasicTurnType(via_edge, right), getTurnDirection(right.angle)};
        return;
    }

    if (getTurnDirection(left.angle) == DirectionModifier::Straight ||
        getTurnDirection(left.angle) == DirectionModifier::SlightLeft ||
        getTurnDirection(right.angle) == DirectionModifier::SlightRight)
    {
        const auto left_classification =
            node_based_graph.GetEdgeData(left.eid).flags.road_classification;
        const auto right_classification =
            node_based_graph.GetEdgeData(right.eid).flags.road_classification;

        if (left_classification.GetPriority() > right_classification.GetPriority())
        {
            // FIXME this should possibly know about the actual roads?
            // here we don't know about the intersection size. To be on the save side,
            // we declare it
            // as complex (at least size 4)
            right.instruction = getInstructionForObvious(4, via_edge, false, right);
            left.instruction = {findBasicTurnType(via_edge, left), DirectionModifier::SlightLeft};
        }
        else
        {
            // FIXME this should possibly know about the actual roads?
            // here we don't know about the intersection size. To be on the save side,
            // we declare it
            // as complex (at least size 4)
            left.instruction = getInstructionForObvious(4, via_edge, false, left);
            right.instruction = {findBasicTurnType(via_edge, right),
                                 DirectionModifier::SlightRight};
        }
        return;
    }
    const auto left_type = findBasicTurnType(via_edge, left);
    const auto right_type = findBasicTurnType(via_edge, right);
    // Two Right Turns
    if (angularDeviation(left.angle, 90) < MAXIMAL_ALLOWED_NO_TURN_DEVIATION)
    {
        // Keep left perfect, shift right
        left.instruction = {left_type, DirectionModifier::Right};
        right.instruction = {right_type, DirectionModifier::SharpRight};
        return;
    }
    if (angularDeviation(right.angle, 90) < MAXIMAL_ALLOWED_NO_TURN_DEVIATION)
    {
        // Keep Right perfect, shift left
        left.instruction = {left_type, DirectionModifier::SlightRight};
        right.instruction = {right_type, DirectionModifier::Right};
        return;
    }
    // Two Left Turns
    if (angularDeviation(left.angle, 270) < MAXIMAL_ALLOWED_NO_TURN_DEVIATION)
    {
        // Keep left perfect, shift right
        left.instruction = {left_type, DirectionModifier::Left};
        right.instruction = {right_type, DirectionModifier::SlightLeft};
        return;
    }
    if (angularDeviation(right.angle, 270) < MAXIMAL_ALLOWED_NO_TURN_DEVIATION)
    {
        // Keep Right perfect, shift left
        left.instruction = {left_type, DirectionModifier::SharpLeft};
        right.instruction = {right_type, DirectionModifier::Left};
        return;
    }
    // Shift the lesser penalty
    if (getTurnDirection(left.angle) == DirectionModifier::SharpLeft)
    {
        left.instruction = {left_type, DirectionModifier::SharpLeft};
        right.instruction = {right_type, DirectionModifier::Left};
        return;
    }
    if (getTurnDirection(right.angle) == DirectionModifier::SharpRight)
    {
        left.instruction = {left_type, DirectionModifier::Right};
        right.instruction = {right_type, DirectionModifier::SharpRight};
        return;
    }

    // turn to the right
    if (getTurnDirection(left.angle) <= 180)
    {
        if (angularDeviation(left.angle, 85) >= angularDeviation(right.angle, 85))
        {
            left.instruction = {left_type, DirectionModifier::SlightRight};
            right.instruction = {right_type, DirectionModifier::Right};
        }
        else
        {
            left.instruction = {left_type, DirectionModifier::Right};
            right.instruction = {right_type, DirectionModifier::SharpRight};
        }
    }
    else
    {
        if (angularDeviation(left.angle, 265) >= angularDeviation(right.angle, 265))
        {
            left.instruction = {left_type, DirectionModifier::SharpLeft};
            right.instruction = {right_type, DirectionModifier::Left};
        }
        else
        {
            left.instruction = {left_type, DirectionModifier::Left};
            right.instruction = {right_type, DirectionModifier::SlightLeft};
        }
    }
}

} // namespace osrm::guidance
