#ifndef ENGINE_API_ROUTE_HPP
#define ENGINE_API_ROUTE_HPP

#include "extractor/maneuver_override.hpp"
#include "engine/api/base_api.hpp"
#include "engine/api/base_result.hpp"
#include "engine/api/json_factory.hpp"
#include "engine/api/route_parameters.hpp"

#include "engine/datafacade/datafacade_base.hpp"

#include "engine/guidance/assemble_geometry.hpp"
#include "engine/guidance/assemble_leg.hpp"
#include "engine/guidance/assemble_overview.hpp"
#include "engine/guidance/assemble_route.hpp"
#include "engine/guidance/assemble_steps.hpp"
#include "engine/guidance/collapse_turns.hpp"
#include "engine/guidance/lane_processing.hpp"
#include "engine/guidance/post_processing.hpp"
#include "engine/guidance/verbosity_reduction.hpp"

#include "engine/internal_route_result.hpp"

#include "guidance/turn_instruction.hpp"

#include "util/coordinate.hpp"
#include "util/integer_range.hpp"
#include "util/json_util.hpp"

#include <bitset>
#include <iterator>
#include <map>
#include <vector>

namespace osrm::engine::api
{

class RouteAPI : public BaseAPI
{
  public:
    RouteAPI(const datafacade::BaseDataFacade &facade_, const RouteParameters &parameters_)
        : BaseAPI(facade_, parameters_), parameters(parameters_)
    {
    }

    void
    MakeResponse(const InternalManyRoutesResult &raw_routes,
                 const std::vector<PhantomNodeCandidates>
                     &waypoint_candidates, // all used coordinates, ignoring waypoints= parameter
                 osrm::engine::api::ResultT &response) const
    {
        BOOST_ASSERT(!raw_routes.routes.empty());

        if (std::holds_alternative<flatbuffers::FlatBufferBuilder>(response))
        {
            auto &fb_result = std::get<flatbuffers::FlatBufferBuilder>(response);
            MakeResponse(raw_routes, waypoint_candidates, fb_result);
        }
        else
        {
            auto &json_result = std::get<util::json::Object>(response);
            MakeResponse(raw_routes, waypoint_candidates, json_result);
        }
    }

    void
    MakeResponse(const InternalManyRoutesResult &raw_routes,
                 const std::vector<PhantomNodeCandidates>
                     &waypoint_candidates, // all used coordinates, ignoring waypoints= parameter
                 flatbuffers::FlatBufferBuilder &fb_result) const
    {

        auto data_timestamp = facade.GetTimestamp();
        flatbuffers::Offset<flatbuffers::String> data_version_string;
        if (!data_timestamp.empty())
        {
            data_version_string = fb_result.CreateString(data_timestamp);
        }

        auto response =
            MakeFBResponse(raw_routes,
                           fb_result,
                           [this, &waypoint_candidates, &fb_result]()
                           { return BaseAPI::MakeWaypoints(&fb_result, waypoint_candidates); });

        if (!data_timestamp.empty())
        {
            response->add_data_version(data_version_string);
        }
        fb_result.Finish(response->Finish());
    }

    void
    MakeResponse(const InternalManyRoutesResult &raw_routes,
                 const std::vector<PhantomNodeCandidates>
                     &waypoint_candidates, // all used coordinates, ignoring waypoints= parameter
                 util::json::Object &response) const
    {
        util::json::Array jsRoutes;

        for (const auto &route : raw_routes.routes)
        {
            if (!route.is_valid())
                continue;

            jsRoutes.values.push_back(MakeRoute(route.leg_endpoints,
                                                route.unpacked_path_segments,
                                                route.source_traversed_in_reverse,
                                                route.target_traversed_in_reverse));
        }

        if (!parameters.skip_waypoints)
        {
            response.values.emplace("waypoints", BaseAPI::MakeWaypoints(waypoint_candidates));
        }
        response.values.emplace("routes", std::move(jsRoutes));
        response.values.emplace("code", "Ok");
        auto data_timestamp = facade.GetTimestamp();
        if (!data_timestamp.empty())
        {
            response.values.emplace("data_version", data_timestamp);
        }
    }

  protected:
    template <typename GetWptsFn>
    std::unique_ptr<fbresult::FBResultBuilder>
    MakeFBResponse(const InternalManyRoutesResult &raw_routes,
                   flatbuffers::FlatBufferBuilder &fb_result,
                   GetWptsFn getWaypoints) const
    {

        std::vector<flatbuffers::Offset<fbresult::RouteObject>> routes;
        for (const auto &raw_route : raw_routes.routes)
        {
            if (!raw_route.is_valid())
                continue;

            routes.push_back(MakeRoute(fb_result,
                                       raw_route.leg_endpoints,
                                       raw_route.unpacked_path_segments,
                                       raw_route.source_traversed_in_reverse,
                                       raw_route.target_traversed_in_reverse));
        }

        auto routes_vector = fb_result.CreateVector(routes);
        flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<fbresult::Waypoint>>>
            waypoints_vector;
        if (!parameters.skip_waypoints)
        {
            waypoints_vector = getWaypoints();
        }

        auto response = std::make_unique<fbresult::FBResultBuilder>(fb_result);
        response->add_routes(routes_vector);
        response->add_waypoints(waypoints_vector);

        return response;
    }

