from __future__ import annotations

import numpy as np  # type: ignore
import osmnx as ox
from geopy.distance import geodesic

from collections.abc import Mapping
from typing import Tuple, Any, List
from urllib.parse import urlsplit, urlunsplit, parse_qsl, urlencode
import folium


def add_query_params(url: str, params: Mapping[str, Any]) -> str:
    """Safely updates URL query parameters without breaking the URL structure."""
    parts = list(urlsplit(url))
    query = dict(parse_qsl(parts[3]))
    query.update(params)
    parts[3] = urlencode(query)
    return urlunsplit(parts)


def extract_subgraph(
    G: Any, north: float, south: float, east: float, west: float
) -> Any:
    """
    Extracts a subgraph within a bbox.
    Uses OSMnx's internal spatial clipping for better performance.
    """
    return ox.truncate.truncate_graph_bbox(G, north, south, east, west, retain_all=True)


def get_distance_between_points(
    point_a: Tuple[float, float], point_b: Tuple[float, float], unit="km"
) -> float:
    try:
        dist = geodesic(point_a, point_b)
        unit_map = {"km": "kilometers", "m": "meters", "mi": "miles"}
        return getattr(dist, unit_map.get(unit, "kilometers"))
    except (ValueError, TypeError, AttributeError):
        return 0.0


def get_bounding_box_from_points(
    gps_points: List[Tuple[float, float]], margin: float = 0.001
) -> Tuple[float, float, float, float]:
    lats, lons = zip(*gps_points)
    return (
        max(lats) + margin,
        min(lats) - margin,
        max(lons) + margin,
        min(lons) - margin,
    )


tile_options = {
    "OpenStreetMap": "OpenStreetMap",
    "CartoDB": "cartodbpositron",
    "CartoDB Voyager": "cartodbvoyager",
}


def add_arrows(
    map_object: folium.Map,
    locations: List[Tuple[float, float]],
    color: str,
    n_arrows: int,
) -> folium.Map:
    """
    Adds directional arrows to a map object to indicate paths or flows.
    """
    n = len(locations)
    if n > 2:
        for i in range(0, n - 1, n // n_arrows):
            start, end = locations[i], locations[i + 1]
            rotation = (
                -np.arctan2((end[1] - start[1]), (end[0] - start[0])) * 180 / np.pi
            )
            folium.RegularPolygonMarker(
                location=end,
                fill_color=color,
                number_of_sides=2,
                radius=6,
                rotation=rotation,
            ).add_to(map_object)
    return map_object


def attach_supported_tiles(
    map_object: folium.Map, default_tile: str = "OpenStreetMap"
) -> None:
    """
    Attaches supported tile layers to a given folium map object.
    """
    normalized_default_tile = default_tile.lower()
    tile_options_filtered = {
        k: v for k, v in tile_options.items() if v.lower() != normalized_default_tile
    }

    for tile, description in tile_options_filtered.items():
        folium.TileLayer(name=tile, tiles=description, show=False).add_to(map_object)
