#include "utility.h"

#include <maps/libs/geolib/include/distance.h>

namespace maps::mrc::ugc_back {

namespace {

class PolylinePositionConverter {
public:
    explicit PolylinePositionConverter(const geolib3::Polyline2& polyline)
    {
        ASSERT(polyline.segmentsNumber() > 0);
        partialSum_.reserve(polyline.segmentsNumber());
        auto len = 0.;
        for (const auto& seg : polyline.segments()) {
            len += geolib3::fastGeoDistance(seg.start(), seg.end());
            partialSum_.push_back(len);
        }
    }

    double operator()(const PolylinePosition& pos) const
    {
        if (partialSum_.empty()) {
            return 0.;
        }
        auto prevLen = previousLength(pos.segmentIndex);
        auto segLen = partialSum_.at(pos.segmentIndex) - prevLen;
        return ratio(prevLen + segLen * pos.segmentPosition, totalLength());
    }

    PolylinePosition operator()(double pos) const
    {
        if (partialSum_.empty()) {
            return {};
        }
        auto len = pos * totalLength();
        auto it = std::lower_bound(partialSum_.begin(), partialSum_.end(), len);
        auto segmentIndex = (size_t)std::distance(partialSum_.begin(), it);
        auto prevLen = previousLength(segmentIndex);
        auto segLen = partialSum_.at(segmentIndex) - prevLen;
        auto segmentPosition = ratio(len - prevLen, segLen);
        return {segmentIndex, segmentPosition};
    }

private:
    std::vector<double> partialSum_;

    double previousLength(size_t i) const
    {
        return i ? partialSum_.at(i - 1) : 0;
    }

    double totalLength() const { return partialSum_.back(); }
};

bool isPosition(double pos)
{
    return pos >= 0. && pos <= 1.;
}

bool lessOrEqual(const PolylinePosition& lhs, const PolylinePosition& rhs)
{
    return std::tie(lhs.segmentIndex, lhs.segmentPosition) <=
           std::tie(rhs.segmentIndex, rhs.segmentPosition);
}

bool isValid(const Subpolyline& sub)
{
    return isPosition(sub.begin.segmentPosition) &&
           isPosition(sub.end.segmentPosition) &&
           lessOrEqual(sub.begin, sub.end);
}

}  // namespace

void PolylinePosition::translateTo(const Subpolyline& sub)
{
    ASSERT(isPosition(segmentPosition));
    ASSERT(isValid(sub));
    if (sub.begin.segmentPosition == 1.) {
        ++segmentIndex;
    }
    bool isStartSeg = segmentIndex == 0;
    auto marginBefore = isStartSeg ? sub.begin.segmentPosition : 0.;
    bool isEndSeg =
        (sub.begin.segmentIndex + segmentIndex) == sub.end.segmentIndex;
    auto marginAfter = isEndSeg ? (1. - sub.end.segmentPosition) : 0.;
    segmentPosition *= (1. - marginBefore - marginAfter);
    segmentPosition += marginBefore;
    segmentPosition = std::clamp(segmentPosition, 0., 1.);
    segmentIndex += sub.begin.segmentIndex;

    // to avoid out of segments
    if (segmentIndex > 0u && segmentPosition == 0.) {
        --segmentIndex;
        segmentPosition = 1.;
    }
}

auto encode(const PolylinePosition& pos)
    -> yandex::maps::proto::common2::geometry::PolylinePosition
{
    auto result = yandex::maps::proto::common2::geometry::PolylinePosition{};
    result.set_segment_index(pos.segmentIndex);
    result.set_segment_position(pos.segmentPosition);
    return result;
}

auto decode(const yandex::maps::proto::common2::geometry::PolylinePosition& pos)
    -> PolylinePosition
{
    return PolylinePosition{.segmentIndex = pos.segment_index(),
                            .segmentPosition = pos.segment_position()};
}

auto encode(const Subpolyline& sub)
    -> yandex::maps::proto::common2::geometry::Subpolyline
{
    auto result = yandex::maps::proto::common2::geometry::Subpolyline{};
    *result.mutable_begin() = encode(sub.begin);
    *result.mutable_end() = encode(sub.end);
    return result;
}

auto decode(const yandex::maps::proto::common2::geometry::Subpolyline& sub)
    -> Subpolyline
{
    return Subpolyline{.begin = decode(sub.begin()), .end = decode(sub.end())};
}

auto encode(const PhotoStream& photos)
    -> yandex::maps::proto::mrcphoto::ugc::ride::PhotoStream
{
    auto result = yandex::maps::proto::mrcphoto::ugc::ride::PhotoStream{};
    for (const auto& src : photos.items) {
        auto dst = result.add_items();
        dst->set_photo_id(TString{std::to_string(src.photo.get().id())});
        *dst->mutable_position() = encode(src.position);
    }
    return result;
}

auto makeTrack(const db::Features& photos)
    -> std::pair<geolib3::Polyline2, PhotoStream>
{
    auto photoStream = PhotoStream{};
    auto points = geolib3::PointsVector{};
    for (const auto& photo : photos) {
        ASSERT(hasPos(photo));
        photoStream.items.push_back(PhotoStream::Item{
            .position =
                PolylinePosition{
                    .segmentIndex = points.empty() ? 0 : points.size() - 1,
                    .segmentPosition = points.empty() ? 0. : 1.},
            .photo = std::cref(photo)});
        points.push_back(getGeodeticPos(photo));
    }
    if (points.size() == 1) {
        points.push_back(points.front());
    }
    return {geolib3::Polyline2(std::move(points)), std::move(photoStream)};
}

Subpolyline makeSubpolyline(const geolib3::Polyline2& polyline,
                            double startPos,
                            double endPos)
{
    ASSERT(isPosition(startPos));
    ASSERT(isPosition(endPos));
    ASSERT(startPos <= endPos);
    auto conv = PolylinePositionConverter{polyline};
    return {conv(startPos), conv(endPos)};
}

geolib3::Polyline2 makePartition(const geolib3::Polyline2& polyline,
                                 const Subpolyline& subpolyline)
{
    ASSERT(isValid(subpolyline));
    auto result = geolib3::Polyline2{};
    result.reserve(subpolyline.end.segmentIndex -
                   subpolyline.begin.segmentIndex + 2);
    bool sameSegment =
        subpolyline.begin.segmentIndex == subpolyline.end.segmentIndex;
    if (sameSegment || subpolyline.begin.segmentPosition < 1.) {
        result.add(subpolyline.begin.pointOf(polyline));
    }
    auto i = subpolyline.begin.segmentIndex + 1;
    while (i <= subpolyline.end.segmentIndex) {
        result.add(polyline.pointAt(i++));
    }
    if (sameSegment || subpolyline.end.segmentPosition > 0.) {
        result.add(subpolyline.end.pointOf(polyline));
    }
    return result;
}

void transformPhotoStream(const geolib3::Polyline2& src,
                          const geolib3::Polyline2& dst,
                          PhotoStream& photoStream)
{
    auto srcConv = PolylinePositionConverter{src};
    auto dstConv = PolylinePositionConverter{dst};
    for (auto& item : photoStream.items) {
        item.position = dstConv(srcConv(item.position));
    }
}

}  // namespace maps::mrc::ugc_back