    template <typename ForwardIter>
    std::variant<flatbuffers::Offset<flatbuffers::String>,
                 flatbuffers::Offset<flatbuffers::Vector<const fbresult::Position *>>>
    MakeGeometry(flatbuffers::FlatBufferBuilder &builder, ForwardIter begin, ForwardIter end) const
    {
        if (parameters.geometries == RouteParameters::GeometriesType::Polyline)
        {
            return builder.CreateString(encodePolyline<100000>(begin, end));
        }
        else if (parameters.geometries == RouteParameters::GeometriesType::Polyline6)
        {
            return builder.CreateString(encodePolyline<1000000>(begin, end));
        }
        std::vector<fbresult::Position> coordinates;
        coordinates.resize(std::distance(begin, end));
        std::transform(begin,
                       end,
                       coordinates.begin(),
                       [](const Coordinate &c)
                       {
                           return fbresult::Position{
                               static_cast<float>(util::toFloating(c.lon).__value),
                               static_cast<float>(util::toFloating(c.lat).__value)};
                       });
        return builder.CreateVectorOfStructs(coordinates);
    }

    std::optional<util::json::Value>
    MakeGeometry(std::optional<std::vector<Coordinate>> &&geometry) const
    {
        std::optional<util::json::Value> json_geometry;
        if (geometry)
        {
            return MakeGeometry(*geometry);
        }
        return std::nullopt;
    }

    util::json::Value MakeGeometry(const std::vector<Coordinate> &geometry) const
    {
        util::json::Value json_geometry;
        auto begin = geometry.cbegin();
        auto end = geometry.cend();
        if (parameters.geometries == RouteParameters::GeometriesType::Polyline)
        {
            json_geometry = json::makePolyline<100000>(begin, end);
        }
        else if (parameters.geometries == RouteParameters::GeometriesType::Polyline6)
        {
            json_geometry = json::makePolyline<1000000>(begin, end);
        }
        else
        {
            BOOST_ASSERT(parameters.geometries == RouteParameters::GeometriesType::GeoJSON);
            json_geometry = json::makeGeoJSONGeometry(begin, end);
        }
        return json_geometry;
    }

    template <typename ValueType, typename GetFn>
    flatbuffers::Offset<flatbuffers::Vector<ValueType>> GetAnnotations(
        flatbuffers::FlatBufferBuilder &fb_result, guidance::LegGeometry &leg, GetFn Get) const
    {
        std::vector<ValueType> annotations_store;
        annotations_store.reserve(leg.annotations.size());

        for (const auto &step : leg.annotations)
        {
            annotations_store.push_back(Get(step));
        }

        return fb_result.CreateVector(annotations_store);
    }

    template <typename GetFn>
    util::json::Array GetAnnotations(const guidance::LegGeometry &leg, GetFn Get) const
    {
        util::json::Array annotations_store;
        annotations_store.values.reserve(leg.annotations.size());

        for (const auto &step : leg.annotations)
        {
            annotations_store.values.push_back(Get(step));
        }

        return annotations_store;
    }

    fbresult::ManeuverType WaypointTypeToFB(guidance::WaypointType type) const
    {
        switch (type)
        {
        case guidance::WaypointType::Arrive:
            return fbresult::ManeuverType_Arrive;
        case guidance::WaypointType::Depart:
            return fbresult::ManeuverType_Depart;
        default:
            return fbresult::ManeuverType_Notification;
        }
    }

    fbresult::ManeuverType TurnTypeToFB(osrm::guidance::TurnType::Enum turn) const
    {
        static std::map<osrm::guidance::TurnType::Enum, fbresult::ManeuverType> mappings = {
            {osrm::guidance::TurnType::Invalid, fbresult::ManeuverType_Notification},
            {osrm::guidance::TurnType::NewName, fbresult::ManeuverType_NewName},
            {osrm::guidance::TurnType::Continue, fbresult::ManeuverType_Continue},
            {osrm::guidance::TurnType::Turn, fbresult::ManeuverType_Turn},
            {osrm::guidance::TurnType::Merge, fbresult::ManeuverType_Merge},
            {osrm::guidance::TurnType::OnRamp, fbresult::ManeuverType_OnRamp},
            {osrm::guidance::TurnType::OffRamp, fbresult::ManeuverType_OffRamp},
            {osrm::guidance::TurnType::Fork, fbresult::ManeuverType_Fork},
            {osrm::guidance::TurnType::EndOfRoad, fbresult::ManeuverType_EndOfRoad},
            {osrm::guidance::TurnType::Notification, fbresult::ManeuverType_Notification},
            {osrm::guidance::TurnType::EnterRoundabout, fbresult::ManeuverType_Roundabout},
            {osrm::guidance::TurnType::EnterAndExitRoundabout,
             fbresult::ManeuverType_ExitRoundabout},
            {osrm::guidance::TurnType::EnterRotary, fbresult::ManeuverType_Rotary},
            {osrm::guidance::TurnType::EnterAndExitRotary, fbresult::ManeuverType_ExitRotary},
            {osrm::guidance::TurnType::EnterRoundaboutIntersection,
             fbresult::ManeuverType_Roundabout},
            {osrm::guidance::TurnType::EnterAndExitRoundaboutIntersection,
             fbresult::ManeuverType_ExitRoundabout},
            {osrm::guidance::TurnType::NoTurn, fbresult::ManeuverType_Notification},
            {osrm::guidance::TurnType::Suppressed, fbresult::ManeuverType_Notification},
            {osrm::guidance::TurnType::EnterRoundaboutAtExit, fbresult::ManeuverType_Roundabout},
            {osrm::guidance::TurnType::ExitRoundabout, fbresult::ManeuverType_ExitRoundabout},
            {osrm::guidance::TurnType::EnterRotaryAtExit, fbresult::ManeuverType_Rotary},
            {osrm::guidance::TurnType::ExitRotary, fbresult::ManeuverType_ExitRotary},
            {osrm::guidance::TurnType::EnterRoundaboutIntersectionAtExit,
             fbresult::ManeuverType_Roundabout},
            {osrm::guidance::TurnType::ExitRoundaboutIntersection,
             fbresult::ManeuverType_ExitRoundabout},
            {osrm::guidance::TurnType::StayOnRoundabout, fbresult::ManeuverType_RoundaboutTurn},
            {osrm::guidance::TurnType::Sliproad, fbresult::ManeuverType_Notification},
            {osrm::guidance::TurnType::MaxTurnType, fbresult::ManeuverType_Notification}};
        return mappings[turn];
    }

