import builtins
from _typeshed import Incomplete, Unused
from collections.abc import Callable, Collection
from typing import Any, ClassVar, Final, Literal, NoReturn, SupportsIndex, TypeAlias, TypeVar, overload
from typing_extensions import Self, deprecated

import numpy as np
import pandas as pd
from numpy.typing import ArrayLike, DTypeLike, NDArray
from pandas._typing import ScalarIndexer, SequenceIndexer, TakeIndexer
from pandas.api.extensions import ExtensionArray, ExtensionDtype
from pyproj import CRS, Transformer
from shapely import Geometry
from shapely.geometry.base import BaseGeometry

from .base import _AffinityOrigin, _ConvertibleToCRS
from .sindex import SpatialIndex

_ScalarType = TypeVar("_ScalarType", bound=np.generic)
_Array1D: TypeAlias = np.ndarray[tuple[int], np.dtype[_ScalarType]]
_Array2D: TypeAlias = np.ndarray[tuple[int, int], np.dtype[_ScalarType]]
_ArrayOrGeom: TypeAlias = GeometryArray | ArrayLike | Geometry

TransformerFromCRS = Transformer.from_crs
POLYGON_GEOM_TYPES: Final[set[str]]
LINE_GEOM_TYPES: Final[set[str]]
POINT_GEOM_TYPES: Final[set[str]]

class GeometryDtype(ExtensionDtype):
    type: ClassVar[type[BaseGeometry]]
    name: ClassVar[str]
    na_value: None
    @classmethod
    def construct_from_string(cls, string: str) -> Self: ...
    @classmethod
    def construct_array_type(cls) -> builtins.type[GeometryArray]: ...

def isna(value: object) -> bool: ...
def from_shapely(data, crs: _ConvertibleToCRS | None = None) -> GeometryArray: ...
def to_shapely(geoms: GeometryArray) -> _Array1D[np.object_]: ...
def from_wkb(
    data, crs: _ConvertibleToCRS | None = None, on_invalid: Literal["raise", "warn", "ignore", "fix"] = "raise"
) -> GeometryArray: ...

@overload
def to_wkb(geoms: GeometryArray, hex: Literal[False] = False, **kwargs) -> _Array1D[np.bytes_]: ...
@overload
def to_wkb(geoms: GeometryArray, hex: Literal[True], **kwargs) -> _Array1D[np.str_]: ...

def from_wkt(
    data, crs: _ConvertibleToCRS | None = None, on_invalid: Literal["raise", "warn", "ignore", "fix"] = "raise"
) -> GeometryArray: ...
def to_wkt(geoms: GeometryArray, **kwargs) -> _Array1D[np.str_]: ...
def points_from_xy(
    x: ArrayLike, y: ArrayLike, z: ArrayLike | None = None, crs: _ConvertibleToCRS | None = None
) -> GeometryArray: ...

