#include "road_markings.h"

#include <maps/renderer/libs/marking/include/crosswalk.h>
#include <maps/renderer/libs/marking/include/double_line.h>
#include <maps/renderer/libs/marking/include/give_way_line.h>
#include <maps/renderer/libs/marking/include/parking.h>
#include <maps/renderer/libs/marking/include/safety_island.h>
#include <maps/renderer/libs/marking/include/speed_bump.h>
#include <maps/renderer/libs/marking/include/symbol.h>
#include <maps/renderer/libs/marking/include/transport_stop_line.h>
#include <maps/renderer/libs/marking/include/waffel.h>

#include <maps/renderer/libs/geos_geometry/include/io.h>

#include <maps/libs/geolib/include/linear_ring.h>
#include <maps/libs/geolib/include/multipolygon.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/conversion_geos.h>
#include <contrib/libs/geos/include/geos/geom/MultiPolygon.h>

using namespace maps::renderer;

namespace geom = maps::renderer::geos_geometry;

namespace maps::wiki::renderer {

namespace {

const double DOUBLE_LINE_DIST =
    /*offset from center*/ 0.07 + /*half line width*/ 0.08;

const std::string LEFT_SIDE = "left";
const std::string RIGHT_SIDE = "right";

struct StyleParams {
    std::string width;
    std::optional<std::string> dash;
    std::string color;
};

// clang-format off
const std::unordered_map<ymapsdf::ft::Type, StyleParams> LINE_STYLES = {
    {ymapsdf::ft::Type::HdmapRoadMarkingPolygonalWaffel,            {"15", std::nullopt, "yellow"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane1,                {"15", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane2,                {"20", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane3,                {"15", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane4,                {"15", std::nullopt, "yellow"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane5,                {"15",    "100_300", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane6,                {"15",    "300_100", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane7,                {"15",      "50_50", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane8,                {"30",    "100_300", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane9,                {"15",    "300_100", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane10,               {"15",    "100_100", "yellow"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane11,               {"15", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearLane12,               {"40", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearTransportStop171,     {"10", std::nullopt, "yellow"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearTransportStop172,     {"10", std::nullopt, "yellow"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingPolygonalIsland,            {"10", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearParking33,            {"10", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingPerpendicular, {"10", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingDiagonal,      {"10", std::nullopt, "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingDottedLine,    {"10",    "100_100", "white"}},
    {ymapsdf::ft::Type::HdmapRoadMarkingLinearBump26,               {"60",    "40_20",   "yellow"}},
};
// clang-format on

// style for dashed part of dashed-solid line marking
const StyleParams StyleParamsLane11Dashed = {"15", "90_30", "white"};

const base::Zoom VIEW_MAX_ZOOM = 22;

const std::unordered_map<ymapsdf::ft::Type, marking::CrossingType> CROSSING_TYPES = {
    {ymapsdf::ft::Type::HdmapRoadMarkingPolygonalPedestrian141, marking::CrossingType::Regular},
    {ymapsdf::ft::Type::HdmapRoadMarkingPolygonalPedestrian142, marking::CrossingType::YellowWhite},
};

struct CrosswalkScale
{
    base::ZoomRange zooms;
    double scale;
};

constexpr std::array<CrosswalkScale, 2> CROSSWALK_SCALES = {{
    {{15, 17}, 2},
    {{18, VIEW_MAX_ZOOM}, 1}
}};

// call once makeWaffel and set 1st level for range {15-17}, and both for range {18-VIEW_MAX_ZOOM}
constexpr std::array<base::ZoomRange, 2> WAFFEL_ZOOM_RANGES = {{
    {15, VIEW_MAX_ZOOM},
    {18, VIEW_MAX_ZOOM}
}};

const double WAFFEL_STEP = 1.5;

using Dict = std::unordered_map<std::string, std::string>;

Dict asDict(const StyleParams& style,
            const std::optional<std::string>& side = std::nullopt)
{
    Dict result;
    result["width"] = style.width;
    result["color"] = style.color;
    if (style.dash)
        result["dash"] = style.dash.value();
    if (side)
        result["side"] = side.value();
    return result;
}

Dict asDict(marking::ColorType color)
{
    return {{"color",  std::string(toString(color))}};
}

const StyleParams& findMarkingStyle(ymapsdf::ft::Type ftTypeId)
{
    return LINE_STYLES.contains(ftTypeId)
        ? LINE_STYLES.at(ftTypeId)
        : LINE_STYLES.at(ymapsdf::ft::Type::HdmapRoadMarkingLinearLane1);
}

std::vector<OutputMarking> generateWaffel(const InputMarking& row, base::Zoom zoom)
{
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));

    const auto& pgon = dynamic_cast<const geom::Polygon&>(*row.geom);
    std::vector<std::unique_ptr<geos::geom::MultiLineString>> geoms =
        std::move(marking::makeWaffel(
            pgon,
            row.yaw.value_or(45),
            WAFFEL_STEP,
            /*waffel level count*/ WAFFEL_ZOOM_RANGES.size()));

    std::vector<OutputMarking> result;
    for (size_t level = 0; level < WAFFEL_ZOOM_RANGES.size(); ++level) {
        const auto& zooms = WAFFEL_ZOOM_RANGES[level];
        if (contains(zooms, zoom)) {
            result.push_back({std::move(geoms[level]), attrs});
        }
    }
    return result;
}

std::vector<OutputMarking> generateDouble(const InputMarking& row)
{
    marking::DoubleLineGeos dline =
        marking::makeDoubleLine(*row.geom, DOUBLE_LINE_DIST);

    const auto style = findMarkingStyle(row.ftTypeId);
    std::vector<OutputMarking> result;
    result.push_back({std::move(dline.left), asDict(style, LEFT_SIDE)});
    result.push_back({std::move(dline.right), asDict(style, RIGHT_SIDE)});
    return result;
}

std::vector<OutputMarking> generateDashedSolid(const InputMarking& row)
{
    marking::DoubleLineGeos dline =
        marking::makeDoubleLine(*row.geom, DOUBLE_LINE_DIST);

    std::vector<OutputMarking> result;
    result.push_back({std::move(dline.left), asDict(
        StyleParamsLane11Dashed, LEFT_SIDE)});
    result.push_back({std::move(dline.right), asDict(
        findMarkingStyle(
            ymapsdf::ft::Type::HdmapRoadMarkingLinearLane1), RIGHT_SIDE)});
    return result;
}

std::vector<OutputMarking> generateTransportStop(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    std::unique_ptr<geos::geom::LineString> geom =
        marking::makeTransportStopLine(pline);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generateCrosswalk(const InputMarking& row, base::Zoom zoom)
{
    auto crossingType = CROSSING_TYPES.at(row.ftTypeId);
    const auto& pgon = dynamic_cast<const geom::Polygon&>(*row.geom);

    std::vector<OutputMarking> result;
    for (const auto& param: CROSSWALK_SCALES) {
        if (contains(param.zooms, zoom)) {
            for (auto& crosswalk: marking::makeCrosswalk(
                     pgon,
                     crossingType,
                     marking::getCrosswalkAngle(pgon), 
                     param.scale)) {
                result.push_back({
                    std::move(crosswalk.geom),
                    asDict(crosswalk.color)});
            }
        }
    }
    return result;
}

std::vector<OutputMarking> generateGiveWayLine(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    std::vector<OutputMarking> result;
    result.push_back({
        marking::makeGiveWayLine(pline),
        asDict(marking::ColorType::White)
    });
    return result;
}

std::vector<OutputMarking> generateSpeedBumpCheckered(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    std::vector<OutputMarking> result;
    result.push_back({
        marking::makeSpeedBumpCheckered(pline),
        asDict(marking::ColorType::White)
    });
    return result;
}

std::vector<OutputMarking> generateSimpleSafetyIsland(const InputMarking& row)
{
    const auto& pgon = dynamic_cast<const geom::Polygon&>(*row.geom);
    auto geom = marking::makeSimpleSafetyIsland(
        pgon, /*angleRad*/ 0.0, /*step*/ 2.0);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({row.geom->getBoundary(), attrs});
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generateDiagonalParking(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    auto geom = marking::makeDiagonalParking(
        pline, /*rotationDeg*/ 45.0);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generateParallelParking(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    auto geom = marking::makeParallelParking(pline);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generatePerpendicularParking(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    auto geom = marking::makeDiagonalParking(pline, /*rotationDeg*/ 0.0);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generateDottedLineParking(const InputMarking& row)
{
    const auto& pline = dynamic_cast<const geom::LineString&>(*row.geom);
    auto geom = marking::makeDottedLineParking(pline);
    auto attrs = asDict(findMarkingStyle(row.ftTypeId));
    std::vector<OutputMarking> result;
    result.push_back({std::move(geom), attrs});
    return result;
}

std::vector<OutputMarking> generateSymbol(
    const marking::SymbolType& symbolType, const InputMarking& row)
{
    ASSERT(row.yaw);
    const auto& p = dynamic_cast<const geom::Point&>(*row.geom);
    auto mpgon =
        marking::makeSymbol(symbolType, *(p.getCoordinate()), row.yaw.value());
    std::vector<OutputMarking> result;
    result.push_back(
        {.geom = std::move(mpgon)});

    return result;
}

} // namespace

std::vector<OutputMarking> generate(InputMarking&& input, base::Zoom zoom)
{
    std::vector<OutputMarking> result;

    if (LINE_STYLES.contains(input.ftTypeId)) {
        switch (input.ftTypeId) {
        case ymapsdf::ft::Type::HdmapRoadMarkingPolygonalWaffel:
            return generateWaffel(input, zoom);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearLane3:
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearLane9:
            return generateDouble(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearLane11:
            return generateDashedSolid(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearTransportStop171:
            return generateTransportStop(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingPolygonalIsland:
            return generateSimpleSafetyIsland(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearParking33:
            return generateParallelParking(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingPerpendicular:
            return generatePerpendicularParking(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingDiagonal:
            return generateDiagonalParking(input);
        case ymapsdf::ft::Type::HdmapRoadMarkingLinearParkingDottedLine:
            return generateDottedLineParking(input);
        default: {
                std::vector<OutputMarking> result;
                result.push_back(
                    {std::move(input.geom), asDict(findMarkingStyle(input.ftTypeId))});
                return result;
            }
        }
    } else if (auto symbolType = marking::ftTypeToSymbolType(input.ftTypeId)) {
        return generateSymbol(*symbolType, input);
    } else if (CROSSING_TYPES.contains(input.ftTypeId)) {
        return generateCrosswalk(input, zoom);
    } else if (input.ftTypeId == ymapsdf::ft::Type::HdmapRoadMarkingLinearLane13) {
        return generateGiveWayLine(input);
    } else if (input.ftTypeId == ymapsdf::ft::Type::HdmapRoadMarkingLinearBump25) {
        return generateSpeedBumpCheckered(input);
    }
    return result;
}

} // namespace maps::wiki::renderer