    fbresult::Turn TurnModifierToFB(osrm::guidance::DirectionModifier::Enum modifier) const
    {
        static std::map<osrm::guidance::DirectionModifier::Enum, fbresult::Turn> mappings = {
            {osrm::guidance::DirectionModifier::UTurn, fbresult::Turn_UTurn},
            {osrm::guidance::DirectionModifier::SharpRight, fbresult::Turn_SharpRight},
            {osrm::guidance::DirectionModifier::Right, fbresult::Turn_Right},
            {osrm::guidance::DirectionModifier::SlightRight, fbresult::Turn_SlightRight},
            {osrm::guidance::DirectionModifier::Straight, fbresult::Turn_Straight},
            {osrm::guidance::DirectionModifier::SlightLeft, fbresult::Turn_SlightLeft},
            {osrm::guidance::DirectionModifier::Left, fbresult::Turn_Left},
            {osrm::guidance::DirectionModifier::SharpLeft, fbresult::Turn_SharpLeft},
        };
        return mappings[modifier];
    }

    std::vector<int8_t> TurnLaneTypeToFB(const extractor::TurnLaneType::Mask lane_type) const
    {
        const static fbresult::Turn mapping[] = {fbresult::Turn_None,
                                                 fbresult::Turn_Straight,
                                                 fbresult::Turn_SharpLeft,
                                                 fbresult::Turn_Left,
                                                 fbresult::Turn_SlightLeft,
                                                 fbresult::Turn_SlightRight,
                                                 fbresult::Turn_Right,
                                                 fbresult::Turn_SharpRight,
                                                 fbresult::Turn_UTurn,
                                                 fbresult::Turn_SlightLeft,
                                                 fbresult::Turn_SlightRight};
        std::vector<int8_t> result;
        std::bitset<8 * sizeof(extractor::TurnLaneType::Mask)> mask(lane_type);
        for (auto index : util::irange<std::size_t>(0, extractor::TurnLaneType::NUM_TYPES))
        {
            if (mask[index])
            {
                result.push_back(mapping[index]);
            }
        }
        return result;
    }

