#include <maps/libs/log8/include/log8.h>
#include <maps/libs/common/include/exception.h>
#include <maps/libs/cmdline/include/cmdline.h>

#include <maps/libs/geolib/include/conversion.h>
#include <maps/libs/geolib/include/bounding_box.h>
#include <maps/libs/geolib/include/multipolygon.h>

#include <contrib/libs/opencv/include/opencv2/opencv.hpp>

#include <maps/wikimap/mapspro/services/autocart/libs/geometry/include/hex_wkb.h>
#include <maps/wikimap/mapspro/services/autocart/libs/satellite/include/load_sat_image.h>

#include <maps/libs/tile/include/utils.h>

#include <library/cpp/resource/resource.h>

#include <util/memory/blob.h>

#include <cmath>
#include <string>
#include <fstream>
#include <exception>

namespace tile = maps::tile;
namespace geolib3 = maps::geolib3;

using namespace maps::wiki::autocart;

namespace {

cv::Mat getDummyImage() {
    static const TString DUMMY_IMAGE_KEY = "/maps/autocart/pipeline/no_image.jpg";
    TBlob blob = TBlob::FromString(NResource::Find(DUMMY_IMAGE_KEY));
    cv::Mat encimage(blob.Size(), 1, CV_8UC1, const_cast<void*>(blob.Data()));
    return cv::imdecode(encimage, cv::IMREAD_COLOR);
}

geolib3::MultiPolygon2 getMultiPolygon(const std::string& hexWKBFilepath) {
    std::ifstream ifs(hexWKBFilepath);
    std::string hexWKB;
    std::getline(ifs, hexWKB);
    return hexWKBToMultiPolygon(hexWKB);
}

geolib3::Point2 getDisplayOrigin(
    const maps::geolib3::BoundingBox& bbox, size_t zoom)
{
    tile::DisplayCoord pt1 = tile::mercatorToDisplay(bbox.lowerCorner(), zoom);
    tile::DisplayCoord pt2 = tile::mercatorToDisplay(bbox.upperCorner(), zoom);
    return geolib3::Point2(std::min(pt1.x(), pt2.x()), std::min(pt1.y(), pt2.y()));
}

cv::Point mercatorToImage(
    const geolib3::Point2& point,
    const geolib3::Point2& origin,
    size_t zoom, double scale)
{
    tile::DisplayCoord dispPt = tile::mercatorToDisplay(point, zoom);
    return cv::Point(scale * (dispPt.x() - origin.x()), scale * (dispPt.y() - origin.y()));
}

size_t getImageWidthAtZoom(const geolib3::BoundingBox& mercBbox, size_t zoom) {
    tile::DisplayCoord pt1 = tile::mercatorToDisplay(mercBbox.lowerCorner(), zoom);
    tile::DisplayCoord pt2 = tile::mercatorToDisplay(mercBbox.upperCorner(), zoom);
    return std::abs((int)(pt1.x() - pt2.x())) + 1;
}

cv::Mat drawSatelliteMultiPolygon(const geolib3::MultiPolygon2& multiPolygon) {
    static const int MIN_ZOOM = 0;
    static const int MAX_ZOOM = 20;
    static const double BBOX_RATIO = 1.2;
    static const size_t IMAGE_WIDTH = 800;
    static const size_t LINE_WIDTH = 5;
    static const cv::Scalar RED_COLOR(0, 0, 255);

    geolib3::MultiPolygon2 mercMultiPolygon
        = geolib3::convertGeodeticToMercator(multiPolygon);
    geolib3::BoundingBox mercBbox = mercMultiPolygon.boundingBox();
    geolib3::BoundingBox paddedMercBbox = geolib3::resizeByRatio(mercBbox, BBOX_RATIO);

    cv::Mat image;
    int zoom = MAX_ZOOM;
    for (; zoom >= MIN_ZOOM; zoom--) {
        size_t widthAtZoom = getImageWidthAtZoom(paddedMercBbox, zoom);
        if (widthAtZoom < 2 * IMAGE_WIDTH || zoom == MIN_ZOOM) {
            try {
                image = loadSatImage(paddedMercBbox, zoom);
                break;
            } catch (const std::exception& e) {
                continue;
            }
        }
    }

    if (image.empty()) {
        return {};
    }

    double scale = IMAGE_WIDTH / (double)image.cols;
    cv::resize(image, image, cv::Size(IMAGE_WIDTH, image.rows * scale));

    geolib3::Point2 origin = getDisplayOrigin(paddedMercBbox, zoom);
    for (size_t i = 0; i < mercMultiPolygon.polygonsNumber(); i++) {
        geolib3::Polygon2 mercPolygon = mercMultiPolygon.polygonAt(i);
        std::vector<cv::Point> polygon;
        for (size_t j = 0; j < mercPolygon.pointsNumber(); j++) {
            geolib3::Point2 pt = mercPolygon.pointAt(j);
            polygon.emplace_back(mercatorToImage(pt, origin, zoom, scale));
        }
        cv::polylines(image, {polygon}, true, RED_COLOR, LINE_WIDTH);
    }

    return image;
}

} // namespace

int main(int argc, const char** argv)
try {
    maps::cmdline::Parser parser("Draw multipolygon in satellite image");

    maps::cmdline::Option<std::string> regionHexWKBFilepath = parser.string("shape_file")
        .required()
        .help("Path to txt file with hex wkb of region");

    maps::cmdline::Option<std::string> outputFilepath = parser.string("output")
        .required()
        .help("Path to output image");

    parser.parse(argc, const_cast<char**>(argv));

    geolib3::MultiPolygon2 multiPolygon = getMultiPolygon(regionHexWKBFilepath);

    cv::Mat image = drawSatelliteMultiPolygon(multiPolygon);
    if (image.empty()) {
        image = getDummyImage();
    }
    cv::imwrite(outputFilepath, image);

    return EXIT_SUCCESS;
}
catch (const maps::Exception& e) {
    INFO() << e;
    return EXIT_FAILURE;
}
catch (const std::exception& e) {
    INFO() << e.what();
    return EXIT_FAILURE;
}
catch (...) {
    INFO() << "Caught unknown exception";
    return EXIT_FAILURE;
}