class GeometryArray(ExtensionArray):
    def __init__(self, data: GeometryArray | NDArray[np.object_], crs: _ConvertibleToCRS | None = None) -> None: ...
    @property
    def sindex(self) -> SpatialIndex: ...
    @property
    def has_sindex(self) -> bool: ...

    @property
    def crs(self) -> CRS | None: ...
    @crs.setter
    def crs(self, value: _ConvertibleToCRS | None) -> None: ...

    def check_geographic_crs(self, stacklevel: int) -> None: ...
    @property
    def dtype(self) -> GeometryDtype: ...
    def __len__(self) -> int: ...

    # np.integer[Any] because precision is not important
    @overload
    def __getitem__(self, idx: ScalarIndexer) -> BaseGeometry: ...  # Always 1-D, doesn't accept tuple
    @overload
    def __getitem__(self, idx: SequenceIndexer) -> GeometryArray: ...

    def __setitem__(
        self, key, value: _ArrayOrGeom | pd.DataFrame | pd.Series[Any]  # Cannot use pd.Series[BaseGeometry]
    ) -> None: ...
    @property
    def is_valid(self) -> _Array1D[np.bool_]: ...
    def is_valid_reason(self) -> _Array1D[np.object_]: ...
    def is_valid_coverage(self, gap_width: float = 0.0) -> bool: ...
    def invalid_coverage_edges(self, gap_width: float = 0.0) -> _Array1D[np.object_]: ...
    @property
    def is_empty(self) -> _Array1D[np.bool_]: ...
    @property
    def is_simple(self) -> _Array1D[np.bool_]: ...
    @property
    def is_ring(self) -> _Array1D[np.bool_]: ...
    @property
    def is_closed(self) -> _Array1D[np.bool_]: ...
    @property
    def is_ccw(self) -> _Array1D[np.bool_]: ...
    @property
    def has_z(self) -> _Array1D[np.bool_]: ...
    @property
    def has_m(self) -> _Array1D[np.bool_]: ...
    @property
    def geom_type(self) -> _Array1D[np.str_]: ...
    @property
    def area(self) -> _Array1D[np.float64]: ...
    @property
    def length(self) -> _Array1D[np.float64]: ...
    def count_coordinates(self) -> _Array1D[np.int32]: ...
    def count_geometries(self) -> _Array1D[np.int32]: ...
    def count_interior_rings(self) -> _Array1D[np.int32]: ...
    def get_precision(self) -> _Array1D[np.float64]: ...
    def get_geometry(self, index: SupportsIndex | ArrayLike) -> _Array1D[np.object_]: ...
    @property
    def boundary(self) -> GeometryArray: ...
    @property
    def centroid(self) -> GeometryArray: ...
    def concave_hull(self, ratio: float, allow_holes: bool) -> _Array1D[np.object_]: ...
    def constrained_delaunay_triangles(self) -> GeometryArray: ...
    @property
    def convex_hull(self) -> GeometryArray: ...
    @property
    def envelope(self) -> GeometryArray: ...
    def minimum_rotated_rectangle(self) -> GeometryArray: ...
    @property
    def exterior(self) -> GeometryArray: ...
    def extract_unique_points(self) -> GeometryArray: ...
    def offset_curve(
        self,
        distance: float | ArrayLike,
        quad_segs: int = 8,
        join_style: Literal["round", "bevel", "mitre"] = "round",
        mitre_limit: float = 5.0,
    ) -> GeometryArray: ...
    @property
    def interiors(self) -> _Array1D[np.object_]: ...
    def remove_repeated_points(self, tolerance: float | ArrayLike = 0.0) -> GeometryArray: ...
    def representative_point(self) -> GeometryArray: ...
    def minimum_bounding_circle(self) -> GeometryArray: ...
    def maximum_inscribed_circle(self, tolerance: float | ArrayLike) -> GeometryArray: ...
    def minimum_bounding_radius(self) -> _Array1D[np.float64]: ...
    def minimum_clearance(self) -> _Array1D[np.float64]: ...
    def minimum_clearance_line(self) -> GeometryArray: ...
    def normalize(self) -> GeometryArray: ...
    def orient_polygons(self, exterior_cw: bool = False) -> GeometryArray: ...
    def make_valid(self, method: Literal["linework", "structure"] = "linework", keep_collapsed: bool = True) -> GeometryArray: ...
    def reverse(self) -> GeometryArray: ...
    def segmentize(self, max_segment_length: float | ArrayLike) -> GeometryArray: ...
    def force_2d(self) -> GeometryArray: ...
    def force_3d(self, z: float | ArrayLike = 0) -> GeometryArray: ...
    def transform(
        self, transformation: Callable[[NDArray[np.float64]], NDArray[np.float64]], include_z: bool = False
    ) -> GeometryArray: ...
    def line_merge(self, directed: bool = False) -> GeometryArray: ...
    def set_precision(
        self, grid_size: float, mode: Literal["valid_output", "pointwise", "keep_collapsed", 0, 1, 2] = "valid_output"
    ) -> GeometryArray: ...
    def covers(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def covered_by(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def contains(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def contains_properly(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def crosses(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def disjoint(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def geom_equals(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def intersects(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def overlaps(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def touches(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def within(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def dwithin(self, other: _ArrayOrGeom, distance: float) -> _Array1D[np.bool_]: ...
    def geom_equals_exact(self, other: _ArrayOrGeom, tolerance: float | ArrayLike) -> _Array1D[np.bool_]: ...
    def geom_equals_identical(self, other: _ArrayOrGeom) -> _Array1D[np.bool_]: ...
    def clip_by_rect(self, xmin: float, ymin: float, xmax: float, ymax: float) -> GeometryArray: ...
    def difference(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def intersection(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def symmetric_difference(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def union(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def shortest_line(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def snap(self, other: _ArrayOrGeom, tolerance: float | ArrayLike) -> GeometryArray: ...
    def shared_paths(self, other: _ArrayOrGeom) -> GeometryArray: ...
    def distance(self, other: _ArrayOrGeom) -> _Array1D[np.float64]: ...
    def hausdorff_distance(self, other: _ArrayOrGeom, **kwargs) -> _Array1D[np.float64]: ...
    def frechet_distance(self, other: _ArrayOrGeom, **kwargs) -> _Array1D[np.float64]: ...
    def buffer(self, distance: float | ArrayLike, resolution: int = 16, **kwargs) -> GeometryArray: ...
    def interpolate(self, distance: float | ArrayLike, normalized: bool = False) -> GeometryArray: ...
    def simplify(self, tolerance: float | ArrayLike, preserve_topology: bool = True) -> GeometryArray: ...
    def simplify_coverage(self, tolerance: float | ArrayLike, simplify_boundary: bool = True) -> GeometryArray: ...
    def project(self, other: _ArrayOrGeom, normalized: bool = False) -> _Array1D[np.float64]: ...
    def relate(self, other: _ArrayOrGeom) -> _Array1D[np.str_]: ...
    def relate_pattern(self, other: _ArrayOrGeom, pattern: str) -> _Array1D[np.bool_]: ...
    @deprecated("Use method `union_all` instead.")
    def unary_union(self) -> BaseGeometry: ...
    def union_all(
        self, method: Literal["coverage", "unary", "disjoint_subset"] = "unary", grid_size: float | None = None
    ) -> BaseGeometry: ...
    def intersection_all(self) -> BaseGeometry: ...
    def affine_transform(self, matrix: Collection[float]) -> GeometryArray: ...
    def translate(self, xoff: float = 0.0, yoff: float = 0.0, zoff: float = 0.0) -> GeometryArray: ...
    def rotate(self, angle: float, origin: _AffinityOrigin = "center", use_radians: bool = False) -> GeometryArray: ...
    def scale(
        self, xfact: float = 1.0, yfact: float = 1.0, zfact: float = 1.0, origin: _AffinityOrigin = "center"
    ) -> GeometryArray: ...
    def skew(
        self, xs: float = 0.0, ys: float = 0.0, origin: _AffinityOrigin = "center", use_radians: bool = False
    ) -> GeometryArray: ...
    def to_crs(self, crs: _ConvertibleToCRS | None = None, epsg: int | None = None) -> GeometryArray: ...
    def estimate_utm_crs(self, datum_name: str = "WGS 84") -> CRS: ...
    @property
    def x(self) -> _Array1D[np.float64]: ...
    @property
    def y(self) -> _Array1D[np.float64]: ...
    @property
    def z(self) -> _Array1D[np.float64]: ...
    @property
    def m(self) -> _Array1D[np.float64]: ...
    @property
    def bounds(self) -> _Array2D[np.float64]: ...
    @property
    def total_bounds(self) -> _Array1D[np.float64]: ...
    @property
    def size(self) -> int: ...
    @property
    def shape(self) -> tuple[int]: ...  # Always 1-D, this is not mistaken for tuple[int, ...]
    @property
    def ndim(self) -> Literal[1]: ...
    def copy(self, *args: Unused, **kwargs: Unused) -> GeometryArray: ...
    def take(self, indices: TakeIndexer, allow_fill: bool = False, fill_value: Geometry | None = None) -> GeometryArray: ...
    def fillna(  # type: ignore[override]
        self,
        value: Geometry | GeometryArray | None = None,
        method: Literal["backfill", "bfill", "pad", "ffill"] | None = None,
        limit: int | None = None,
        copy: bool = True,
    ) -> GeometryArray: ...

    @overload  # type: ignore[override]
    def astype(self, dtype: GeometryDtype, copy: bool = True) -> GeometryArray: ...
    @overload
    def astype(self, dtype: ExtensionDtype | Literal["string"], copy: bool = True) -> ExtensionArray: ...  # type: ignore[overload-overlap]
    @overload
    def astype(self, dtype: DTypeLike, copy: bool = True) -> _Array1D[Incomplete]: ...

    def isna(self) -> _Array1D[np.bool_]: ...
    def value_counts(self, dropna: bool = True) -> pd.Series[int]: ...
    def unique(self) -> GeometryArray: ...
    @property
    def nbytes(self) -> int: ...
    def shift(self, periods: int = 1, fill_value: Geometry | None = None) -> GeometryArray: ...  # type: ignore[override]
    def argmin(self, skipna: bool = True) -> NoReturn: ...
    def argmax(self, skipna: bool = True) -> NoReturn: ...
    def __array__(self, dtype: DTypeLike | None = None, copy: bool | None = None) -> _Array1D[np.object_]: ...
    def __eq__(self, other: object) -> _Array1D[np.bool_]: ...  # type: ignore[override]
    def __ne__(self, other: object) -> _Array1D[np.bool_]: ...  # type: ignore[override]
    def __contains__(self, item: object) -> np.bool_: ...

# TODO: Improve `func` type with a callable protocol (with overloads for 2D and 3D geometries)
def transform(
    data: NDArray[np.object_] | GeometryArray | pd.Series[Any],  # Cannot use pd.Series[BaseGeometry]
    func: Callable[..., NDArray[np.float64]],
) -> NDArray[np.object_]: ...