    flatbuffers::Offset<fbresult::RouteObject>
    MakeRoute(flatbuffers::FlatBufferBuilder &fb_result,
              const std::vector<PhantomEndpoints> &leg_endpoints,
              const std::vector<std::vector<PathData>> &unpacked_path_segments,
              const std::vector<bool> &source_traversed_in_reverse,
              const std::vector<bool> &target_traversed_in_reverse) const
    {
        auto legs_info = MakeLegs(leg_endpoints,
                                  unpacked_path_segments,
                                  source_traversed_in_reverse,
                                  target_traversed_in_reverse);
        std::vector<guidance::RouteLeg> &legs = legs_info.first;
        std::vector<guidance::LegGeometry> &leg_geometries = legs_info.second;
        auto route = guidance::assembleRoute(legs);

        // Fill legs
        std::vector<flatbuffers::Offset<fbresult::Leg>> routeLegs;
        routeLegs.reserve(legs.size());
        for (const auto idx : util::irange<std::size_t>(0UL, legs.size()))
        {
            auto leg = legs[idx];
            auto &leg_geometry = leg_geometries[idx];

            // Fill steps
            std::vector<flatbuffers::Offset<fbresult::Step>> legSteps;
            if (!leg.steps.empty())
            {
                legSteps.resize(leg.steps.size());
                std::transform(leg.steps.begin(),
                               leg.steps.end(),
                               legSteps.begin(),
                               [this, &fb_result, &leg_geometry](auto &step)
                               { return this->MakeFBStep(fb_result, leg_geometry, step); });
            }
            auto steps_vector = fb_result.CreateVector(legSteps);

            // Fill annotations
            // To maintain support for uses of the old default constructors, we check
            // if annotations property was set manually after default construction
            auto requested_annotations = parameters.annotations_type;
            if (parameters.annotations &&
                (parameters.annotations_type == RouteParameters::AnnotationsType::None))
            {
                requested_annotations = RouteParameters::AnnotationsType::All;
            }

            flatbuffers::Offset<fbresult::Annotation> annotation_buffer;
            if (requested_annotations != RouteParameters::AnnotationsType::None)
            {
                annotation_buffer =
                    MakeFBAnnotations(fb_result, leg_geometry, requested_annotations);
            }

            flatbuffers::Offset<flatbuffers::String> summary_string;
            if (!leg.summary.empty())
            {
                summary_string = fb_result.CreateString(leg.summary);
            }

            // Fill leg geometry if overview=by_legs
            std::variant<flatbuffers::Offset<flatbuffers::String>,
                         flatbuffers::Offset<flatbuffers::Vector<const fbresult::Position *>>>
                leg_geometry_variant;
            bool has_leg_geometry = false;

            if (parameters.overview == RouteParameters::OverviewType::ByLegs)
            {
                leg_geometry_variant = MakeGeometry(
                    fb_result, leg_geometry.locations.begin(), leg_geometry.locations.end());
                has_leg_geometry = true;
            }

            fbresult::LegBuilder legBuilder(fb_result);
            legBuilder.add_distance(leg.distance);
            legBuilder.add_duration(leg.duration);
            legBuilder.add_weight(leg.weight);
            if (!leg.summary.empty())
            {
                legBuilder.add_summary(summary_string);
            }
            legBuilder.add_steps(steps_vector);

            if (requested_annotations != RouteParameters::AnnotationsType::None)
            {
                legBuilder.add_annotations(annotation_buffer);
            }

            if (has_leg_geometry)
            {
                std::visit(GeometryVisitor<fbresult::LegBuilder>(legBuilder), leg_geometry_variant);
            }

            routeLegs.emplace_back(legBuilder.Finish());
        }
        auto legs_vector = fb_result.CreateVector(routeLegs);

        // Fill geometry
        auto overview = MakeOverview(leg_geometries);
        std::variant<flatbuffers::Offset<flatbuffers::String>,
                     flatbuffers::Offset<flatbuffers::Vector<const fbresult::Position *>>>
            geometry;
        if (overview)
        {
            geometry = MakeGeometry(fb_result, overview->begin(), overview->end());
        }

        auto weight_name_string = fb_result.CreateString(facade.GetWeightName());

        fbresult::RouteObjectBuilder routeObject(fb_result);
        routeObject.add_distance(route.distance);
        routeObject.add_duration(route.duration);
        routeObject.add_weight(route.weight);
        routeObject.add_weight_name(weight_name_string);
        routeObject.add_legs(legs_vector);
        if (overview)
        {
            std::visit(GeometryVisitor<fbresult::RouteObjectBuilder>(routeObject), geometry);
        }

        return routeObject.Finish();
    }

    flatbuffers::Offset<fbresult::Annotation>
    MakeFBAnnotations(flatbuffers::FlatBufferBuilder &fb_result,
                      guidance::LegGeometry &leg_geometry,
                      const RouteParameters::AnnotationsType &requested_annotations) const
    {
        // AnnotationsType uses bit flags, & operator checks if a property is set
        flatbuffers::Offset<flatbuffers::Vector<float>> speed;
        if (requested_annotations & RouteParameters::AnnotationsType::Speed)
        {
            double prev_speed = 0;
            speed = GetAnnotations<float>(
                fb_result,
                leg_geometry,
                [&prev_speed](const guidance::LegGeometry::Annotation &anno)
                {
                    if (anno.duration < std::numeric_limits<float>::min())
                    {
                        return prev_speed;
                    }
                    else
                    {
                        auto speed = std::round(anno.distance / anno.duration * 10.) / 10.;
                        prev_speed = speed;
                        return util::json::clamp_float(speed);
                    }
                });
        }

        flatbuffers::Offset<flatbuffers::Vector<uint32_t>> duration;
        if (requested_annotations & RouteParameters::AnnotationsType::Duration)
        {
            duration = GetAnnotations<uint32_t>(fb_result,
                                                leg_geometry,
                                                [](const guidance::LegGeometry::Annotation &anno)
                                                { return anno.duration; });
        }

        flatbuffers::Offset<flatbuffers::Vector<uint32_t>> distance;
        if (requested_annotations & RouteParameters::AnnotationsType::Distance)
        {
            distance = GetAnnotations<uint32_t>(fb_result,
                                                leg_geometry,
                                                [](const guidance::LegGeometry::Annotation &anno)
                                                { return anno.distance; });
        }

        flatbuffers::Offset<flatbuffers::Vector<uint32_t>> weight;
        if (requested_annotations & RouteParameters::AnnotationsType::Weight)
        {
            weight = GetAnnotations<uint32_t>(fb_result,
                                              leg_geometry,
                                              [](const guidance::LegGeometry::Annotation &anno)
                                              { return anno.weight; });
        }

        flatbuffers::Offset<flatbuffers::Vector<uint32_t>> datasources;
        if (requested_annotations & RouteParameters::AnnotationsType::Datasources)
        {
            datasources = GetAnnotations<uint32_t>(fb_result,
                                                   leg_geometry,
                                                   [](const guidance::LegGeometry::Annotation &anno)
                                                   { return anno.datasource; });
        }
        std::vector<uint32_t> nodes;
        if (requested_annotations & RouteParameters::AnnotationsType::Nodes)
        {
            nodes.reserve(leg_geometry.node_ids.size());
            for (const auto node_id : leg_geometry.node_ids)
            {
                nodes.emplace_back(static_cast<uint64_t>(facade.GetOSMNodeIDOfNode(node_id)));
            }
        }
        auto nodes_vector = fb_result.CreateVector(nodes);
        // Add any supporting metadata, if needed
        bool use_metadata = requested_annotations & RouteParameters::AnnotationsType::Datasources;
        flatbuffers::Offset<fbresult::Metadata> metadata_buffer;
        if (use_metadata)
        {
            const auto MAX_DATASOURCE_ID = 255u;
            std::vector<flatbuffers::Offset<flatbuffers::String>> names;
            for (auto i = 0u; i < MAX_DATASOURCE_ID; i++)
            {
                const auto name = facade.GetDatasourceName(i);
                // Length of 0 indicates the first empty name, so we can stop here
                if (name.empty())
                    break;
                names.emplace_back(
                    fb_result.CreateString(std::string(facade.GetDatasourceName(i))));
            }
            metadata_buffer = fbresult::CreateMetadataDirect(fb_result, &names);
        }
        fbresult::AnnotationBuilder annotation(fb_result);
        annotation.add_speed(speed);
        annotation.add_duration(duration);
        annotation.add_distance(distance);
        annotation.add_weight(weight);
        annotation.add_datasources(datasources);
        annotation.add_nodes(nodes_vector);
        if (use_metadata)
        {
            annotation.add_metadata(metadata_buffer);
        }

        return annotation.Finish();
    }

