"""Functions useful to convert to/from latitude-longitude coordinates to pixel image coordinates."""

from math import asinh, floor, log, pi, radians, sin, tan

from samgis_core import app_logger
from samgis_core.geo.types import ImagePixelCoordinates, LatLng

# Constants from samgis_web.utilities.constants
TILE_SIZE = 256
EARTH_EQUATORIAL_RADIUS = 6378137.0


def _validate_latlng_bounds(lat: float, lng: float) -> None:
    """Raise ValueError if lat/lng outside WGS84 range."""
    if not (-90 <= lat <= 90):
        raise ValueError(f"lat must be in [-90, 90], got {lat}")
    if not (-180 <= lng <= 180):
        raise ValueError(f"lng must be in [-180, 180], got {lng}")


_MERCATOR_LAT_LIMIT = 85.06


def _validate_mercator_bounds(lat: float, lng: float) -> None:
    """Raise ValueError if lat/lng outside Web Mercator range."""
    if not (-_MERCATOR_LAT_LIMIT < lat < _MERCATOR_LAT_LIMIT):
        raise ValueError(
            f"lat must be in (-{_MERCATOR_LAT_LIMIT}, {_MERCATOR_LAT_LIMIT}) for Mercator, got {lat}"
        )
    if not (-180 <= lng <= 180):
        raise ValueError(f"lng must be in [-180, 180], got {lng}")


def _get_latlng2pixel_projection(latlng: LatLng) -> ImagePixelCoordinates:
    _validate_latlng_bounds(latlng.lat, latlng.lng)
    app_logger.debug(f"latlng: {type(latlng)}, value:{latlng}.")
    app_logger.debug(f"latlng lat: {type(latlng.lat)}, value:{latlng.lat}.")
    app_logger.debug(f"latlng lng: {type(latlng.lng)}, value:{latlng.lng}.")
    try:
        sin_y: float = sin(latlng.lat * pi / 180)
        app_logger.debug(f"sin_y, #1:{sin_y}.")
        sin_y = min(max(sin_y, -0.9999), 0.9999)
        app_logger.debug(f"sin_y, #2:{sin_y}.")
        x = TILE_SIZE * (0.5 + latlng.lng / 360)
        app_logger.debug(f"x:{x}.")
        y = TILE_SIZE * (0.5 - log((1 + sin_y) / (1 - sin_y)) / (4 * pi))
        app_logger.debug(f"y:{y}.")

        return ImagePixelCoordinates(x=x, y=y)
    except Exception as e_get_latlng2pixel_projection:
        app_logger.error(f"args type:{type(latlng)}, {latlng}.")
        app_logger.exception(
            f"e_get_latlng2pixel_projection:{e_get_latlng2pixel_projection}.",
        )
        raise


def _get_point_latlng_to_pixel_coordinates(
    latlng: LatLng, zoom: int | float
) -> ImagePixelCoordinates:
    try:
        world_coordinate: ImagePixelCoordinates = _get_latlng2pixel_projection(latlng)
        app_logger.debug(f"world_coordinate:{world_coordinate}.")
        scale: float = pow(2, zoom)
        app_logger.debug(f"scale:{scale}.")
        return ImagePixelCoordinates(
            x=floor(world_coordinate.x * scale), y=floor(world_coordinate.y * scale)
        )
    except Exception as e_format_latlng_to_pixel_coordinates:
        app_logger.error(f"latlng type:{type(latlng)}, {latlng}.")
        app_logger.error(f"zoom type:{type(zoom)}, {zoom}.")
        app_logger.exception(
            f"e_format_latlng_to_pixel_coordinates:{e_format_latlng_to_pixel_coordinates}.",
        )
        raise


def get_latlng_to_pixel_coordinates(
    latlng_origin_ne: LatLng,
    latlng_origin_sw: LatLng,
    latlng_current_point: LatLng,
    zoom: int | float,
    k: str,
) -> ImagePixelCoordinates:
    """
    Parse the input request lambda event

    Args:
        latlng_origin_ne: NE latitude-longitude origin point
        latlng_origin_sw: SW latitude-longitude origin point
        latlng_current_point: latitude-longitude prompt point
        zoom: Level of detail
        k: prompt type

    Returns:
        ImagePixelCoordinates: pixel image coordinate point

    """
    app_logger.debug(
        f"latlng_origin - {k}: {type(latlng_origin_ne)}, value:{latlng_origin_ne}."
    )
    app_logger.debug(
        f"latlng_current_point - {k}: {type(latlng_current_point)}, value:{latlng_current_point}."
    )
    latlng_map_origin_ne = _get_point_latlng_to_pixel_coordinates(
        latlng_origin_ne, zoom
    )
    latlng_map_origin_sw = _get_point_latlng_to_pixel_coordinates(
        latlng_origin_sw, zoom
    )
    latlng_map_current_point = _get_point_latlng_to_pixel_coordinates(
        latlng_current_point, zoom
    )
    diff_coord_x = abs(latlng_map_origin_sw.x - latlng_map_current_point.x)
    diff_coord_y = abs(latlng_map_origin_ne.y - latlng_map_current_point.y)
    point = ImagePixelCoordinates(x=diff_coord_x, y=diff_coord_y)
    app_logger.debug(f"point type - {k}: {point}.")
    return point


def _from4326_to3857(lat: float, lon: float) -> tuple[float, float]:
    _validate_mercator_bounds(lat, lon)
    x_tile: float = radians(lon) * EARTH_EQUATORIAL_RADIUS
    y_tile: float = log(tan(radians(45 + lat / 2.0))) * EARTH_EQUATORIAL_RADIUS
    return x_tile, y_tile


def _deg2num(lat: float, lon: float, zoom: int) -> tuple[float, float]:
    _validate_mercator_bounds(lat, lon)
    if zoom < 0:
        raise ValueError(f"zoom must be >= 0, got {zoom}")
    n = 2**zoom
    x_tile = (lon + 180) / 360 * n
    y_tile = (1 - asinh(tan(radians(lat))) / pi) * n / 2
    return x_tile, y_tile
