#include "lost_mapper.h"
#include "common.h"
#include "column_names.h"

#include <maps/libs/geolib/include/distance.h>
#include <maps/libs/geolib/include/serialization.h>
#include <yandex/maps/jams/static_graph2/persistent_segment_id.h>

namespace mjs = maps::jams::static_graph2;

namespace maps::wiki::route_lost_feedback {

namespace {

const int MAX_POINTS_DESCRIPTION = 30;
double MAX_LENGTH_DESCRIPTION = 1000.; // in meters


bool rowShouldBeSkipped(const NYT::TNode& row)
{
    const auto& lostType = row[column_names::ROUTE_LOST_TYPE];
    const auto& type = row[column_names::TYPE];
    const auto& position = row[column_names::ROUTE_LOST_POSITION];
    const auto& routeSegments = row[column_names::ROUTE_PERSISTENT_SEGMENTS];
    const auto& trackSegments = row[column_names::TRACK_PERSISTENT_SEGMENTS];

    return lostType.IsNull() ||
           lostType.AsString() != "true" ||
           type.AsString() != "route" ||
           position.IsNull() ||
           routeSegments.IsNull() ||
           trackSegments.IsNull();

    return false;
}

TString
buildLostKey(
    const mjs::PersistentSegmentId& beforeLostSegment,
    const mjs::PersistentSegmentId& afterLostRouteSegment,
    const mjs::PersistentSegmentId& afterLostTrackSegment)
{
    std::stringstream keyStream;

    keyStream <<
        beforeLostSegment << "," <<
        afterLostRouteSegment << "," <<
        afterLostTrackSegment;

    return TString(keyStream.str());
}

NYT::TNode cutPointsListForward(
    const NYT::TNode::TListType& pointList,
    uint64_t startIndex)
{
    auto resNode = NYT::TNode::CreateList();
    auto& resList = resNode.AsList();

    const auto startPoint = asPoint(pointList.at(startIndex));

    for (uint64_t ind = startIndex; ind < pointList.size(); ind++) {
        if (resList.size() >= MAX_POINTS_DESCRIPTION) {
            break;
        }

        double distanceToStart = geolib3::geoDistance(
            asPoint(pointList[ind]),
            startPoint
        );

        if (distanceToStart > MAX_LENGTH_DESCRIPTION) {
            break;
        }

        resList.push_back(pointList[ind]);
    }

    return resNode;
}

NYT::TNode cutPointsListBackward(
    const NYT::TNode::TListType& pointList,
    uint64_t startIndex)
{
    auto pointListReversed = pointList;
    std::reverse(pointListReversed.begin(), pointListReversed.end());

    return cutPointsListForward(
        pointListReversed,
        pointList.size() - startIndex - 1
    );
}

} // unnamed namespace

LostMapper::LostMapper(const geolib3::Polygon2& polygonGeo) :
    polygonGeoBytes_(geolib3::WKB::toBytes<geolib3::Polygon2>(polygonGeo))
{
}

void LostMapper::Do(TReader* reader, TWriter* writer)
{
    const auto polygon = geolib3::WKB::read<geolib3::Polygon2>(polygonGeoBytes_);

    for (; reader->IsValid(); reader->Next()) {

        const NYT::TNode& row = reader->GetRow();

        if (rowShouldBeSkipped(row)) {
            continue;
        }
        // 'beforeLostRouteIndex' and 'trackLostIndex' are (zero-based) indices of
        // the last COMMON segment in 'route' and 'track' respectively
        //
        uint64_t beforeLostRouteIndex =
            row[column_names::ROUTE_LOST_POSITION][column_names::ROUTE].AsInt64();
        uint64_t beforeLostTrackIndex =
            row[column_names::ROUTE_LOST_POSITION][column_names::TRACK].AsInt64();

        uint64_t afterLostRouteIndex = beforeLostRouteIndex + 1;
        uint64_t afterLostTrackIndex = beforeLostTrackIndex + 1;
        uint64_t afterAfterLostTrackIndex =
            std::min(
                beforeLostTrackIndex + 2,
                row[column_names::TRACK_PERSISTENT_SEGMENTS].AsList().size() - 1
            );

        uint64_t lostRoutePointIndex = beforeLostRouteIndex + 1;
        uint64_t lostTrackPointIndex = beforeLostTrackIndex + 1;

        // Check if considered route-lost is inside area of interest.
        // It not - skip it
        //
        auto pointOfLoss = asPoint(
            row[column_names::ROUTE_GEOMETRY].AsList()[lostRoutePointIndex]);

        if (!pointIsInsidePolygon(pointOfLoss, polygon)) {
            continue;
        }

        const auto& routeSegments =
            row[column_names::ROUTE_PERSISTENT_SEGMENTS].AsList();
        const auto& trackSegments =
            row[column_names::TRACK_PERSISTENT_SEGMENTS].AsList();

        // Case when route is ended, but user continued to go, and his track
        // goes further
        //
        if (afterLostRouteIndex == routeSegments.size()) {
            continue;
        }

        auto beforeLostSegment =
            asPersistentSegment(routeSegments.at(beforeLostRouteIndex));

        auto afterLostRouteSegment =
            asPersistentSegment(routeSegments.at(afterLostRouteIndex));

        auto afterLostTrackSegment =
            asPersistentSegment(trackSegments.at(afterLostTrackIndex));

        auto afterAfterLostTrackSegment =
            asPersistentSegment(trackSegments.at(afterAfterLostTrackIndex));


        NYT::TNode res;

        res(
            column_names::KEY,
            buildLostKey(
                beforeLostSegment,
                afterLostRouteSegment,
                afterLostTrackSegment
            )
        );

        res(column_names::PERSISTENT_ID, beforeLostSegment.edgeId().value());
        res(column_names::SEGMENT_INDEX, beforeLostSegment.segmentIndex());

        // Point (common for both route and tracks), after which
        // route and track became different
        //
        res(
            column_names::LOST_POINT,
            row[column_names::ROUTE_GEOMETRY].AsList().at(lostRoutePointIndex)
        );

        // Route and track nearest segments in neighbourhood of lost point
        //
        res(
            column_names::BEFORE_LOST_SEGMENT,
            toTNode(beforeLostSegment)
        );
        res(
            column_names::AFTER_LOST_ROUTE_SEGMENT,
            toTNode(afterLostRouteSegment)
        );
        res(
            column_names::AFTER_LOST_TRACK_SEGMENT,
            toTNode(afterLostTrackSegment)
        );
        res(
            column_names::AFTER_AFTER_LOST_TRACK_SEGMENT,
            toTNode(afterAfterLostTrackSegment)
        );

        // Route and track point sequences in neighbourhood of lost point
        //
        res(
            column_names::POINTS_BEFORE_LOST,
            cutPointsListBackward(
                row[column_names::ROUTE_GEOMETRY].AsList(),
                lostRoutePointIndex
            )
        );

        res(
            column_names::POINTS_TRACK_AFTER_LOST,
            cutPointsListForward(
                row[column_names::TRACK_GEOMETRY].AsList(),
                lostTrackPointIndex
            )
        );

        res(
            column_names::POINTS_ROUTE_AFTER_LOST,
            cutPointsListForward(
                row[column_names::ROUTE_GEOMETRY].AsList(),
                lostRoutePointIndex
            )
        );

        // Additional information about route
        //
        ASSERT(row[column_names::ROUTE_GEOMETRY].AsList().size() >= 2);

        res(
            column_names::ROUTE_ID,
            row[column_names::ROUTE_ID].AsString()
        );

        res(
            column_names::ROUTE_START,
            row[column_names::ROUTE_GEOMETRY].AsList().front()
        );

        res(
            column_names::ROUTE_END,
            row[column_names::ROUTE_GEOMETRY].AsList().back()
        );

        writer->AddRow(res);
    }
}

REGISTER_MAPPER(LostMapper);

} // namespace maps::wiki::route_lost_feedback