    template <typename Builder> class GeometryVisitor
    {
      public:
        GeometryVisitor(Builder &builder) : builder(builder) {}

        void operator()(const flatbuffers::Offset<flatbuffers::String> &value)
        {
            builder.add_polyline(value);
        }
        void operator()(
            const flatbuffers::Offset<flatbuffers::Vector<const fbresult::Position *>> &value)
        {
            builder.add_coordinates(value);
        }

      private:
        Builder &builder;
    };

    flatbuffers::Offset<fbresult::Step> MakeFBStep(flatbuffers::FlatBufferBuilder &builder,
                                                   const guidance::LegGeometry &leg_geometry,
                                                   const guidance::RouteStep &step) const
    {
        auto name_string = builder.CreateString(step.name);

        flatbuffers::Offset<flatbuffers::String> ref_string;
        if (!step.ref.empty())
        {
            ref_string = builder.CreateString(step.ref);
        }

        flatbuffers::Offset<flatbuffers::String> pronunciation_string;
        if (!step.pronunciation.empty())
        {
            pronunciation_string = builder.CreateString(step.pronunciation);
        }

        flatbuffers::Offset<flatbuffers::String> destinations_string;
        if (!step.destinations.empty())
        {
            destinations_string = builder.CreateString(step.destinations);
        }

        flatbuffers::Offset<flatbuffers::String> exists_string;
        if (!step.exits.empty())
        {
            exists_string = builder.CreateString(step.exits);
        }

        flatbuffers::Offset<flatbuffers::String> rotary_name_string;
        flatbuffers::Offset<flatbuffers::String> rotary_pronunciation_string;
        if (!step.rotary_name.empty())
        {
            rotary_name_string = builder.CreateString(step.rotary_name);
            if (!step.rotary_pronunciation.empty())
            {
                rotary_pronunciation_string = builder.CreateString(step.rotary_pronunciation);
            }
        }
        auto mode_string = builder.CreateString(extractor::travelModeToString(step.mode));

        // Geometry
        auto geometry = MakeGeometry(builder,
                                     leg_geometry.locations.begin() + step.geometry_begin,
                                     leg_geometry.locations.begin() + step.geometry_end);
        // Maneuver
        fbresult::StepManeuverBuilder maneuver(builder);
        fbresult::Position maneuverPosition{
            static_cast<float>(util::toFloating(step.maneuver.location.lon).__value),
            static_cast<float>(util::toFloating(step.maneuver.location.lat).__value)};
        maneuver.add_location(&maneuverPosition);
        maneuver.add_bearing_before(step.maneuver.bearing_before);
        maneuver.add_bearing_after(step.maneuver.bearing_after);
        if (step.maneuver.waypoint_type == guidance::WaypointType::None)
            maneuver.add_type(TurnTypeToFB(step.maneuver.instruction.type));
        else
            maneuver.add_type(WaypointTypeToFB(step.maneuver.waypoint_type));
        if (osrm::engine::api::json::detail::isValidModifier(step.maneuver))
        {
            maneuver.add_modifier(TurnModifierToFB(step.maneuver.instruction.direction_modifier));
        }
        if (step.maneuver.exit != 0)
        {
            maneuver.add_exit(step.maneuver.exit);
        }
        auto maneuver_buffer = maneuver.Finish();

        // intersections
        auto intersections_vector = MakeFBIntersections(builder, step);

        fbresult::StepBuilder stepBuilder(builder);
        stepBuilder.add_duration(step.duration);
        stepBuilder.add_distance(step.distance);
        stepBuilder.add_weight(step.weight);
        stepBuilder.add_name(name_string);
        stepBuilder.add_mode(mode_string);
        stepBuilder.add_driving_side(step.is_left_hand_driving);
        stepBuilder.add_ref(ref_string);
        stepBuilder.add_pronunciation(pronunciation_string);
        stepBuilder.add_destinations(destinations_string);
        stepBuilder.add_exits(exists_string);
        stepBuilder.add_rotary_name(rotary_name_string);
        stepBuilder.add_rotary_pronunciation(rotary_pronunciation_string);
        stepBuilder.add_intersections(intersections_vector);
        stepBuilder.add_maneuver(maneuver_buffer);
        std::visit(GeometryVisitor<fbresult::StepBuilder>(stepBuilder), geometry);
        return stepBuilder.Finish();
    };

