#include "tools.h"
#include "configuration.h"
#include "feature_author.h"
#include "yacare_params.h"

#include <util/generic/string.h>
#include <util/string/cast.h>

#include <maps/analyzer/libs/graphmatching/include/match_geometry.h>
#include <maps/infra/yacare/include/yacare.h>
#include <maps/libs/chrono/include/days.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/make_batches.h>
#include <maps/libs/common/include/math.h>
#include <maps/libs/geolib/include/closest_point.h>
#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/polygon.h>
#include <maps/libs/geolib/include/serialization.h>
#include <maps/libs/geolib/include/units.h>
#include <maps/libs/geolib/include/units_literals.h>
#include <maps/libs/geolib/include/vector.h>
#include <maps/libs/json/include/builder.h>
#include <maps/libs/json/include/std.h>
#include <maps/libs/json/include/value.h>
#include <maps/libs/log8/include/log8.h>
#include <maps/libs/tile/include/const.h>
#include <maps/libs/tile/include/geometry.h>
#include <maps/libs/tile/include/utils.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/types.h>
#include <yandex/maps/geolib3/sproto.h>

#include <boost/accumulators/accumulators.hpp>
#include <boost/accumulators/statistics/stats.hpp>
#include <boost/accumulators/statistics/sum_kahan.hpp>
#include <boost/algorithm/string.hpp>
#include <boost/filesystem.hpp>
#include <boost/range/adaptor/filtered.hpp>
#include <boost/range/adaptor/map.hpp>
#include <boost/range/adaptor/transformed.hpp>
#include <boost/range/algorithm/copy.hpp>
#include <boost/range/algorithm_ext/erase.hpp>

#include <opencv2/opencv.hpp>
#include <opencv2/imgcodecs/imgcodecs_c.h>

#include <algorithm>
#include <array>
#include <cmath>
#include <functional>
#include <memory>
#include <regex>
#include <sstream>
#include <tuple>
#include <type_traits>
#include <unordered_set>

namespace fs = boost::filesystem;
namespace ba = boost::adaptors;
namespace graphmatching = maps::analyzer::graphmatching;
using namespace maps::geolib3::literals;

namespace maps::mrc::browser {
namespace {


struct BySimilarity {
    geolib3::Point2 geodeticPos;
    std::optional<geolib3::Direction2> direction;
    double radiusMeters;

    double distanceFactor(const db::Feature& feature) const
    {
        return 1. -
               geolib3::fastGeoDistance(feature.geodeticPos(), geodeticPos) /
                   radiusMeters;
    }

    double headingFactor(const db::Feature& feature) const
    {
        geolib3::Direction2 featureDir(feature.heading());
        return 1. - (geolib3::angleBetween(featureDir,
                                           direction.value_or(featureDir)) /
                     (geolib3::PI / 4));
    }

    double totalFactor(const db::Feature& feature) const
    {
        return distanceFactor(feature) + headingFactor(feature);
    }

    bool operator()(const db::Feature& feature) const
    {
        return distanceFactor(feature) > 0. && headingFactor(feature) > 0.;
    }

    bool operator()(const db::Feature& lhs, const db::Feature& rhs) const
    {
        return totalFactor(lhs) > totalFactor(rhs);
    };
};

struct GeometryManipulator {
    const db::Feature& feature;

    void json(json::ObjectBuilder builder) const
    {
        builder["type"] = "Point";
        builder["coordinates"]
            = std::vector<double>{feature.geodeticPos().x(), feature.geodeticPos().y()};
    }
};

GeometryManipulator geometry(const db::Feature& feature) { return {feature}; }

common::Size normThumbnailSizeOf(const db::Feature& feature)
{
    const auto size = transformByImageOrientation(feature.size(), feature.orientation());
    return common::getThumbnailSize(size);
}


struct ImageFullManipulator {
    const db::Feature& feature;
    const std::string& url;

