#include <maps/wikimap/mapspro/services/mrc/libs/db/include/house_number.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/sign.h>
#include <maps/wikimap/mapspro/services/mrc/libs/db/include/traffic_light.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/panorama_cropper.h>

#include <maps/libs/http/include/client.h>
#include <maps/libs/http/include/response.h>

#include <algorithm>

namespace maps::mrc::common {

namespace {

constexpr int BBOX_SURROUNDING_PIXELS = 30.0f;

cv::Point2f operator-(const cv::Point2f& pt, const cv::Size2f& size)
{
    return {pt.x - size.width, pt.y - size.height};
}

// Translate a given heading to panorama image coordinates.
// Returns X - coordinate corresponding to the given heading and Y -
// coordinate corresponding to the horizon line.
cv::Point2f headingToPanoramaCoords(
    const PanoCutParams& cutParams,
    geolib3::Heading heading,
    geolib3::Heading horizontalAngle,
    geolib3::Degrees verticalAngle)
{
    heading = geolib3::normalize(heading - horizontalAngle);

    const float x = convertToPixels(cutParams.totalWidth, heading);
    const float y = cutParams.totalHeight / 2.0f +
                    convertToPixels(cutParams.totalWidth, verticalAngle);
    return {x, y};
}

// Compute a crop-out rectangle size out of an image horizontal FOV.
cv::Size2f getRectSize(
    const PanoCutParams& cutParams, geolib3::Degrees imageHorizontalFOV)
{
    const float width =
        cutParams.totalWidth / 360.0f * imageHorizontalFOV.value();
    const float height = width * 9.0f / 16.0f; // 16/9 ratio image
    return {width, height};
}

// Compute a rectangle a given heading is centered in. Notice what the
// heading specifies a horizontal coordinate only so vertically the
// rectangle is centered on the horizon line.
cv::Rect2f getRect(
    const PanoCutParams& cutParams,
    geolib3::Heading heading,
    geolib3::Heading horizontalAngle,
    geolib3::Degrees verticalAngle,
    geolib3::Degrees imageHorizontalFOV)
{
    const cv::Size2f imageSize = getRectSize(cutParams, imageHorizontalFOV);
    const cv::Point2f centerPixel = headingToPanoramaCoords(
        cutParams, heading, horizontalAngle, verticalAngle);

    cv::Point2f topLeftPixel = centerPixel - (imageSize / 2.0f);
    // Fix left crop out edge coordinate so that X coordinate is always positive
    if (topLeftPixel.x < 0) {
        topLeftPixel.x += cutParams.totalWidth;
    }

    return {topLeftPixel, imageSize};
}

// Return three anlges - view heading, vertical angle, and horizontal FOV for
// a crop-out on which an object is visible good enougth.
template<typename Object>
std::tuple<geolib3::Heading, geolib3::Degrees, geolib3::Degrees>
chooseVisibleCropOutParameters(
    const PanoCutParams& cutParams,
    const Object& object,
    geolib3::Heading horizontalAngle,
    geolib3::Degrees verticalAngle,
    geolib3::Degrees imageHorizontalFOV)
{
    const geolib3::Heading heading = geolib3::normalize(
        convertPixelsTo<geolib3::Heading>(
            cutParams.totalWidth, (object.minX() + object.maxX()) / 2.0f) +
        horizontalAngle);

    const float objectArea =
        (object.maxX() - object.minX()) * (object.maxY() - object.minY());
    const cv::Size2f imageSize = getRectSize(cutParams, imageHorizontalFOV);
    const float actualBBoxRatio = objectArea / imageSize.area();

    // Try to make best view for a given object.
    // 1. Its bbox area ratio to an image area it is in should be greater or
    //    equal to MIN_VISIBLE_BBOX_AREA_RATIO.
    // 2. the image itself should contain as much surrounding as possible, so
    //    don't let it be smaller then FHD.
    const geolib3::Degrees objectHorizontalFOV =
        actualBBoxRatio < MIN_VISIBLE_BBOX_AREA_RATIO
            ? imageHorizontalFOV / MIN_VISIBLE_BBOX_AREA_RATIO * actualBBoxRatio
            : imageHorizontalFOV;

    const geolib3::Degrees fhdHorizontalFOV =
        convertPixelsTo<geolib3::Degrees>(cutParams.totalWidth, 1920.f);

    const geolib3::Degrees visibleHorizontalFOV = std::min(
        std::max(objectHorizontalFOV, fhdHorizontalFOV), imageHorizontalFOV);

    cv::Rect2f visibleRect = getRect(
        cutParams,
        heading,
        horizontalAngle,
        verticalAngle,
        visibleHorizontalFOV);

    // Move crop-out up or down to prevent object going away from the
    // crop-out view.
    if ((int)object.minY() < visibleRect.tl().y + BBOX_SURROUNDING_PIXELS) {
        visibleRect.y =
            std::max(0, (int)object.minY() - BBOX_SURROUNDING_PIXELS);
    } else if (
        visibleRect.br().y - BBOX_SURROUNDING_PIXELS <= (int)object.maxY()) {
        visibleRect.y = std::min(
            (int)cutParams.totalHeight - 1 - visibleRect.height,
            (int)object.maxY() - visibleRect.height +
                BBOX_SURROUNDING_PIXELS);
    }

    const float visibleVerticalRectCenter =
        visibleRect.y + visibleRect.height / 2.f;

    const auto visibleVerticalAngle =
        convertPixelsTo<geolib3::Degrees>(
            cutParams.totalWidth,
            visibleVerticalRectCenter - cutParams.totalHeight / 2.f);

    return {heading, visibleVerticalAngle, visibleHorizontalFOV};
}

template<typename Object>
inline std::tuple<geolib3::Heading, PanoCropOut> getObjectCropOutWithHeadingImpl(
    const db::Panorama& panorama, const Object& object)
{
    const auto cutParams = makeCutParams(panorama);

    geolib3::Heading heading {0};
    geolib3::Heading horizontalAngle {panorama.horizontalAngleDeg()};
    geolib3::Degrees verticalAngle {panorama.verticalAngleDeg()};
    geolib3::Degrees imageHorizontalFOV {CROP_OUT_DEFAULT_HORIZONTAL_FOV};

    // Choose an optimal panorama crop-out for an object
    std::tie(heading, verticalAngle, imageHorizontalFOV) =
        chooseVisibleCropOutParameters(
            cutParams,
            object,
            horizontalAngle,
            verticalAngle,
            imageHorizontalFOV);

    // A cropper instance is created only to compute a crop out parameter.
    PanoramaCropper cropper{
        []() -> std::unique_ptr<PanoTilesDownloader> { return nullptr; },
        cutParams,
        horizontalAngle,
        verticalAngle,
        imageHorizontalFOV};

    return {heading, cropper.getCropOut(heading)};
}

} // namespace

PanoramaCropper::PanoramaCropper(
    PanoTilesDownloaderFactory panoDownloaderFactory,
    const PanoCutParams& cutParams,
    geolib3::Heading horizontalAngle,
    geolib3::Degrees verticalAngle,
    geolib3::Degrees cropOutHorizontalFOV)
    : panoDownloaderFactory_{panoDownloaderFactory}
    , cutParams_{cutParams}
    , horizontalAngle_{horizontalAngle}
    , verticalAngle_{verticalAngle}
    , cropOutHorizontalFOV_{cropOutHorizontalFOV}
{}

PanoCropOut PanoramaCropper::getCropOut(geolib3::Heading heading) const
{
    cv::Rect2f rect = getRect(cutParams_, heading, horizontalAngle_,
                              verticalAngle_, cropOutHorizontalFOV_);
    rect &= {rect.x, 0.0f, rect.width, (float)cutParams_.totalHeight};
    // Fix the crop-out aspect ratio.
    const float widthBefore = rect.width;
    rect.width = rect.height / 9.0f * 16.0f;
    // And adjust the rectangle center.
    rect.x += (widthBefore - rect.width) / 2.0f;

    return {round<std::uint32_t>(rect.x),
            round<std::uint32_t>(rect.y),
            round<std::uint32_t>(rect.width),
            round<std::uint32_t>(rect.height),
            1920.f / rect.width < 1.0f ? 1920.f / rect.width : 1.0f};
}

cv::Point PanoramaCropper::getCentralHorizonPoint(
    geolib3::Heading heading) const
{
    cv::Rect2f rect = getRect(cutParams_, heading, horizontalAngle_,
                              verticalAngle_, cropOutHorizontalFOV_);

    const float x = rect.width / 2.0f;
    float y = rect.height / 2.0f;

    if (rect.y < 0) {
        y += rect.y;
    }

    return {round<int>(x), round<int>(y)};
}

cv::Mat PanoramaCropper::getImage(
    geolib3::Heading cropOutCenter)
{
    const auto cropOut = getCropOut(cropOutCenter);

    const auto tiles =
        panoDownloaderFactory_()->download(cutParams_, cropOut);

    return concatAndCropTiles(cutParams_, cropOut, tiles);
}

std::vector<cv::Mat> PanoramaCropper::getImagesRange(
    geolib3::Heading startingCropOutCenter,
    geolib3::Heading cropOutStride)
{
    std::vector<cv::Mat> range;
    range.reserve(round<std::size_t>(360.0 / cropOutStride.value()));

    for (geolib3::Heading heading = startingCropOutCenter;
         heading < startingCropOutCenter + geolib3::Heading{360.0};
         heading += cropOutStride) {
        range.push_back(getImage(heading));
    }

    return range;
}

template<>
std::tuple<geolib3::Heading, PanoCropOut>
getObjectCropOutWithHeading<db::HouseNumberPanorama>(
    const db::Panorama& panorama, const db::HouseNumberPanorama& object)
{
    return getObjectCropOutWithHeadingImpl(panorama, object);
}

template<>
std::tuple<geolib3::Heading, PanoCropOut>
getObjectCropOutWithHeading<db::SignPanorama>(
    const db::Panorama& panorama, const db::SignPanorama& object)
{
    return getObjectCropOutWithHeadingImpl(panorama, object);
}

template<>
std::tuple<geolib3::Heading, PanoCropOut>
getObjectCropOutWithHeading<db::TrafficLightPanorama>(
    const db::Panorama& panorama, const db::TrafficLightPanorama& object)
{
    return getObjectCropOutWithHeadingImpl(panorama, object);
}


} // namespace maps::mrc::common