    flatbuffers::Offset<flatbuffers::Vector<flatbuffers::Offset<fbresult::Intersection>>>
    MakeFBIntersections(flatbuffers::FlatBufferBuilder &fb_result,
                        const guidance::RouteStep &step) const
    {
        std::vector<flatbuffers::Offset<fbresult::Intersection>> intersections;
        intersections.resize(step.intersections.size());
        std::transform(
            step.intersections.begin(),
            step.intersections.end(),
            intersections.begin(),
            [&fb_result, this](const guidance::IntermediateIntersection &intersection)
            {
                std::vector<flatbuffers::Offset<fbresult::Lane>> lanes;
                if (json::detail::hasValidLanes(intersection))
                {
                    BOOST_ASSERT(intersection.lanes.lanes_in_turn >= 1);
                    lanes.reserve(intersection.lane_description.size());
                    LaneID lane_id = intersection.lane_description.size();

                    for (const auto &lane_desc : intersection.lane_description)
                    {
                        --lane_id;
                        auto indications = TurnLaneTypeToFB(lane_desc);

                        auto lane_valid = lane_id >= intersection.lanes.first_lane_from_the_right &&
                                          lane_id < intersection.lanes.first_lane_from_the_right +
                                                        intersection.lanes.lanes_in_turn;
                        lanes.push_back(
                            fbresult::CreateLaneDirect(fb_result, &indications, lane_valid));
                    }
                }
                auto lanes_vector = fb_result.CreateVector(lanes);

                fbresult::Position maneuverPosition{
                    static_cast<float>(util::toFloating(intersection.location.lon).__value),
                    static_cast<float>(util::toFloating(intersection.location.lat).__value)};
                auto bearings_vector = fb_result.CreateVector(intersection.bearings);
                std::vector<flatbuffers::Offset<flatbuffers::String>> classes;
                classes.resize(intersection.classes.size());
                std::transform(intersection.classes.begin(),
                               intersection.classes.end(),
                               classes.begin(),
                               [&fb_result](const std::string &cls)
                               { return fb_result.CreateString(cls); });
                auto classes_vector = fb_result.CreateVector(classes);
                auto entry_vector = fb_result.CreateVector(intersection.entry);

                fbresult::IntersectionBuilder intersectionBuilder(fb_result);
                intersectionBuilder.add_location(&maneuverPosition);
                intersectionBuilder.add_bearings(bearings_vector);
                intersectionBuilder.add_classes(classes_vector);
                intersectionBuilder.add_entry(entry_vector);
                intersectionBuilder.add_in_bearing(intersection.in);
                intersectionBuilder.add_out_bearing(intersection.out);
                intersectionBuilder.add_lanes(lanes_vector);
                return intersectionBuilder.Finish();
            });
        return fb_result.CreateVector(intersections);
    }

