from _typeshed import Incomplete, SupportsGetItem
from collections.abc import Callable, Collection, Hashable, Iterable, Mapping, Sequence
from typing import Any, Literal, Protocol, SupportsIndex, overload, type_check_only
from typing_extensions import Self, TypeAlias, deprecated

import numpy as np
import pandas as pd
from numpy.random import BitGenerator, Generator as RandomGenerator, SeedSequence
from numpy.typing import ArrayLike, NDArray
from pandas._typing import ListLikeU
from pandas.core.base import IndexOpsMixin
from pyproj import CRS
from shapely import Geometry, MultiPolygon, Point, Polygon
from shapely.geometry.base import BaseGeometry

from .array import GeometryArray, _Array1D
from .geodataframe import GeoDataFrame
from .geoseries import GeoSeries
from .sindex import SpatialIndex

@type_check_only
class _SupportsToWkt(Protocol):
    def to_wkt(self) -> str: ...

@type_check_only
class _SupportsGeoInterface(Protocol):  # noqa: Y046
    @property
    def __geo_interface__(self) -> dict[str, Any]: ...  # values are arbitrary

_ConvertibleToCRS: TypeAlias = str | int | tuple[str, str] | list[str] | dict[str, Incomplete] | _SupportsToWkt
_AffinityOrigin: TypeAlias = Literal["center", "centroid"] | Point | tuple[float, float] | tuple[float, float, float]
_ClipMask: TypeAlias = GeoDataFrame | GeoSeries | Polygon | MultiPolygon | tuple[float, float, float, float]  # noqa: Y047
# np.floating[Any] because precision is not important
_BboxLike: TypeAlias = Sequence[float] | NDArray[np.floating[Any]] | Geometry | GeoDataFrame | GeoSeries  # noqa: Y047
_MaskLike: TypeAlias = dict[str, Incomplete] | Geometry | GeoDataFrame | GeoSeries  # noqa: Y047

# Cannot use IndexOpsMixin[Geometry] because of IndexOpsMixin type variable bounds
_GeoListLike: TypeAlias = ArrayLike | Sequence[Geometry] | IndexOpsMixin[Any]
_ConvertibleToGeoSeries: TypeAlias = Geometry | Mapping[int, Geometry] | Mapping[str, Geometry] | _GeoListLike  # noqa: Y047

# Cannot use pd.Series[Geometry] because of pd.Series type variable bounds
_GeomSeq: TypeAlias = Sequence[Geometry] | NDArray[np.object_] | pd.Series[Any] | GeometryArray | GeoSeries
_GeomCol: TypeAlias = Hashable | _GeomSeq  # name of column or column values  # noqa: Y047
# dict[Any, Any] because of variance issues
_ConvertibleToDataFrame: TypeAlias = (  # noqa: Y047
    ListLikeU | pd.DataFrame | dict[Any, Any] | Iterable[ListLikeU | tuple[Hashable, ListLikeU] | dict[Any, Any]]
)

def is_geometry_type(data: object) -> bool: ...

