#include <maps/wikimap/mapspro/services/autocart/libs/satellite/include/load_sat_params.h>

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

#include <opencv2/opencv.hpp>

#include <limits>
#include <thread>
#include <chrono>
#include <regex>

namespace maps {
namespace wiki {
namespace autocart {

namespace {

// Mean earth radius
// https://en.wikipedia.org/wiki/Earth_radius#Mean_radius
const double EARTH_RADIUS_IN_METERS = 6371008.8;

/**
 * @brief Сonverts degrees to radians.
 * @param degree - angle in degrees
 * @return angle in radians
 */
double toRadians(double degree) {
    return M_PI * degree / 180.;
}

/**
 * @brief Extracts parameter value from string.
 *     Parameter must have the following format:
 *     "<name>": "<value>"
 * @param str   - string from which parameter value is extracted
 * @param name  - parameter name
 * @return parameter value, if parameter is present in the string,
 *     otherwise return std::nullopt
 */
std::optional<double>
extractParam(const std::string& str, const std::string& name) {
    std::regex rgx("\"" + name + "\": \"([0-9.]+)\"");
    std::smatch matches;
    std::regex_search(str, matches, rgx);

    if (2 == matches.size()) {
        return std::stod(matches[1]);
    }
    return std::nullopt;
}

/**
 * @brief Calculates size of pixel in meters.
 *     Size of one pixel is equal to size of bbox in meters
 *     divided by pixels number.
 * @param image         - satellite image
 * @param geodeticBbox  - bounding box of region in geodetic coordinates
 * @return width and height one pixel in meters
 */
PixelSize calcPixelSize(const cv::Size& imageSize,
                        const geolib3::BoundingBox& geodeticBbox) {
    geolib3::Point2 leftBottomPoint = geodeticBbox.lowerCorner();
    geolib3::Point2 rightTopPoint = geodeticBbox.upperCorner();
    geolib3::Point2 rightBottomPoint(rightTopPoint.x(), leftBottomPoint.y());

    double bboxWidth = distanceInMeters(leftBottomPoint, rightBottomPoint);
    double bboxHeight = distanceInMeters(rightBottomPoint, rightTopPoint);
    PixelSize pixelSize;
    pixelSize.widthInMeters = bboxWidth / imageSize.width;
    pixelSize.heightInMeters = bboxHeight / imageSize.height;
    return pixelSize;
}



} // anonymous namespace

std::istream&
operator>>(std::istream& in, std::optional<SatelliteAngles>& angles) {
    std::string str;
    std::getline(in, str);

    std::optional<double> elev = extractParam(str, "elev");
    std::optional<double> azimAngle = extractParam(str, "azim_angle");

    if (elev.has_value() && azimAngle.has_value()) {
        angles = SatelliteAngles(toRadians(*elev), toRadians(*azimAngle));
    } else {
        angles = std::nullopt;
    }

    return in;
}

std::optional<SatelliteAngles> parseSatXml(const xml3::Doc& satXml) {
    std::optional<SatelliteAngles> angles;
    xml3::Node root = satXml.root();
    xml3::Nodes mosaicNodes = root.nodes("mosaic", true);

    int maxZOrder = std::numeric_limits<int>::min();
    for (size_t i = 0; i < mosaicNodes.size(); i++) {
        int zOrder = mosaicNodes[i].attr<int>("zorder", std::numeric_limits<int>::min());
        std::string releaseStatus = mosaicNodes[i].attr<std::string>("release-status", "none");
        if ("production" == releaseStatus && zOrder > maxZOrder) {
            maxZOrder = zOrder;
            xml3::Node metaDataNode = mosaicNodes[i].node("metadata", true);
            if (!metaDataNode.isNull()) {
                angles = metaDataNode.value<std::optional<SatelliteAngles>>();
            }
        }
    }
    return angles;
}

SatelliteParams
getSatelliteParams(const cv::Size& imageSize,
                   const geolib3::BoundingBox& geodeticBbox,
                   const std::string& satParamsUrl) {
    constexpr size_t RETRY_NUMBER = 10;
    constexpr std::chrono::seconds RETRY_TIMEOUT(3);

    http::Client client;
    http::URL url(satParamsUrl);
    // Get satellite information from center of region
    geolib3::Point2 centerPoint = geodeticBbox.center();
    url.addParam("lat", centerPoint.y());
    url.addParam("lon", centerPoint.x());

    http::Request request(client, http::GET, url);

    for (size_t i = 0; i < RETRY_NUMBER; i++) {
        try {
            auto response = request.perform();
            if (response.status() == 200) {
                std::string data = response.readBody();
                xml3::Doc satXml = xml3::Doc::fromString(data);
                SatelliteParams params;
                params.angles = parseSatXml(satXml);
                params.pixelSize = calcPixelSize(imageSize, geodeticBbox);
                return params;
            } else {
                INFO() << "Got unexpected response status " << response.status();
            }
        } catch (const maps::Exception& e) {
            WARN() << e;
        }
        if (i != RETRY_NUMBER) {
            INFO() << "Retry";
            std::this_thread::sleep_for(RETRY_TIMEOUT);
        }
    }
    throw maps::RuntimeError("Failed to read satellite params");
}

double distanceInMeters(const geolib3::Point2& geodeticPoint1,
                        const geolib3::Point2& geodeticPoint2) {
    double lat1 = toRadians(geodeticPoint1.y());
    double lon1 = toRadians(geodeticPoint1.x());

    double lat2 = toRadians(geodeticPoint2.y());
    double lon2 = toRadians(geodeticPoint2.x());

    // Distance on sphere
    double angleDist;
    angleDist = acos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(lon1 - lon2));

    return EARTH_RADIUS_IN_METERS * angleDist;
}

} // namespace autocart
} // namespace wiki
} // namespace maps