    util::json::Object MakeRoute(const std::vector<PhantomEndpoints> &leg_endpoints,
                                 const std::vector<std::vector<PathData>> &unpacked_path_segments,
                                 const std::vector<bool> &source_traversed_in_reverse,
                                 const std::vector<bool> &target_traversed_in_reverse) const
    {
        auto legs_info = MakeLegs(leg_endpoints,
                                  unpacked_path_segments,
                                  source_traversed_in_reverse,
                                  target_traversed_in_reverse);
        std::vector<guidance::RouteLeg> &legs = legs_info.first;
        std::vector<guidance::LegGeometry> &leg_geometries = legs_info.second;

        auto route = guidance::assembleRoute(legs);
        std::optional<util::json::Value> json_overview = MakeGeometry(MakeOverview(leg_geometries));

        std::vector<util::json::Value> json_overview_by_legs;
        if (parameters.overview == RouteParameters::OverviewType::ByLegs)
        {
            json_overview_by_legs.reserve(leg_geometries.size());
            for (const auto &leg_geometry : leg_geometries)
            {
                json_overview_by_legs.emplace_back(MakeGeometry(leg_geometry.locations));
            }
        }

        std::vector<util::json::Value> step_geometries;
        const auto total_step_count =
            std::accumulate(legs.begin(),
                            legs.end(),
                            0,
                            [](const auto &v, const auto &leg) { return v + leg.steps.size(); });
        step_geometries.reserve(total_step_count);

        for (const auto idx : util::irange<std::size_t>(0UL, legs.size()))
        {
            auto &leg_geometry = leg_geometries[idx];

            std::transform(
                legs[idx].steps.begin(),
                legs[idx].steps.end(),
                std::back_inserter(step_geometries),
                [this, &leg_geometry](const guidance::RouteStep &step)
                {
                    if (parameters.geometries == RouteParameters::GeometriesType::Polyline)
                    {
                        return static_cast<util::json::Value>(json::makePolyline<100000>(
                            leg_geometry.locations.begin() + step.geometry_begin,
                            leg_geometry.locations.begin() + step.geometry_end));
                    }

                    if (parameters.geometries == RouteParameters::GeometriesType::Polyline6)
                    {
                        return static_cast<util::json::Value>(json::makePolyline<1000000>(
                            leg_geometry.locations.begin() + step.geometry_begin,
                            leg_geometry.locations.begin() + step.geometry_end));
                    }

                    BOOST_ASSERT(parameters.geometries == RouteParameters::GeometriesType::GeoJSON);
                    return static_cast<util::json::Value>(json::makeGeoJSONGeometry(
                        leg_geometry.locations.begin() + step.geometry_begin,
                        leg_geometry.locations.begin() + step.geometry_end));
                });
        }

        std::vector<util::json::Object> annotations;

        // To maintain support for uses of the old default constructors, we check
        // if annotations property was set manually after default construction
        auto requested_annotations = parameters.annotations_type;
        if (parameters.annotations &&
            (parameters.annotations_type == RouteParameters::AnnotationsType::None))
        {
            requested_annotations = RouteParameters::AnnotationsType::All;
        }

        if (requested_annotations != RouteParameters::AnnotationsType::None)
        {
            for (const auto idx : util::irange<std::size_t>(0UL, leg_geometries.size()))
            {
                auto &leg_geometry = leg_geometries[idx];
                util::json::Object annotation;

                // AnnotationsType uses bit flags, & operator checks if a property is set
                if (requested_annotations & RouteParameters::AnnotationsType::Speed)
                {
                    double prev_speed = 0;
                    annotation.values.emplace(
                        "speed",
                        GetAnnotations(leg_geometry,
                                       [&prev_speed](const guidance::LegGeometry::Annotation &anno)
                                       {
                                           if (anno.duration < std::numeric_limits<double>::min())
                                           {
                                               return prev_speed;
                                           }
                                           else
                                           {
                                               auto speed =
                                                   std::round(anno.distance / anno.duration * 10.) /
                                                   10.;
                                               prev_speed = speed;
                                               return util::json::clamp_float(speed);
                                           }
                                       }));
                }

                if (requested_annotations & RouteParameters::AnnotationsType::Duration)
                {
                    annotation.values.emplace(
                        "duration",
                        GetAnnotations(leg_geometry,
                                       [](const guidance::LegGeometry::Annotation &anno)
                                       { return anno.duration; }));
                }
                if (requested_annotations & RouteParameters::AnnotationsType::Distance)
                {
                    annotation.values.emplace(
                        "distance",
                        GetAnnotations(leg_geometry,
                                       [](const guidance::LegGeometry::Annotation &anno)
                                       { return anno.distance; }));
                }
                if (requested_annotations & RouteParameters::AnnotationsType::Weight)
                {
                    annotation.values.emplace(
                        "weight",
                        GetAnnotations(leg_geometry,
                                       [](const guidance::LegGeometry::Annotation &anno)
                                       { return anno.weight; }));
                }
                if (requested_annotations & RouteParameters::AnnotationsType::Datasources)
                {
                    annotation.values.emplace(
                        "datasources",
                        GetAnnotations(leg_geometry,
                                       [](const guidance::LegGeometry::Annotation &anno)
                                       { return anno.datasource; }));
                }
                if (requested_annotations & RouteParameters::AnnotationsType::Nodes)
                {
                    util::json::Array nodes;
                    nodes.values.reserve(leg_geometry.node_ids.size());
                    for (const auto node_id : leg_geometry.node_ids)
                    {
                        nodes.values.push_back(
                            static_cast<std::uint64_t>(facade.GetOSMNodeIDOfNode(node_id)));
                    }
                    annotation.values.emplace("nodes", std::move(nodes));
                }
                // Add any supporting metadata, if needed
                if (requested_annotations & RouteParameters::AnnotationsType::Datasources)
                {
                    const auto MAX_DATASOURCE_ID = 255u;
                    util::json::Object metadata;
                    util::json::Array datasource_names;
                    for (auto i = 0u; i < MAX_DATASOURCE_ID; i++)
                    {
                        const auto name = facade.GetDatasourceName(i);
                        // Length of 0 indicates the first empty name, so we can stop here
                        if (name.empty())
                            break;
                        datasource_names.values.push_back(std::string(facade.GetDatasourceName(i)));
                    }
                    metadata.values.emplace("datasource_names", datasource_names);
                    annotation.values.emplace("metadata", metadata);
                }

                annotations.push_back(std::move(annotation));
            }
        }

        auto result = json::makeRoute(route,
                                      json::makeRouteLegs(std::move(legs),
                                                          std::move(json_overview_by_legs),
                                                          std::move(step_geometries),
                                                          std::move(annotations)),
                                      std::move(json_overview),
                                      facade.GetWeightName());

        return result;
    }

    const RouteParameters &parameters;

