#include "route_context.h"
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/polyline.h>
#include <maps/libs/geolib/include/serialization.h>

namespace maps::wiki::sync_fbapi_feedback {

namespace {

void pointsToJsonString(
    json::ObjectBuilder& builder,
    const std::string& name,
    const std::vector<geolib3::Point2>& points)
{
    builder[name] << [&](json::ObjectBuilder builder) {
        builder["type"] = "GeometryCollection";
        builder["geometries"] = [&](json::ArrayBuilder builder) {
            for (const auto& point : points) {
                builder << geolib3::geojson(point);
            }
        };
    };
}

template <typename T>
void putOptional(
    json::ObjectBuilder& builder,
    const std::string& fieldName,
    const std::optional<T>& value)
{
    if (value) {
        builder[fieldName] = *value;
    }
}

std::string vehicleRestrictionsAsString(const fbapi::VehicleRestrictions& vehicleRestrictions)
{
    json::Builder localBuilder;
    localBuilder.setDoublePrecision(4);
    localBuilder << [&](json::ObjectBuilder builder) {
        putOptional(builder, "weight", vehicleRestrictions.weight());
        putOptional(builder, "maxWeight", vehicleRestrictions.maxWeight());
        putOptional(builder, "payload", vehicleRestrictions.payload());
        putOptional(builder, "axleWeight", vehicleRestrictions.axleWeight());
        putOptional(builder, "height", vehicleRestrictions.height());
        putOptional(builder, "length", vehicleRestrictions.length());
        putOptional(builder, "width", vehicleRestrictions.width());
        putOptional(builder, "ecoClass", vehicleRestrictions.ecoClass());
        putOptional(builder, "hasTrailer", vehicleRestrictions.hasTrailer());
    };

    return localBuilder.str();
}

} // unnamed namespace

RouteContext::RouteContext(
        fbapi::TravelMode travelMode,
        bool trafficJams,
        geolib3::PointsVector wayPointsMercator,
        geolib3::PointsVector pointsGeodetic,
        fbapi::RouteQuestionSegments segments)
    : travelMode_(travelMode)
    , trafficJams_(trafficJams)
    , wayPointsMercator_(std::move(wayPointsMercator))
    , pointsGeodetic_(std::move(pointsGeodetic))
    , segments_(std::move(segments))
{}

RouteContext& RouteContext::setViaPointsMercator(geolib3::PointsVector viaPointsMercator)
{
    viaPointsMercator_ = std::move(viaPointsMercator);
    return *this;
}

RouteContext& RouteContext::setUserErrors(fbapi::RouteErrors userErrors)
{
    userErrors_ = std::move(userErrors);
    return *this;
}

RouteContext& RouteContext::setUserRoute(fbapi::RouteAnswerSegments userRoute)
{
    userRoute_ = std::move(userRoute);
    return *this;
}

RouteContext& RouteContext::setUri(std::string uri)
{
    uri_ = std::move(uri);
    return *this;
}

RouteContext& RouteContext::setDepartureAt(chrono::TimePoint timePoint)
{
    departureAt_ = timePoint;
    return *this;
}

RouteContext& RouteContext::setVehicleRestrictions(fbapi::VehicleRestrictions value)
{
    vehicleRestrictions_ = std::move(value);
    return *this;
}

void toJson(json::ObjectBuilder& builder, const fbapi::RouteError& routeError)
{
    builder["point"] << geolib3::geojson(
        geolib3::convertMercatorToGeodetic(routeError.point()));
    builder["type"] << toString(routeError.type());
    if (routeError.type() == fbapi::RouteErrorType::ProhibitingSign) {
        auto context = routeError.prohibitingSignContext();
        if (context) {
            builder["context"] << [&](json::ObjectBuilder builder) {
                auto vehicleRestrictions = context->vehicleRestrictions();
                if (vehicleRestrictions) {
                    builder["vehicleRestrictions"]
                        << json::Verbatim(vehicleRestrictionsAsString(*vehicleRestrictions));
                }
            };
        }
    }
}

void RouteContext::json(json::ObjectBuilder builder) const
{
    builder["type"] << "route";
    builder["content"] << [&](json::ObjectBuilder builder) {
        builder["travelMode"] << toString(travelMode_);
        builder["trafficJams"] << trafficJams_;
        pointsToJsonString(builder, "wayPoints",
            geolib3::convertMercatorToGeodetic(wayPointsMercator_));
        if (!viaPointsMercator_.empty()) {
            pointsToJsonString(builder, "viaPoints",
                geolib3::convertMercatorToGeodetic(viaPointsMercator_));
        }
        builder["points"]
            << geolib3::geojson(geolib3::Polyline2(pointsGeodetic_));
        putOptional(builder, "uri", uri_);
        if (departureAt_) {
            builder["departureAt"] << chrono::formatIsoDateTime(*departureAt_);
        }
        builder["segments"] << [&](json::ArrayBuilder builder) {
            for (const auto& segment : segments_) {
                builder << [&](json::ObjectBuilder builder) {
                    builder["from"] << geolib3::geojson(
                        geolib3::convertMercatorToGeodetic(segment.from()));
                    builder["to"] << geolib3::geojson(
                        geolib3::convertMercatorToGeodetic(segment.to()));
                    builder["travelMode"] << toString(segment.travelMode());
                };
            }
        };
        if (vehicleRestrictions_) {
            builder["vehicleRestrictions"]
                << json::Verbatim(vehicleRestrictionsAsString(*vehicleRestrictions_));
        }
        if (!userErrors_.empty()) {
            builder["userErrors"] << [&](json::ArrayBuilder builder) {
                for (const auto& userError : userErrors_) {
                    builder << [&](json::ObjectBuilder builder) {
                        toJson(builder, userError);
                    };
                }
            };
        }
        if (!userRoute_.empty()) {
            builder["userRoute"] << [&](json::ArrayBuilder builder) {
                for (const auto& answerSegment : userRoute_) {
                    builder << [&](json::ObjectBuilder builder) {
                        builder["travelMode"] << toString(answerSegment.travelMode());
                        builder["points"] << geolib3::geojson(geolib3::Polyline2(
                            geolib3::convertMercatorToGeodetic(answerSegment.points())));
                    };
                }
            };
        }
    };
}

} // namespace maps::wiki::sync_fbapi_feedback