    void json(json::ObjectBuilder builder) const
    {
        std::ostringstream imageUrl;
        imageUrl << url << "feature/" << feature.id() << "/image";
        builder["url"] = imageUrl.str();
        const auto size = transformByImageOrientation(feature.size(),
                                                      feature.orientation());
        builder["width"] = size.width;
        builder["height"] = size.height;
    }
};

ImageFullManipulator imageFull(const db::Feature& feature,
                               const std::string& url)
{
    return {feature, url};
}

auto selectFromEachPassage(FeaturesRange features, const BySimilarity& similar)
{
    db::Features result{};
    forEachPassage(features, [&](auto first, auto last) {
        const auto& feature = *std::min_element(first, last, similar);
        if (similar(feature)) {
            result.push_back(feature);
        }
        return LoopControl::Continue;
    });
    return result;
}

auto mostRecent(FeaturesRange features, size_t limit)
{
    limit = std::min(limit, features.size());
    std::partial_sort(features.begin(),
                      features.begin() + limit,
                      features.end(),
                      [](const auto& lhs, const auto& rhs) {
                          return lhs.timestamp() > rhs.timestamp();
                      });
    return db::Features(features.begin(), features.begin() + limit);
}

db::Features mostSimilar(FeaturesRange features,
                         size_t limit,
                         db::CameraDeviation direction)
{
    using namespace chrono::literals;
    auto priority = [direction](const db::Feature& photo) {
        auto penalty = photo.cameraDeviation() == direction ? 0_days : 30_days;
        return photo.timestamp() - penalty;
    };
    limit = std::min(limit, features.size());
    std::partial_sort(features.begin(),
                      features.begin() + limit,
                      features.end(),
                      [&priority](const auto& lhs, const auto& rhs) {
                          return priority(lhs) > priority(rhs);
                      });
    return {features.begin(), features.begin() + limit};
}

auto uniqueUsers(const db::Features& features,
                 const FeatureIdToUserInfo& featureIdToUserInfo)
{
    blackbox_client::UidToUserInfoMap result;
    for (const auto& feature : features) {
        auto it = featureIdToUserInfo.find(feature.id());
        const auto& userInfo =
            (it == featureIdToUserInfo.end() ? YANDEX_USER_INFO : it->second);
        result.insert(
            {boost::lexical_cast<blackbox_client::Uid>(userInfo.uid()),
             userInfo});
    }
    return result;
}

void removeDuplicates(db::Features& features)
{
    std::unordered_set<db::TId> featureIds;
    features.erase(
        std::remove_if(features.begin(), features.end(),
            [&](const db::Feature& feature) {
                auto [it, inserted] = featureIds.insert(feature.id());
                return !inserted;
            }
        ),
        features.end()
    );
}

geolib3::Polyline2 polyline(const road_graph::Graph& graph,
                            road_graph::EdgeId edgeId)
{
    return geolib3::Polyline2(graph.edgeData(edgeId).geometry());
}

using EdgeIdSet = std::set<road_graph::EdgeId>;

bool isPassPhysicallyAccessible(
    const road_graph::Graph& graph,
    road_graph::EdgeId from,
    road_graph::EdgeId to)
{
    const auto distance = geolib3::fastGeoDistance(
        polyline(graph, from).points().back(),
        polyline(graph, to).points().front());

    return distance < geolib3::EPS;
}

bool  isReverseEdge(
    const road_graph::Graph& graph,
    road_graph::EdgeId edgeId,
    road_graph::EdgeId maybeReverseEdgeId)
{
    if (const auto reverseEdgeId = graph.reverse(edgeId)) {
        return graph.base(*reverseEdgeId) == graph.base(maybeReverseEdgeId);
    }
    return false;
}

EdgeIdSet getInEdgeIds(const road_graph::Graph& graph,
                       road_graph::EdgeId edgeId)
{
    EdgeIdSet result;
    const auto inEdgeIds = graph.inEdgeIds(graph.edge(edgeId).source);

    for (auto inEdgeId: inEdgeIds) {
        if (!isReverseEdge(graph, edgeId, inEdgeId) &&
            isPassPhysicallyAccessible(graph, inEdgeId, edgeId)) {
            result.insert(inEdgeId);
        }
    }

    return result;
}

EdgeIdSet getOutEdgeIds(const road_graph::Graph& graph,
                        road_graph::EdgeId edgeId)
{
    EdgeIdSet result;
    const auto outEdgeIds = graph.outEdgeIds(graph.edge(edgeId).target);

    for (auto outEdgeId: outEdgeIds) {
        if (!isReverseEdge(graph, edgeId, outEdgeId) &&
            isPassPhysicallyAccessible(graph, edgeId, outEdgeId)) {
            result.insert(outEdgeId);
        }
    }

    return result;
}

geolib3::Degrees degrees(const db::Ray& lhs, const geolib3::Point2& rhs)
{
    using namespace geolib3;
    auto counterClockwise =
        orientedAngleBetween(lhs.direction, Direction2(Segment2(lhs.pos, rhs)));
    auto clockwise = -counterClockwise;
    auto result = Degrees(std::round(toDegrees(clockwise).value()));
    return result == 180_deg ? -180_deg : result;
}

using DistanceToFeatureMap = std::map<double, db::Feature>;

DistanceToFeatureMap getEdgeFeatures(Direction direction,
                                     IDataAccess& data,
                                     const IGraph& graph,
                                     road_graph::EdgeId edgeId,
                                     const geolib3::Polyline2& line)
{
    DistanceToFeatureMap result;
    auto featureIds = graph.getFeatureIdsByEdgeId(edgeId);

    for (auto&& feature : data.getFeaturesByIds(featureIds)) {
        if (snapToGraph(graph, feature) == edgeId)
            try {
                const auto featurePos = feature.geodeticPos();
                const auto featureLine =
                    Direction::Backward == direction
                        ? geolib3::partitionToEnd(line, featurePos)
                        : geolib3::partitionFromStart(line, featurePos);

                const auto distance = geoLength(featureLine);
                result.insert({distance, std::move(feature)});
            } catch (const RuntimeError&) {
                // geolib3::partitionToEnd and geolib3::partitionFromStart
                // could throw and exception in case a point doesn't belong to
                // a polyline. It is expected behaviour in case the line is
                // just a part of an edge.
            }
    }
    return result;
}

enum CollectFeaturesStrategy { MoveOverCrossroads, StopOnCrossroads };

using IdToFeatureMap = std::map<db::TId, db::Feature>;

// Move along road graph in specified direction and collect closest edge
// feature from graph coverage per edge. Once a feature is met on an edge then
// no further traverse behind that feature is performed. Otherwise if strategy
// allows passing crossroads then all outgoing or incoming edges (depends on
// strategy argument) are followed and each of theese edges can add a covering
// feature to result.
void collectFeatures(CollectFeaturesStrategy strategy,
                     Direction direction,
                     IDataAccess& data,
                     const IGraph& graph,
                     road_graph::EdgeId edgeId,
                     const geolib3::Polyline2& line,
                     EdgeIdSet& visited,
                     const FeatureFilter& skip,
                     const std::optional<double>& minDistance,
                     const std::optional<double>& maxDistance,
                     IdToFeatureMap& result)
{
    // Graph coverage is only knows about so called base edges which have a
    // persistent edge ID
    edgeId = graph.graph().base(edgeId);

    if (auto turnaroundEdgeId = graph.graph().reverse(edgeId)) {
        // avoid turnaround edges
        visited.insert(graph.graph().base(*turnaroundEdgeId));
    }

    if (!visited.insert(edgeId).second ||
        (maxDistance && *maxDistance < 0.0)) {
        return;
    }

    auto distanceToFeatureMap =
        getEdgeFeatures(direction, data, graph, edgeId, line);

    for (const auto& [distance, feature] : distanceToFeatureMap) {
        if (maxDistance && maxDistance < distance) {
            break;
        }

        if (distance <= minDistance) {
            continue;
        }

        if (!skip(feature)) {
            result.insert({feature.id(), feature});
            return;
        }
    }

    const auto nextEdgeIds = Direction::Backward == direction
                                 ? getInEdgeIds(graph.graph(), edgeId)
                                 : getOutEdgeIds(graph.graph(), edgeId);

    if (nextEdgeIds.size() == 1 ||
        CollectFeaturesStrategy::MoveOverCrossroads == strategy) {
        // Note: in case of recursive call the min distance is set to zero so
        //       it won't skip features what are really close to an edge beginning.
        constexpr double MIN_DISTANCE_TO_NEXT_FEATURE_METERS = 0.0;
        const auto remainedDistance =
            maxDistance
                ? std::optional<double>{*maxDistance - geoLength(line)}
                : std::optional<double>{};

        for (auto nextEdgeId: nextEdgeIds) {
            collectFeatures(
                strategy,
                direction,
                data,
                graph,
                nextEdgeId,
                polyline(graph.graph(), nextEdgeId),
                visited,
                skip,
                MIN_DISTANCE_TO_NEXT_FEATURE_METERS,
                remainedDistance,
                result);
        }
    }
}

IdToFeatureMap
collectFeaturesInBothDirections(CollectFeaturesStrategy strategy,
                                IDataAccess& data,
                                const IGraph& graph,
                                road_graph::EdgeId baseEdgeId,
                                const geolib3::Point2& pos,
                                const std::optional<double>& minDistance,
                                const std::optional<double>& maxDistance,
                                const FeatureFilter& skip)
{
    const auto line = polyline(graph.graph(), baseEdgeId);

    auto result = IdToFeatureMap{};
    auto visited = EdgeIdSet{};

    const auto leftBaseLine = geolib3::partitionFromStart(line, pos);
    collectFeatures(
        strategy,
        Direction::Backward,
        data,
        graph,
        baseEdgeId,
        leftBaseLine,
        visited,
        skip,
        minDistance,
        maxDistance,
        result);

    // Remove the base edge id from the visited set so it can be visited while
    // traversing in forward direction too
    //
    // Note: only road graph base edges have persistent edge IDs the graph
    //       coverage is aware of
    visited.erase(graph.graph().base(baseEdgeId));
    const auto rightBaseLine = geolib3::partitionToEnd(line, pos);
    collectFeatures(
        strategy,
        Direction::Forward,
        data,
        graph,
        baseEdgeId,
        rightBaseLine,
        visited,
        skip,
        minDistance,
        maxDistance,
        result);

    return result;
}

db::Feature selectClosestFeature(
    const geolib3::Point2& pos, const IdToFeatureMap& idToFeatureMap)
{
    REQUIRE(!idToFeatureMap.empty(), "Illegal second argument");

    if (idToFeatureMap.size() == 1) {
        return idToFeatureMap.begin()->second;
    }

    if (geolib3::fastGeoDistance(
            pos, idToFeatureMap.begin()->second.geodeticPos()) <
        geolib3::fastGeoDistance(
            pos, idToFeatureMap.rbegin()->second.geodeticPos())) {
        return idToFeatureMap.begin()->second;
    } else {
        return idToFeatureMap.rbegin()->second;
    }
}

} // anonymous namespace

SnapLossFunc makeSnapLossFunc(
    Meters snapDistanceThreshold, geolib3::Radians snapAngleThreshold)
{
    return [snapDistanceThreshold, snapAngleThreshold](
               const db::Ray& lhs,
               const db::Ray& rhs) -> std::optional<double> {

        const auto distance = Meters{fastGeoDistance(lhs.pos, rhs.pos)};
        if (snapDistanceThreshold <= distance) {
            return std::nullopt;
        }

        const auto angle = angleBetween(lhs.direction, rhs.direction);
        if (snapAngleThreshold <= angle) {
            return std::nullopt;
        }

        return distance / snapDistanceThreshold + angle / snapAngleThreshold;
    };
}

db::Ray reverseRay(const db::Ray& ray) {
    auto reverseDirection = ray.direction.degrees() + 180_deg;
    if (reverseDirection >= 360_deg) {
        reverseDirection -= 360_deg;
    }
    return {
        .pos = ray.pos,
        .direction = geolib3::Direction2(reverseDirection)};
}

std::string formatToContentType(const std::string& format)
{
    static const std::unordered_map<std::string, std::string> FORMATS {
        {"protobuf", CONTENT_TYPE_PROTOBUF},
        {"text", CONTENT_TYPE_TEXT_PROTOBUF},
        {"png", CONTENT_TYPE_PNG},
        {"xml", CONTENT_TYPE_XML},
        {"json", CONTENT_TYPE_JSON},
        {"js", CONTENT_TYPE_JAVASCRIPT},
    };

    auto it = FORMATS.find(format);
    return (it == FORMATS.end()) ? "" : it->second;
}

void formatFeatureHotspotProperties(const std::string& url, json::ObjectBuilder obj, const db::Feature& dbFeature, const IdToPointMap& targetPoints)
{
    auto it = targetPoints.find(dbFeature.id());

    obj[FIELD_ID] = std::to_string(dbFeature.id());
    obj[FIELD_SOURCE_ID] = dbFeature.sourceId();
    if (it != targetPoints.end()) {
        auto segment = geolib3::Segment2(dbFeature.mercatorPos(),
                                         convertGeodeticToMercator(it->second));
        auto direction = geolib3::Direction2(segment);
        obj[FIELD_HEADING] << direction.heading().value();
    }
    else {
        obj[FIELD_HEADING] << dbFeature.heading().value();
    }
    obj[FIELD_CAMERA_DIRECTION] << toCameraDirection(dbFeature);
    obj[FIELD_TIMESTAMP] = chrono::formatIsoDateTime(dbFeature.timestamp());
    obj[FIELD_IMAGE_PREVIEW] = imagePreview(dbFeature, url);
    obj[FIELD_IMAGE_FULL] = imageFull(dbFeature, url);
    obj[FIELD_POS] << [&](json::ArrayBuilder posBuilder) {
        posBuilder << dbFeature.geodeticPos().x();
        posBuilder << dbFeature.geodeticPos().y();
    };

    if (it != targetPoints.end()) {
        obj[FIELD_TARGET_GEOMETRY] = geolib3::geojson(it->second);
    }
}

void toFeaturesResponseJson(db::Features features,
                            const IdToPointMap& targetPoints,
                            const std::string& url,
                            const ResponseOptions& responseOptions,
                            json::ObjectBuilder result)
{
    FeatureIdToUserInfo featureIdToUserInfo;
    if (responseOptions.withAuthors) {
        featureIdToUserInfo = getBlackboxUserInfos(features);
    }
    if (responseOptions.snapToGraph) {
        auto graph = Configuration::instance()->graphByType(
            *responseOptions.snapToGraph);
        for (auto& feature : features) {
            snapToGraph(*graph, feature);
        }
    }

    result["data"] = [&](json::ObjectBuilder bld) {
        bld["type"] = "FeatureCollection";
        bld["features"] = [&](json::ArrayBuilder bld) {
            for (const auto& feature : features) {
                bld << [&](json::ObjectBuilder bld) {
                    bld["type"] = "Feature";
                    bld["geometry"] = geometry(feature);
                    bld[FIELD_PROPERTIES] = [&](json::ObjectBuilder bld) {
                        formatFeatureHotspotProperties(url, bld, feature, targetPoints);
                    };
                    if (responseOptions.withAuthors) {
                        toFeatureAuthorJson(bld, feature.id(),
                                            featureIdToUserInfo);
                    }
                };
            }
        };

        // Only if all features are from the same author
        if (responseOptions.withAuthors) {
            auto users = uniqueUsers(features, featureIdToUserInfo);
            if (users.size() == 1) {
                toFeatureAuthorJson(bld, users.begin()->second);
            }
        }
    };
}

Direction toDirection(const std::string& direction)
{
    if (direction == VALUE_FORWARD) {
        return Direction::Forward;
    }
    else if (direction == VALUE_BACKWARD) {
        return Direction::Backward;
    }
    else {
        throw Exception() << "Invalid direction: " << direction;
    }
}

std::string toCameraDirection(const db::Feature& feature)
{
    return to_lower(ToString(feature.cameraDeviation()));
}


void ImagePreviewManipulator::json(json::ObjectBuilder builder) const
{
    std::ostringstream imageUrl;
    imageUrl << url << "feature/" << feature.id() << "/thumbnail";
    builder["url"] = imageUrl.str();
    const auto size = normThumbnailSizeOf(feature);
    builder["width"] = size.width;
    builder["height"] = size.height;
}

ImagePreviewManipulator imagePreview(const db::Feature& feature,
                                     const std::string& url)
{
    return {feature, url};
}


geolib3::Polyline2 toTrack(const std::string& requestBody)
{
    geolib3::PointsVector result;
    auto items = json::Value::fromString(requestBody)[FIELD_GEOMETRY];
    for (auto item : items) {
        result.push_back(item.construct<geolib3::Point2>(0, 1));
    }
    return geolib3::Polyline2{result};
}


std::string makePath(const std::string& dir, const std::string& file)
{
    return (fs::path(dir) / file).string();
}

bool userMayViewPhotos(
    std::optional<blackbox_client::Uid> uid, db::FeaturePrivacy privacy)
{
    return Configuration::instance()->permissionChecker().userMayViewPhotos(
               uid, privacy);
}

FeatureFilter FeatureFilter::fromRequest(const yacare::Request& request)
{
    static constexpr std::string_view DATASETS = "datasets";

    FeatureFilter result;
    if (request.input().has(DATASETS)) {
        std::vector<std::string> datasetStrings;
        boost::split(datasetStrings, request.input()[DATASETS],
                     [](char c) { return c == ','; });
        for (const auto& datasetString : datasetStrings) {
            result.datasets.push_back(enum_io::fromString<db::Dataset>(datasetString));
        }
    }
    result.cameraDeviation = parseOptionalCameraDeviation(request);
    result.actualizedAfter = parseOptionalDate(request, "actualized-after");
    result.actualizedBefore = parseOptionalDate(request, "actualized-before");
    return result;
}

bool FeatureFilter::operator()(const db::Feature& feature) const
{
    return !datasets.empty()
               && std::find(datasets.begin(), datasets.end(), feature.dataset())
                      == datasets.end()
           || cameraDeviation.has_value()
                  && cameraDeviation.value() != feature.cameraDeviation()
           || !userMayViewPhotos(uid, feature.privacy());
}

db::Features loadFeaturesWithMargins(IDataAccessPtr dataAccess,
                                     const geolib3::BoundingBox& mercatorBbox,
                                     double marginInMetricMeters,
                                     const FeatureFilter& filter)
{
    auto latlongBox = geolib3::convertMercatorToGeodetic(mercatorBbox);
    auto diagonal = geolib3::fastGeoDistance(latlongBox.lowerCorner(),
                                             latlongBox.upperCorner());
    auto side = diagonal * cos(M_PI_4);
    auto margin = marginInMetricMeters / side;
    latlongBox = geolib3::resizeByRatio(latlongBox, 1. + 2. * margin);
    auto features = dataAccess->getFeaturesByBbox(latlongBox);
    return boost::remove_erase_if(features, filter);
}

db::Features loadFeaturesWithMargins(IDataAccessPtr dataAccess,
                                     const geolib3::BoundingBox& mercatorBbox,
                                     const FeatureFilter& filter)
{
    return loadFeaturesWithMargins(dataAccess, mercatorBbox, 0, filter);
}


void
loadFeaturesAlongEdgeSegments(IDataAccessPtr dataAccess,
                              const IGraphPtr graph,
                              road_graph::LongEdgeId longEdgeId,
                              const std::vector<road_graph::SegmentIndex>& segments,
                              db::CameraDeviation cameraDeviation,
                              db::Features& features)
{
    auto edgeId = graph->persistentIndex().findShortId(longEdgeId);
    ASSERT(edgeId);
    auto edge = polyline(graph->graph(), *edgeId);
    std::vector<fb::CoveredSubPolyline> coveredSubpolylines =
        graph->getCoveredSubpolylinesForEdgeSegments(
            *edgeId, segments, cameraDeviation, db::FeaturePrivacy::Public);
    dataAccess->removeUnpublishedFeatures(coveredSubpolylines);

    for (const auto& coveredSubpolyline : coveredSubpolylines) {
        if (auto feature = dataAccess->tryGetFeatureById(coveredSubpolyline.featureId())) {
            geolib3::Polyline2 polyline = partition(
                edge, coveredSubpolyline.begin(), coveredSubpolyline.end());

            feature->setGeodeticPos(polyline.pointAt(0));
            if (polyline.pointsNumber() > 1) {
                feature->setHeading(
                    geolib3::Direction2(
                        geolib3::convertGeodeticToMercator(polyline.segmentAt(0))
                    ).heading()
                );
            }
            features.push_back(std::move(feature.value()));
        }
    }
}


db::Features loadFeaturesTourAlongLine(IDataAccessPtr dataAccess,
                                       const IGraphPtr graph,
                                       const geolib3::Polyline2& geoLine,
                                       db::CameraDeviation cameraDeviation)
{
    db::Features result;

    /// Minimal start/end segment part to accept it
    constexpr double MIN_SEGMENT_PART_TO_ACCEPT = 0.2;
    graphmatching::MatchedGeometry matchedGeometry =
        graphmatching::matchGeometry(geoLine,
                                     graph->graph(),
                                     graph->rtree(),
                                     graph->persistentIndex(),
                                     MIN_SEGMENT_PART_TO_ACCEPT);


    for (const graphmatching::MatchedGeometryPart& geometryPart : matchedGeometry.parts) {
        if (geometryPart.segments.empty()) {
            continue;
        }
        std::vector<road_graph::SegmentIndex> segments;
        auto segmentIt = geometryPart.segments.begin();
        road_graph::LongEdgeId longEdgeId = segmentIt->edgeId;
        segments.push_back(segmentIt->segmentIndex);
        ++segmentIt;

        for (; segmentIt != geometryPart.segments.end(); ++segmentIt) {
            if (segmentIt->edgeId != longEdgeId) {
                loadFeaturesAlongEdgeSegments(dataAccess, graph, longEdgeId, segments, cameraDeviation, result);
                segments.clear();
                segments.push_back(segmentIt->segmentIndex);
                longEdgeId = segmentIt->edgeId;
            } else {
                segments.push_back(segmentIt->segmentIndex);
            }
        }

        loadFeaturesAlongEdgeSegments(dataAccess, graph, longEdgeId, segments, cameraDeviation, result);

    }
    removeDuplicates(result);
    return result;
}

cv::Mat toThumbnail(const cv::Mat& image)
{
    const auto size = common::getThumbnailSize(image.cols, image.rows);

    cv::Mat thumbnail(static_cast<int>(size.height), static_cast<int>(size.width), CV_8UC3);
    cv::resize(image, thumbnail, thumbnail.size(), 0, 0);

    return thumbnail;
}

void drawImageBoxes(cv::Mat& image, const common::ImageBoxes& boxes)
{
    const cv::Scalar RED{0, 0, 255};

    for (const auto box: boxes) {
        cv::rectangle(image, box, /* color = */ RED, /* thicknes = */ 2);
    }
}

void toPathJson(db::TId baseFeatureId,
                db::Features features,
                const std::string& url,
                const ResponseOptions& responseOptions,
                json::ObjectBuilder builder)
{
    bool done = false;
    forEachPassage<DistanceCheck::Off>(features, [&](db::Features::iterator
                                                         begin,
                                                     db::Features::iterator
                                                         end) {
        if (end == std::find_if(begin, end,
                                [baseFeatureId](const db::Feature& feature) {
                                    return baseFeatureId == feature.id();
                                })) {
            return LoopControl::Continue;
        }
        toFeaturesResponseJson({begin, end}, {}, url, responseOptions, builder);
        done = true;
        return LoopControl::Break;
    });
    if (!done) {
        toFeaturesResponseJson({}, {}, url, responseOptions, builder);
    }
}

void toSimilarJson(db::TId baseFeatureId,
                   db::Features features,
                   const IdToPointMap& targetPoints,
                   const std::string& url,
                   const ResponseOptions& responseOptions,
                   json::ObjectBuilder builder)
{
    auto isBase = [baseFeatureId](const db::Feature& feature) {
        return baseFeatureId == feature.id();
    };

    auto baseIt = std::find_if(features.begin(), features.end(), isBase);
    ASSERT(baseIt != features.end());
    auto base = *baseIt;

    auto result = selectFromEachPassage(
        features,
        BySimilarity{.geodeticPos = base.geodeticPos(),
                     .direction = geolib3::Direction2{base.heading()},
                     .radiusMeters = SIMILARITY_RADIUS_METERS});
    ASSERT(!result.empty());
    result = mostSimilar(result, SIMILAR_LIMIT, base.cameraDeviation());

    if (std::none_of(result.begin(), result.end(), isBase)) {
        result.back() = base;
    }

    toFeaturesResponseJson(std::move(result),
                           targetPoints,
                           url,
                           responseOptions,
                           builder);
}

void toNearJson(const geolib3::Point2& geodeticPos,
                const std::optional<geolib3::Direction2>& dir,
                double radiusMeters,
                db::Features features,
                const IdToPointMap& targetPoints,
                const std::string& url,
                const ResponseOptions& responseOptions,
                json::ObjectBuilder builder)
{
    auto result =
        selectFromEachPassage(features,
                              BySimilarity{.geodeticPos = geodeticPos,
                                           .direction = dir,
                                           .radiusMeters = radiusMeters});
    toFeaturesResponseJson(mostRecent(result, SIMILAR_LIMIT),
                           targetPoints,
                           url,
                           responseOptions,
                           builder);
}

void orderByPassages(FeaturesRange features)
{
    std::sort(features.begin(), features.end(),
              [](const db::Feature& lhs, const db::Feature& rhs) {
                  return std::make_tuple(lhs.dataset(), lhs.sourceId(),
                                         lhs.timestamp(), lhs.id())
                         < std::make_tuple(rhs.dataset(), rhs.sourceId(),
                                           rhs.timestamp(), rhs.id());
              });
}

std::optional<road_graph::EdgeId> snapToGraph(
    const IGraph& graph, db::Ray& rayToChange, SnapLossFunc lossFunc)
{
    std::optional<road_graph::EdgeId> result;
    auto origin = rayToChange;
    auto bbox = db::expandBbox(origin.pos.boundingBox(), export_gen::COVERAGE_METERS_SNAP_THRESHOLD);
    auto edgeIds = graph.rtree().baseEdgesInWindow(bbox);
    auto minLoss = std::optional<double>{};
    for (auto edgeId : edgeIds) {
        auto edge = polyline(graph.graph(), edgeId);
        for (const auto& segment : edge.segments()) {
            auto closest = db::Ray{closestPoint(segment, origin.pos),
                                   fastGeoDirection(segment)};
            auto curLoss = lossFunc(origin, closest);

            if (curLoss && (!minLoss || curLoss < minLoss)) {
                rayToChange = closest;
                minLoss = curLoss;
                result = edgeId;
            }
        }
    }
    return result;
}

std::optional<road_graph::EdgeId> snapToGraph(
    const IGraph& graph, db::Feature& featureToChange, SnapLossFunc lossFunc)
{
    db::Ray rayToChange{
        featureToChange.geodeticPos(), direction(featureToChange)};
    auto result = snapToGraph(graph, rayToChange, lossFunc);
    featureToChange.setGeodeticPos(rayToChange.pos)
        .setHeading(rayToChange.direction.heading());

    return result;
}

db::Features leaveSameFeaturesByThreshold(const db::Features& features, double thresholdMeters)
{
    if (features.size() < 2)
        return features;

    db::Features result;
    Gap<DistanceCheck::Off> timeGapChecker;

    auto prevIt = features.begin();
    while (prevIt != features.end()) {
        result.push_back(*prevIt);
        bool found = false;
        for (auto currIt = std::next(prevIt);
             currIt != features.end()
               && geolib3::fastGeoDistance(prevIt->geodeticPos(), currIt->geodeticPos())
                  < thresholdMeters;
             ++currIt) {
            if (!timeGapChecker(*prevIt, *currIt)) {
                found = true;
                prevIt = currIt;
                break;
            }
        }
        if (!found) {
            ++prevIt;
        }
    }
    return result;
}

std::optional<db::Feature> findClosestEdgeCoveringFeature(
    IDataAccess& data,
    const IGraph& graph,
    road_graph::EdgeId baseEdgeId,
    const geolib3::Point2& pos,
    const std::optional<double>& minDistance,
    const std::optional<double>& maxDistance,
    const FeatureFilter& skip)
{
    const auto features = collectFeaturesInBothDirections(
        CollectFeaturesStrategy::StopOnCrossroads,
        data,
        graph,
        baseEdgeId,
        pos,
        minDistance,
        maxDistance,
        skip);

    if (features.empty()) {
        return std::nullopt;
    }

    return selectClosestFeature(pos, features);
}

db::Features evalCoveredRoadGraphDirections(db::Ray baseRay,
                                            IDataAccess& data,
                                            const IGraph& graph,
                                            const FeatureFilter& skip)
{
    constexpr double MIN_DISTANCE_TO_FEATURE_METERS = 1.0;

    auto baseEdgeId = snapToGraph(graph, baseRay);
    if (!baseEdgeId) {
        return {};
    }

    auto result = collectFeaturesInBothDirections(
        CollectFeaturesStrategy::MoveOverCrossroads,
        data,
        graph,
        *baseEdgeId,
        baseRay.pos,
        MIN_DISTANCE_TO_FEATURE_METERS,
        DIRECT_SPATIAL_CONNECTION_DISTANCE_METERS,
        skip);

    // Look for a turnaround photo
    auto reverseBaseRay = reverseRay(baseRay);
    if (auto reverseEdgeId = snapToGraph(graph, reverseBaseRay)) {
        auto turnaround = findClosestEdgeCoveringFeature(
            data,
            graph,
            *reverseEdgeId,
            reverseBaseRay.pos,
            std::nullopt /* minDistance */,
            TURNAROUND_SPATIAL_CONNECTION_DISTANCE_METERS,
            skip);

        if (turnaround) {
            result.insert({turnaround->id(), *turnaround});
        }
    }

    auto features = result | boost::adaptors::map_values;
    return {features.begin(), features.end()};
}

void toDirectionsResponseJson(const db::Ray& baseRay,
                              const db::Features& features,
                              const std::string& url,
                              bool withAuthors,
                              json::ObjectBuilder result)
{
    FeatureIdToUserInfo featureIdToUserInfo;
    if (withAuthors) {
        featureIdToUserInfo = getBlackboxUserInfos(features);
    }

    result["directions"] = [&](json::ObjectBuilder bld) {
        bld[FIELD_TYPE] = "FeatureCollection";
        bld["features"] = [&](json::ArrayBuilder bld) {
            for (const auto& feature : features) {
                bld << [&](json::ObjectBuilder bld) {
                    bld[FIELD_TYPE] = "Feature";
                    bld[FIELD_ANGLE] =
                        degrees(baseRay, feature.geodeticPos()).value();
                    bld[FIELD_DISTANCE] = std::round(
                        fastGeoDistance(baseRay.pos, feature.geodeticPos()));
                    bld[FIELD_GEOMETRY] = geometry(feature);
                    bld[FIELD_PROPERTIES] = [&](json::ObjectBuilder bld) {
                        formatFeatureHotspotProperties(url, bld, feature, {});
                    };
                    if (withAuthors) {
                        toFeatureAuthorJson(
                            bld, feature.id(), featureIdToUserInfo);
                    }
                };
            }
        };
    };
}

geolib3::PointsVector makeGridPoints(const geolib3::BoundingBox& bbox,
                                     size_t rows,
                                     size_t cols)
{
    REQUIRE(rows > 1 && cols > 1, "invalid grid: " << rows << ", " << cols);

    using kahan_accumulator = boost::accumulators::accumulator_set<
        double,
        boost::accumulators::stats<boost::accumulators::tag::sum_kahan>>;

    geolib3::PointsVector result;
    result.reserve(rows * cols);
    auto dx = bbox.width() / (cols - 1);
    auto dy = bbox.height() / (rows - 1);
    kahan_accumulator accY(bbox.minY());
    for (size_t row = 0; row < rows; ++row, accY(dy)) {
        kahan_accumulator accX(bbox.minX());
        for (size_t col = 0; col < cols; ++col, accX(dx)) {
            result.emplace_back(boost::accumulators::sum_kahan(accX),
                                boost::accumulators::sum_kahan(accY));
        }
    }
    return result;
}

void logUgcPhotoViewEvent(
    const std::optional<uint64_t>& maybeUserId,
    const db::TIds& featureIds,
    const yacare::Request& request)
{
    auto ugcEventLogger = Configuration::instance()->ugcEventLogger();
    if (!ugcEventLogger) {
        return;
    }

    ugc_event_logger::UserInfo userInfo{.ip = std::string(request.getClientIpAddress())};
    auto maybePort = request.getClientPort();
    if (maybePort.has_value()) {
        userInfo.port = maybePort.value();
    }
    if (maybeUserId.has_value()) {
        userInfo.uid = std::to_string(maybeUserId.value());
    }

    for (auto featureId : featureIds) {
        ugcEventLogger->logEvent(
            userInfo,
            ugc_event_logger::Action::View,
            ugc_event_logger::Photo{.id = std::to_string(featureId)});
    }
}

Meters fastGeoDistanceAlong(const geolib3::Polyline2& polyline,
                            common::geometry::PolylinePosition lhs,
                            common::geometry::PolylinePosition rhs)
{
    ASSERT(
        lhs.segmentIdx() < polyline.segmentsNumber() &&
        rhs.segmentIdx() < polyline.segmentsNumber());

    // Warn: the approach below requires absolute comparison of relative positions.
    if (std::make_tuple(rhs.segmentIdx(), rhs.segmentRelPosition()) <
        std::make_tuple(lhs.segmentIdx(), lhs.segmentRelPosition())) {
        std::swap(lhs, rhs);
    }

    // Collect all points between lhs and rhs including these two.
    geolib3::PointsVector points{
        polyline.segmentAt(lhs.segmentIdx())
            .pointByPosition(lhs.segmentRelPosition())};

    // There is segmentsNumber() + 1 points in a polyline and their indices
    // start from 0. So, the same Nth point is the ending of Nth segment and
    // the beginning of Nth + 1 segment.
    for (std::size_t segmentIdx = lhs.segmentIdx();
         segmentIdx < rhs.segmentIdx();
         ++segmentIdx) {
        points.push_back(polyline.pointAt(segmentIdx + 1));
    }

    points.push_back(polyline.segmentAt(rhs.segmentIdx())
                         .pointByPosition(rhs.segmentRelPosition()));

    Meters distance{0.0};
    for (std::size_t idx = 1; idx < points.size(); ++idx) {
        distance =
            distance + geolib3::fastGeoDistance(points[idx - 1], points[idx]);
    }
    return distance;
}

} // namespace maps::mrc::browser