class GeoPandasBase:
    @property
    def area(self) -> pd.Series[float]: ...
    @property
    def crs(self) -> CRS | None: ...
    @crs.setter
    def crs(self, value: _ConvertibleToCRS | None) -> None: ...
    @property
    def geom_type(self) -> pd.Series[str]: ...
    @property
    def type(self) -> pd.Series[str]: ...
    @property
    def length(self) -> pd.Series[float]: ...
    @property
    def is_valid(self) -> pd.Series[bool]: ...
    def is_valid_reason(self) -> pd.Series[str]: ...
    def is_valid_coverage(self, *, gap_width: float = 0.0) -> bool: ...
    def invalid_coverage_edges(self, *, gap_width: float = 0.0) -> GeoSeries: ...
    @property
    def is_empty(self) -> pd.Series[bool]: ...
    def count_coordinates(self) -> pd.Series[int]: ...
    def count_geometries(self) -> pd.Series[int]: ...
    def count_interior_rings(self) -> pd.Series[int]: ...
    @property
    def is_simple(self) -> pd.Series[bool]: ...
    @property
    def is_ring(self) -> pd.Series[bool]: ...
    @property
    def is_ccw(self) -> pd.Series[bool]: ...
    @property
    def is_closed(self) -> pd.Series[bool]: ...
    @property
    def has_z(self) -> pd.Series[bool]: ...
    @property
    def has_m(self) -> pd.Series[bool]: ...
    def get_precision(self) -> pd.Series[float]: ...
    def get_geometry(self, index: SupportsIndex | ArrayLike) -> GeoSeries: ...
    @property
    def boundary(self) -> GeoSeries: ...
    @property
    def centroid(self) -> GeoSeries: ...
    def concave_hull(self, ratio: float = 0.0, allow_holes: bool = False) -> GeoSeries: ...
    def constrained_delaunay_triangles(self) -> GeoSeries: ...
    @property
    def convex_hull(self) -> GeoSeries: ...
    def delaunay_triangles(self, tolerance: float | ArrayLike = 0.0, only_edges: bool | ArrayLike = False) -> GeoSeries: ...
    def voronoi_polygons(
        self, tolerance: float | ArrayLike = 0.0, extend_to: Geometry | None = None, only_edges: bool = False
    ) -> GeoSeries: ...
    @property
    def envelope(self) -> GeoSeries: ...
    def minimum_rotated_rectangle(self) -> GeoSeries: ...
    @property
    def exterior(self) -> GeoSeries: ...
    def extract_unique_points(self) -> GeoSeries: ...
    def offset_curve(
        self,
        distance: float | ArrayLike,
        quad_segs: int = 8,
        join_style: Literal["round", "bevel", "mitre"] = "round",
        mitre_limit: float = 5.0,
    ) -> GeoSeries: ...
    @property
    def interiors(self) -> pd.Series[Any]: ...  # Cannot use pd.Series[BaseGeometry]
    def remove_repeated_points(self, tolerance: float = 0.0) -> GeoSeries: ...
    def set_precision(
        self, grid_size: float, mode: Literal["valid_output", "pointwise", "keep_collapsed"] = "valid_output"
    ) -> GeoSeries: ...
    def representative_point(self) -> GeoSeries: ...
    def minimum_bounding_circle(self) -> GeoSeries: ...
    def maximum_inscribed_circle(self, *, tolerance: float | ArrayLike | None = None) -> GeoSeries: ...
    def minimum_bounding_radius(self) -> pd.Series[float]: ...
    def minimum_clearance(self) -> pd.Series[float]: ...
    def minimum_clearance_line(self) -> GeoSeries: ...
    def normalize(self) -> GeoSeries: ...
    def orient_polygons(self, *, exterior_cw: bool = False) -> GeoSeries: ...
    def make_valid(self, *, method: Literal["linework", "structure"] = "linework", keep_collapsed: bool = True) -> GeoSeries: ...
    def reverse(self) -> GeoSeries: ...
    def segmentize(self, max_segment_length: float | ArrayLike) -> GeoSeries: ...
    def transform(
        self, transformation: Callable[[NDArray[np.float64]], NDArray[np.float64]], include_z: bool = False
    ) -> GeoSeries: ...
    def force_2d(self) -> GeoSeries: ...
    def force_3d(self, z: float | ArrayLike = 0) -> GeoSeries: ...
    def line_merge(self, directed: bool = False) -> GeoSeries: ...
    @property
    @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 contains(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def contains_properly(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def dwithin(self, other: GeoSeries | Geometry, distance: float | ArrayLike, align: bool | None = None) -> pd.Series[bool]: ...
    def geom_equals(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def geom_equals_exact(
        self, other: GeoSeries | Geometry, tolerance: float | ArrayLike, align: bool | None = None
    ) -> pd.Series[bool]: ...
    def geom_equals_identical(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def crosses(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def disjoint(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def intersects(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def overlaps(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def touches(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def within(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def covers(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def covered_by(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[bool]: ...
    def distance(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[float]: ...
    def hausdorff_distance(
        self, other: GeoSeries | Geometry, align: bool | None = None, densify: float | ArrayLike | None = None
    ) -> pd.Series[float]: ...
    def frechet_distance(
        self, other: GeoSeries | Geometry, align: bool | None = None, densify: float | ArrayLike | None = None
    ) -> pd.Series[float]: ...
    def difference(self, other: GeoSeries | Geometry, align: bool | None = None) -> GeoSeries: ...
    def symmetric_difference(self, other: GeoSeries | Geometry, align: bool | None = None) -> GeoSeries: ...
    def union(self, other: GeoSeries | Geometry, align: bool | None = None) -> GeoSeries: ...
    def intersection(self, other: GeoSeries | Geometry, align: bool | None = None) -> GeoSeries: ...
    def clip_by_rect(self, xmin: float, ymin: float, xmax: float, ymax: float) -> GeoSeries: ...
    def shortest_line(self, other: GeoSeries | Geometry, align: bool | None = None) -> GeoSeries: ...
    def snap(self, other: GeoSeries | Geometry, tolerance: float | ArrayLike, align: bool | None = None) -> GeoSeries: ...
    def shared_paths(self, other: GeoSeries | Geometry, align: bool | None = None): ...
    @property
    def bounds(self) -> pd.DataFrame: ...
    @property
    def total_bounds(self) -> _Array1D[np.float64]: ...
    @property
    def sindex(self) -> SpatialIndex: ...
    @property
    def has_sindex(self) -> bool: ...
    def buffer(
        self,
        distance: float | ArrayLike,
        resolution: int = 16,
        cap_style: Literal["round", "square", "flat"] = "round",
        join_style: Literal["round", "mitre", "bevel"] = "round",
        mitre_limit: float = 5.0,
        single_sided: bool = False,
        **kwargs,
    ) -> GeoSeries: ...
    def simplify(self, tolerance: float | ArrayLike, preserve_topology: bool = True) -> GeoSeries: ...
    def simplify_coverage(self, tolerance: float | ArrayLike, *, simplify_boundary: bool = True) -> GeoSeries: ...
    def relate(self, other: GeoSeries | Geometry, align: bool | None = None) -> pd.Series[str]: ...
    def relate_pattern(self, other: GeoSeries | Geometry, pattern: str, align: bool | None = None) -> pd.Series[bool]: ...
    def project(self, other: GeoSeries | Geometry, normalized: bool = False, align: bool | None = None) -> pd.Series[float]: ...
    def interpolate(self, distance: float | ArrayLike, normalized: bool = False) -> GeoSeries: ...
    def affine_transform(self, matrix: Collection[float]) -> GeoSeries: ...
    def translate(self, xoff: float = 0.0, yoff: float = 0.0, zoff: float = 0.0) -> GeoSeries: ...
    def rotate(self, angle: float, origin: _AffinityOrigin = "center", use_radians: bool = False) -> GeoSeries: ...
    def scale(
        self, xfact: float = 1.0, yfact: float = 1.0, zfact: float = 1.0, origin: _AffinityOrigin = "center"
    ) -> GeoSeries: ...
    def skew(
        self, xs: float = 0.0, ys: float = 0.0, origin: _AffinityOrigin = "center", use_radians: bool = False
    ) -> GeoSeries: ...
    @property
    def cx(self) -> SupportsGetItem[tuple[SupportsIndex | slice, SupportsIndex | slice], Self]: ...
    def get_coordinates(
        self, include_z: bool = False, ignore_index: bool = False, index_parts: bool = False, *, include_m: bool = False
    ) -> pd.DataFrame: ...
    def hilbert_distance(
        self, total_bounds: tuple[float, float, float, float] | Iterable[float] | None = None, level: int = 16
    ) -> pd.Series[int]: ...
    @overload
    def sample_points(
        self,
        size: int | ArrayLike,
        method: str = "uniform",
        seed: None = None,
        rng: int | ArrayLike | SeedSequence | BitGenerator | RandomGenerator | None = None,
        **kwargs,
    ) -> GeoSeries: ...
    @overload
    @deprecated("Parameter `seed` is deprecated. Use `rng` instead.")
    def sample_points(
        self,
        size: int | ArrayLike,
        method: str = "uniform",
        *,
        seed: int | ArrayLike | SeedSequence | BitGenerator | RandomGenerator,
        rng: int | ArrayLike | SeedSequence | BitGenerator | RandomGenerator | None = None,
        **kwargs,
    ) -> GeoSeries: ...
    def build_area(self, node: bool = True) -> GeoSeries: ...
    @overload
    def polygonize(self, node: bool = True, full: Literal[False] = False) -> GeoSeries: ...
    @overload
    def polygonize(self, node: bool = True, *, full: Literal[True]) -> tuple[GeoSeries, GeoSeries, GeoSeries, GeoSeries]: ...
    @overload
    def polygonize(self, node: bool, full: Literal[True]) -> tuple[GeoSeries, GeoSeries, GeoSeries, GeoSeries]: ...
