#include <maps/libs/common/include/exception.h>
#include <maps/libs/common/include/math.h>
#include <maps/wikimap/mapspro/services/mrc/libs/common/include/opencv.h>

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

#include <algorithm>
#include <numeric>
#include <vector>

namespace maps {
namespace mrc {
namespace common {
namespace {

double toConfidence(double value, double min, double mid, double max)
{
    constexpr double HALF_CONFIDENCE = 0.5;

    value = maps::common::clip(value, min, max);
    if (value < mid) {
        return (value - min) / (mid - min) * HALF_CONFIDENCE;
    }
    else {
        return (value - mid) / (max - mid) * HALF_CONFIDENCE
               + HALF_CONFIDENCE;
    }
}

double getClarity(const cv::Mat& image)
{
    constexpr double MIN_CLARITY = 0.;
    constexpr double MID_CLARITY = 10.;
    constexpr double MAX_CLARITY = 100.;

    cv::Mat laplacian;
    cv::Laplacian(image, laplacian, CV_64F);

    cv::Scalar mean;
    cv::Scalar stddev;
    cv::meanStdDev(laplacian, mean, stddev);

    return toConfidence(stddev.val[0], MIN_CLARITY, MID_CLARITY, MAX_CLARITY);
}

cv::MatND getGrayHistogram(const cv::Mat& image, int size)
{
    const int CHANNELS[] = {0};
    const int SIZES[] = {size};
    const float BIN_BOUNDARIES[] = {0, 256};
    const float* RANGES[] = {BIN_BOUNDARIES};

    cv::Mat gray;
    cv::cvtColor(image, gray, CV_BGR2GRAY);

    cv::MatND histogram;
    cv::calcHist(&gray, /*image count*/ 1, CHANNELS, cv::Mat(), histogram,
                 /*dimensions*/ 1, SIZES, RANGES);
    return histogram;
}

double getDeviation(const cv::MatND& histogram)
{
    constexpr float MIN_PERCENT = 0.15;
    constexpr float MID_PERCENT = 0.5;
    constexpr float MAX_PERCENT = 0.85;

    std::vector<float> psum = histogram;
    std::partial_sum(psum.begin(), psum.end(), psum.begin());
    auto sum = psum.back();
    auto min = std::lower_bound(psum.begin(), psum.end(), MIN_PERCENT * sum);
    auto mid = std::lower_bound(psum.begin(), psum.end(), MID_PERCENT * sum);
    auto max = std::lower_bound(psum.begin(), psum.end(), MAX_PERCENT * sum);
    return std::min(mid - min, max - mid);
}

double getColorBalance(const cv::Mat& image)
{
    constexpr int MIN_DEVIATION = 0;
    constexpr int MID_DEVIATION = 20;
    constexpr int MAX_DEVIATION = 85;
    constexpr int HISTOGRAM_SIZE = 255;

    auto deviation = getDeviation(getGrayHistogram(image, HISTOGRAM_SIZE));
    return toConfidence(deviation, MIN_DEVIATION, MID_DEVIATION,
                        MAX_DEVIATION);
}

} // anonymous namespace

Size sizeOf(const cv::Mat& mat)
{
    return Size {
        static_cast<size_t>(mat.size().width),
        static_cast<size_t>(mat.size().height)
    };
}

cv::Mat fitImageTo(const cv::Mat& image, Size wndSize, int interpolation)
{
    ASSERT(wndSize.width > 0 && wndSize.height > 0);
    REQUIRE(
        image.cols > 0 && image.rows > 0,
        "image has a wrong size: " << image.cols << "x" << image.rows << "."
    );

    if (static_cast<size_t>(image.cols) <= wndSize.width &&
        static_cast<size_t>(image.rows) <= wndSize.height)
    {
        return image;
    }

    const auto xScaleFactor = static_cast<double>(wndSize.width)  / image.cols;
    const auto yScaleFactor = static_cast<double>(wndSize.height) / image.rows;
    const auto scaleFactor = std::min(xScaleFactor, yScaleFactor);

    cv::Mat result;
    cv::resize(image, result, {}, scaleFactor, scaleFactor, interpolation);
    return result;
}


double estimateImageQuality(const Blob& imageData) try {
    auto image = decodeImage(imageData);
    cv::Rect crop(image.cols / 5, image.rows / 5, image.cols * 3 / 5,
                  image.rows * 3 / 5);
    cv::Mat croppedImage = image(crop);
    return std::min(getClarity(croppedImage), getColorBalance(croppedImage));
}
catch (const cv::Exception& e) {
    throw Exception() << e.what();
}

void equalizeHistogramBGR(cv::Mat& img)
{
    constexpr double CLIP_LIMIT = 2; // Heuristic

    cv::Mat img_yuv;
    cv::cvtColor(img, img_yuv, cv::COLOR_BGR2YUV);
    std::vector<cv::Mat> channels;
    cv::split(img_yuv, channels);
    cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(CLIP_LIMIT);
    clahe->apply(channels[0], channels[0]); // Apply to Y channel
    cv::merge(channels, img_yuv);
    cv::cvtColor(img_yuv, img, cv::COLOR_YUV2BGR);
}

Bytes equalizeHistogram(const Bytes& encodedImage)
{
    cv::Mat bgrImg = decodeImage(encodedImage);
    equalizeHistogramBGR(bgrImg);

    Bytes jpgImg;
    cv::imencode(".jpg", bgrImg, jpgImg);
    return jpgImg;
}

Blob equalizeHistogram(const Blob& encodedImage)
{
    const auto jpgImg = equalizeHistogram(toBytes(encodedImage));
    return toBlob(jpgImg);
}

void ellipticalBlur(cv::Mat& image, const cv::Rect& circumscribedRect)
{
    // A kernel with size of a fifth of a rectangle size most likely destroys
    // all recognizable features of an object.
    constexpr int BLURRING_KERNEL_DIVISOR = 5;

    const auto areaToBlur = circumscribedRect & cv::Rect{{0, 0}, image.size()};
    if (areaToBlur.width <= 0 || areaToBlur.height <= 0) {
        return;
    }
    auto bboxImg = image(areaToBlur);

    auto ksize = bboxImg.size() / BLURRING_KERNEL_DIVISOR;
    ksize.width = std::max(1, ksize.width);
    ksize.height = std::max(1, ksize.height);

    cv::Mat blured;
    cv::blur(bboxImg, blured, ksize);

    // Make an elliptical mask.
    cv::Mat mask(blured.size(), CV_8U, cv::Scalar{0});
    cv::ellipse(mask, cv::RotatedRect{{(float)mask.cols / 2.f,
                                       (float)mask.rows / 2.f} /* center */,
                                      {(float)mask.cols,
                                       (float)mask.rows} /* width x height*/,
                                      .0f /* angle */},
                cv::Scalar(1), -1 /* fill the area */);
    // Copy the blured ellipse.
    blured.copyTo(bboxImg, mask);
}

template <>
Bytes getTestImage()
{
    // Smallest possible PNG from:
    // https://github.com/mathiasbynens/small/blob/master/png-transparent.png
    return {0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 0x00, 0x00,
            0x00, 0x0d, 0x49, 0x48, 0x44, 0x52, 0x00, 0x00, 0x00, 0x01,
            0x00, 0x00, 0x00, 0x01, 0x08, 0x06, 0x00, 0x00, 0x00, 0x1f,
            0x15, 0xc4, 0x89, 0x00, 0x00, 0x00, 0x0a, 0x49, 0x44, 0x41,
            0x54, 0x78, 0x9c, 0x63, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00,
            0x01, 0x0d, 0x0a, 0x2d, 0xb4, 0x00, 0x00, 0x00, 0x00, 0x49,
            0x45, 0x4e, 0x44, 0xae, 0x42, 0x60, 0x82};
}

template <>
Blob getTestImage()
{
    return toBlob(getTestImage<Bytes>());
}

} // namespace common
} // namespace mrc
} // namespace maps
