#ifndef OSRM_UTIL_GUIDANCE_TOOLKIT_HPP_
#define OSRM_UTIL_GUIDANCE_TOOLKIT_HPP_

/* A set of tools required for guidance in both pre and post-processing */

#include "extractor/guidance/constants.hpp"
#include "extractor/guidance/turn_instruction.hpp"
#include "extractor/suffix_table.hpp"
#include "engine/guidance/route_step.hpp"
#include "engine/phantom_node.hpp"
#include "util/attributes.hpp"
#include "util/guidance/bearing_class.hpp"
#include "util/guidance/entry_class.hpp"
#include "util/name_table.hpp"
#include "util/simple_logger.hpp"

#include <algorithm>
#include <string>
#include <vector>

#include <boost/algorithm/string.hpp>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/functional/hash.hpp>

namespace osrm
{
namespace util
{
namespace guidance
{

inline double angularDeviation(const double angle, const double from)
{
    const double deviation = std::abs(angle - from);
    return std::min(360 - deviation, deviation);
}

inline bool hasRampType(const extractor::guidance::TurnInstruction instruction)
{
    return instruction.type == extractor::guidance::TurnType::OffRamp ||
           instruction.type == extractor::guidance::TurnType::OnRamp;
}

inline extractor::guidance::DirectionModifier::Enum getTurnDirection(const double angle)
{
    // An angle of zero is a u-turn
    // 180 goes perfectly straight
    // 0-180 are right turns
    // 180-360 are left turns
    if (angle > 0 && angle < 60)
        return extractor::guidance::DirectionModifier::SharpRight;
    if (angle >= 60 && angle < 140)
        return extractor::guidance::DirectionModifier::Right;
    if (angle >= 140 && angle < 160)
        return extractor::guidance::DirectionModifier::SlightRight;
    if (angle >= 160 && angle <= 200)
        return extractor::guidance::DirectionModifier::Straight;
    if (angle > 200 && angle <= 220)
        return extractor::guidance::DirectionModifier::SlightLeft;
    if (angle > 220 && angle <= 300)
        return extractor::guidance::DirectionModifier::Left;
    if (angle > 300 && angle < 360)
        return extractor::guidance::DirectionModifier::SharpLeft;
    return extractor::guidance::DirectionModifier::UTurn;
}

// swaps left <-> right modifier types
OSRM_ATTR_WARN_UNUSED
inline extractor::guidance::DirectionModifier::Enum
mirrorDirectionModifier(const extractor::guidance::DirectionModifier::Enum modifier)
{
    const constexpr extractor::guidance::DirectionModifier::Enum results[] = {
        extractor::guidance::DirectionModifier::UTurn,
        extractor::guidance::DirectionModifier::SharpLeft,
        extractor::guidance::DirectionModifier::Left,
        extractor::guidance::DirectionModifier::SlightLeft,
        extractor::guidance::DirectionModifier::Straight,
        extractor::guidance::DirectionModifier::SlightRight,
        extractor::guidance::DirectionModifier::Right,
        extractor::guidance::DirectionModifier::SharpRight};
    return results[modifier];
}

inline bool hasLeftModifier(const extractor::guidance::TurnInstruction instruction)
{
    return instruction.direction_modifier == extractor::guidance::DirectionModifier::SharpLeft ||
           instruction.direction_modifier == extractor::guidance::DirectionModifier::Left ||
           instruction.direction_modifier == extractor::guidance::DirectionModifier::SlightLeft;
}

inline bool hasRightModifier(const extractor::guidance::TurnInstruction instruction)
{
    return instruction.direction_modifier == extractor::guidance::DirectionModifier::SharpRight ||
           instruction.direction_modifier == extractor::guidance::DirectionModifier::Right ||
           instruction.direction_modifier == extractor::guidance::DirectionModifier::SlightRight;
}

inline bool isLeftTurn(const extractor::guidance::TurnInstruction instruction)
{
    switch (instruction.type)
    {
    case extractor::guidance::TurnType::Merge:
        return hasRightModifier(instruction);
    default:
        return hasLeftModifier(instruction);
    }
}

inline bool isRightTurn(const extractor::guidance::TurnInstruction instruction)
{
    switch (instruction.type)
    {
    case extractor::guidance::TurnType::Merge:
        return hasLeftModifier(instruction);
    default:
        return hasRightModifier(instruction);
    }
}

inline bool entersRoundabout(const extractor::guidance::TurnInstruction instruction)
{
    return (instruction.type == extractor::guidance::TurnType::EnterRoundabout ||
            instruction.type == extractor::guidance::TurnType::EnterRotary ||
            instruction.type == extractor::guidance::TurnType::EnterRoundaboutIntersection ||
            instruction.type == extractor::guidance::TurnType::EnterRoundaboutAtExit ||
            instruction.type == extractor::guidance::TurnType::EnterRotaryAtExit ||
            instruction.type == extractor::guidance::TurnType::EnterRoundaboutIntersectionAtExit ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRoundabout ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRotary ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRoundaboutIntersection);
}

inline bool leavesRoundabout(const extractor::guidance::TurnInstruction instruction)
{
    return (instruction.type == extractor::guidance::TurnType::ExitRoundabout ||
            instruction.type == extractor::guidance::TurnType::ExitRotary ||
            instruction.type == extractor::guidance::TurnType::ExitRoundaboutIntersection ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRoundabout ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRotary ||
            instruction.type == extractor::guidance::TurnType::EnterAndExitRoundaboutIntersection);
}

inline bool staysOnRoundabout(const extractor::guidance::TurnInstruction instruction)
{
    return instruction.type == extractor::guidance::TurnType::StayOnRoundabout;
}

// Name Change Logic
// Used both during Extraction as well as during Post-Processing

inline std::pair<std::string, std::string> getPrefixAndSuffix(const std::string &data)
{
    const auto suffix_pos = data.find_last_of(' ');
    if (suffix_pos == std::string::npos)
        return {};

    const auto prefix_pos = data.find_first_of(' ');
    auto result = std::make_pair(data.substr(0, prefix_pos), data.substr(suffix_pos + 1));
    boost::to_lower(result.first);
    boost::to_lower(result.second);
    return result;
}

// Note: there is an overload without suffix checking below.
// (that's the reason we template the suffix table here)
template <typename SuffixTable>
inline bool requiresNameAnnounced(const std::string &from_name,
                                  const std::string &from_ref,
                                  const std::string &from_pronunciation,
                                  const std::string &to_name,
                                  const std::string &to_ref,
                                  const std::string &to_pronunciation,
                                  const SuffixTable &suffix_table)
{
    // first is empty and the second is not
    if ((from_name.empty() && from_ref.empty()) && !(to_name.empty() && to_ref.empty()))
        return true;

    // FIXME, handle in profile to begin with?
    // Input for this function should be a struct separating streetname, suffix (e.g. road,
    // boulevard, North, West ...), and a list of references

    // check similarity of names
    const auto names_are_empty = from_name.empty() && to_name.empty();
    const auto name_is_contained =
        boost::starts_with(from_name, to_name) || boost::starts_with(to_name, from_name);

    const auto checkForPrefixOrSuffixChange = [](
        const std::string &first, const std::string &second, const SuffixTable &suffix_table) {

        const auto first_prefix_and_suffixes = getPrefixAndSuffix(first);
        const auto second_prefix_and_suffixes = getPrefixAndSuffix(second);

        // reverse strings, get suffices and reverse them to get prefixes
        const auto checkTable = [&](const std::string &str) {
            return str.empty() || suffix_table.isSuffix(str);
        };

        const auto getOffset = [](const std::string &str) -> std::size_t {
            if (str.empty())
                return 0;
            else
                return str.length() + 1;
        };

        const bool is_prefix_change = [&]() -> bool {
            if (!checkTable(first_prefix_and_suffixes.first))
                return false;
            if (!checkTable(second_prefix_and_suffixes.first))
                return false;
            return !first.compare(getOffset(first_prefix_and_suffixes.first),
                                  std::string::npos,
                                  second,
                                  getOffset(second_prefix_and_suffixes.first),
                                  std::string::npos);
        }();

        const bool is_suffix_change = [&]() -> bool {
            if (!checkTable(first_prefix_and_suffixes.second))
                return false;
            if (!checkTable(second_prefix_and_suffixes.second))
                return false;
            return !first.compare(0,
                                  first.length() - getOffset(first_prefix_and_suffixes.second),
                                  second,
                                  0,
                                  second.length() - getOffset(second_prefix_and_suffixes.second));
        }();

        return is_prefix_change || is_suffix_change;
    };

    const auto is_suffix_change = checkForPrefixOrSuffixChange(from_name, to_name, suffix_table);
    const auto names_are_equal = from_name == to_name || name_is_contained || is_suffix_change;
    const auto name_is_removed = !from_name.empty() && to_name.empty();
    // references are contained in one another
    const auto refs_are_empty = from_ref.empty() && to_ref.empty();
    const auto ref_is_contained =
        from_ref.empty() || to_ref.empty() ||
        (from_ref.find(to_ref) != std::string::npos || to_ref.find(from_ref) != std::string::npos);
    const auto ref_is_removed = !from_ref.empty() && to_ref.empty();

    const auto obvious_change =
        (names_are_empty && refs_are_empty) || (names_are_equal && ref_is_contained) ||
        (names_are_equal && refs_are_empty) || (ref_is_contained && name_is_removed) ||
        (names_are_equal && ref_is_removed) || is_suffix_change;

    const auto needs_announce =
        // " (Ref)" -> "Name "
        (from_name.empty() && !from_ref.empty() && !to_name.empty() && to_ref.empty());

    const auto pronunciation_changes = from_pronunciation != to_pronunciation;

    return !obvious_change || needs_announce || pronunciation_changes;
}

// Overload without suffix checking
inline bool requiresNameAnnounced(const std::string &from_name,
                                  const std::string &from_ref,
                                  const std::string &from_pronunciation,
                                  const std::string &to_name,
                                  const std::string &to_ref,
                                  const std::string &to_pronunciation)
{
    // Dummy since we need to provide a SuffixTable but do not have the data for it.
    // (Guidance Post-Processing does not keep the suffix table around at the moment)
    struct NopSuffixTable final
    {
        NopSuffixTable() {}
        bool isSuffix(const std::string &) const { return false; }
    } static const table;

    return requiresNameAnnounced(
        from_name, from_ref, from_pronunciation, to_name, to_ref, to_pronunciation, table);
}

inline bool requiresNameAnnounced(const NameID from_name_id,
                                  const NameID to_name_id,
                                  const util::NameTable &name_table,
                                  const extractor::SuffixTable &suffix_table)
{
    return requiresNameAnnounced(name_table.GetNameForID(from_name_id),
                                 name_table.GetRefForID(from_name_id),
                                 name_table.GetPronunciationForID(from_name_id),
                                 name_table.GetNameForID(to_name_id),
                                 name_table.GetRefForID(to_name_id),
                                 name_table.GetPronunciationForID(to_name_id),
                                 suffix_table);
}

inline bool requiresNameAnnounced(const NameID from_name_id,
                                  const NameID to_name_id,
                                  const util::NameTable &name_table)
{
    return requiresNameAnnounced(name_table.GetNameForID(from_name_id),
                                 name_table.GetRefForID(from_name_id),
                                 name_table.GetPronunciationForID(from_name_id),
                                 name_table.GetNameForID(to_name_id),
                                 name_table.GetRefForID(to_name_id),
                                 name_table.GetPronunciationForID(to_name_id));
}

} // namespace guidance
} // namespace util
} // namespace osrm

#endif /* OSRM_UTIL_GUIDANCE_TOOLKIT_HPP_ */