    std::pair<std::vector<guidance::RouteLeg>, std::vector<guidance::LegGeometry>>
    MakeLegs(const std::vector<PhantomEndpoints> &leg_endpoints,
             const std::vector<std::vector<PathData>> &unpacked_path_segments,
             const std::vector<bool> &source_traversed_in_reverse,
             const std::vector<bool> &target_traversed_in_reverse) const
    {
        auto result =
            std::make_pair(std::vector<guidance::RouteLeg>(), std::vector<guidance::LegGeometry>());
        auto &legs = result.first;
        auto &leg_geometries = result.second;
        auto number_of_legs = leg_endpoints.size();
        legs.reserve(number_of_legs);
        leg_geometries.reserve(number_of_legs);

        for (auto idx : util::irange<std::size_t>(0UL, number_of_legs))
        {
            const auto &phantoms = leg_endpoints[idx];
            const auto &path_data = unpacked_path_segments[idx];

            const bool reversed_source = source_traversed_in_reverse[idx];
            const bool reversed_target = target_traversed_in_reverse[idx];

            auto leg = guidance::assembleLeg(facade,
                                             path_data,
                                             phantoms.source_phantom,
                                             phantoms.target_phantom,
                                             reversed_target);

            guidance::LegGeometry leg_geometry;

            // Generate additional geometry data if request includes turn-by-turn steps,
            // overview geometry or route geometry annotations.
            // Note that overview geometry and route geometry annotations can return different
            // results depending on whether turn-by-turn steps are also requested.
            if (parameters.steps || parameters.annotations ||
                parameters.overview != RouteParameters::OverviewType::False)
            {

                leg_geometry = guidance::assembleGeometry(BaseAPI::facade,
                                                          path_data,
                                                          phantoms.source_phantom,
                                                          phantoms.target_phantom,
                                                          reversed_source,
                                                          reversed_target);

                util::Log(logDEBUG) << "Assembling steps " << std::endl;
                if (parameters.steps)
                {
                    leg.summary = guidance::assembleSummary(
                        facade, path_data, phantoms.target_phantom, reversed_target);

                    auto steps = guidance::assembleSteps(BaseAPI::facade,
                                                         path_data,
                                                         leg_geometry,
                                                         phantoms.source_phantom,
                                                         phantoms.target_phantom,
                                                         reversed_source,
                                                         reversed_target);

                    // Apply maneuver overrides before any other post
                    // processing is performed
                    guidance::applyOverrides(BaseAPI::facade, steps, leg_geometry);

                    // Collapse segregated steps before others
                    const auto max_collapse_distance = BaseAPI::facade.GetMaxCollapseDistance();
                    steps = guidance::collapseSegregatedTurnInstructions(std::move(steps),
                                                                         max_collapse_distance);

                    /* Perform step-based post-processing.
                     *
                     * Using post-processing on basis of route-steps for a single leg at a time
                     * comes at the cost that we cannot count the correct exit for roundabouts.
                     * We can only emit the exit nr/intersections up to/starting at a part of the
                     *leg. If a roundabout is not terminated in a leg, we will end up with a
                     *enter-roundabout
                     * and exit-roundabout-nr where the exit nr is out of sync with the previous
                     *enter.
                     *
                     *         | S |
                     *         *   *
                     *  ----*        * ----
                     *                  T
                     *  ----*        * ----
                     *       V *   *
                     *         |   |
                     *         |   |
                     *
                     * Coming from S via V to T, we end up with the legs S->V and V->T. V-T will say
                     *to take the second exit, even though counting from S it would be the third.
                     * For S, we only emit `roundabout` without an exit number, showing that we
                     *enter a roundabout to find a via point. The same exit will be emitted, though,
                     *if we should start routing at S, making the overall response consistent.
                     *
                     * ⚠ CAUTION: order of post-processing steps is important
                     *    - handleRoundabouts must be called before collapseTurnInstructions that
                     *      expects post-processed roundabouts
                     */

                    guidance::trimShortSegments(steps, leg_geometry);
                    leg.steps = guidance::handleRoundabouts(std::move(steps));
                    leg.steps = guidance::collapseTurnInstructions(std::move(leg.steps),
                                                                   max_collapse_distance);
                    leg.steps = guidance::anticipateLaneChange(std::move(leg.steps));
                    leg.steps = guidance::buildIntersections(std::move(leg.steps));
                    leg.steps = guidance::suppressShortNameSegments(std::move(leg.steps));
                    leg.steps = guidance::assignRelativeLocations(std::move(leg.steps),
                                                                  leg_geometry,
                                                                  phantoms.source_phantom,
                                                                  phantoms.target_phantom);
                    leg_geometry = guidance::resyncGeometry(std::move(leg_geometry), leg.steps);
                }
            }

            leg_geometries.push_back(std::move(leg_geometry));
            legs.push_back(std::move(leg));
        }
        return result;
    }

    std::optional<std::vector<Coordinate>>
    MakeOverview(const std::vector<guidance::LegGeometry> &leg_geometries) const
    {
        std::optional<std::vector<Coordinate>> overview;
        if (parameters.overview != RouteParameters::OverviewType::False &&
            parameters.overview != RouteParameters::OverviewType::ByLegs)
        {
            const auto use_simplification =
                parameters.overview == RouteParameters::OverviewType::Simplified;
            BOOST_ASSERT(use_simplification ||
                         parameters.overview == RouteParameters::OverviewType::Full);

            overview = guidance::assembleOverview(leg_geometries, use_simplification);
        }
        return overview;
    }
};

} // namespace osrm::engine::api

#endif
