import sys
import types
from collections.abc import Callable, Iterable, Iterator
from typing import (
    Any,
    Generic,
    Protocol,
    SupportsFloat,
    TypeAlias,
    TypeVar,
)

# use typing_extensions for compatibility with older Python versions
if sys.version_info >= (3, 13):
    from warnings import deprecated
else:
    from typing_extensions import deprecated

if sys.version_info >= (3, 11):
    from typing import Self
else:
    from typing_extensions import Self

from pygame.mask import Mask
from pygame.rect import FRect, Rect
from pygame.surface import Surface
from pygame.typing import Point, RectLike

# Some sprite functions only need objects with certain attributes, not always a sprite
class _HasRect(Protocol):
    @property
    def rect(self) -> FRect | Rect | None: ...

# image in addition to rect
class _HasImageAndRect(_HasRect, Protocol):
    @property
    def image(self) -> Surface | None: ...

# mask in addition to rect
class _HasMaskAndRect(_HasRect, Protocol):
    @property
    def mask(self) -> Mask: ...

class Sprite(_HasImageAndRect):
    @property
    def image(self) -> Surface | None: ...
    @image.setter
    def image(self, value: Surface | None) -> None: ...
    @property
    def rect(self) -> FRect | Rect | None: ...
    @rect.setter
    def rect(self, value: FRect | Rect | None) -> None: ...
    @property
    def layer(self) -> int: ...
    @layer.setter
    def layer(self, value: int) -> None: ...
    def __init__(self, *groups: _GroupOrGroups[Any]) -> None: ...
    def add_internal(self, group: AbstractGroup[Any]) -> None: ...
    def remove_internal(self, group: AbstractGroup[Any]) -> None: ...
    def update(self, *args: Any, **kwargs: Any) -> None: ...
    def add(self, *groups: _GroupOrGroups[Any]) -> None: ...
    def remove(self, *groups: _GroupOrGroups[Any]) -> None: ...
    def kill(self) -> None: ...
    def alive(self) -> bool: ...
    def groups(self) -> list[AbstractGroup[Sprite]]: ...

class DirtySprite(Sprite):
    dirty: int
    blendmode: int
    source_rect: FRect | Rect
    visible: int
    _layer: int

_SpriteT = TypeVar("_SpriteT", bound=Sprite)
_SpriteT2 = TypeVar("_SpriteT2", bound=Sprite)
_DirtySpriteT = TypeVar("_DirtySpriteT", bound=DirtySprite)

_GroupOrGroups: TypeAlias = AbstractGroup[_SpriteT] | Iterable[_GroupOrGroups[_SpriteT]]
_SpriteOrSprites: TypeAlias = _SpriteT | Iterable[_SpriteOrSprites[_SpriteT]]

class AbstractGroup(Generic[_SpriteT]):
    spritedict: dict[_SpriteT, FRect | Rect | None]
    lostsprites: list[FRect | Rect]
    def __class_getitem__(cls, item: Any, /) -> types.GenericAlias: ...
    def __init__(self) -> None: ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[_SpriteT]: ...
    def __bool__(self) -> bool: ...
    def __contains__(self, item: Any) -> bool: ...
    def add_internal(self, sprite: _SpriteT, layer: int | None = None) -> None: ...
    def remove_internal(self, sprite: _SpriteT) -> None: ...
    def has_internal(self, sprite: _SpriteT) -> bool: ...
    def copy(self) -> Self: ...
    def sprites(self) -> list[_SpriteT]: ...
    def add(self, *sprites: _SpriteOrSprites[_SpriteT]) -> None: ...
    def remove(self, *sprites: _SpriteOrSprites[_SpriteT]) -> None: ...
    def has(self, *sprites: _SpriteOrSprites[_SpriteT]) -> bool: ...
    def update(self, *args: Any, **kwargs: Any) -> None: ...
    def draw(
        self, surface: Surface, bgd: Surface | None = None, special_flags: int = 0
    ) -> list[FRect | Rect]: ...
    def clear(
        self,
        surface: Surface,
        bgd: Surface | Callable[[Surface, FRect | Rect], Any],
    ) -> None: ...
    def empty(self) -> None: ...

class Group(AbstractGroup[_SpriteT]):
    def __init__(self, *sprites: _SpriteOrSprites[_SpriteT]) -> None: ...

# These deprecated types are just aliases in the code too
@deprecated("Use `pygame.sprite.Group` instead")
class RenderPlain(Group[_SpriteT]): ...

@deprecated("Use `pygame.sprite.Group` instead")
class RenderClear(Group[_SpriteT]): ...

class RenderUpdates(Group[_SpriteT]): ...

@deprecated("Use `pygame.sprite.RenderUpdates` instead")
class OrderedUpdates(RenderUpdates[_SpriteT]): ...

class LayeredUpdates(AbstractGroup[_SpriteT]):
    def __init__(self, *sprites: _SpriteOrSprites[_SpriteT], **kwargs: Any) -> None: ...
    def add(self, *sprites: _SpriteOrSprites[_SpriteT], **kwargs: Any) -> None: ...
    def get_sprites_at(self, pos: Point) -> list[_SpriteT]: ...
    def get_sprite(self, idx: int) -> _SpriteT: ...
    def remove_sprites_of_layer(self, layer_nr: int) -> list[_SpriteT]: ...
    def layers(self) -> list[int]: ...
    def change_layer(self, sprite: _SpriteT, new_layer: int) -> None: ...
    def get_layer_of_sprite(self, sprite: _SpriteT) -> int: ...
    def get_top_layer(self) -> int: ...
    def get_bottom_layer(self) -> int: ...
    def move_to_front(self, sprite: _SpriteT) -> None: ...
    def move_to_back(self, sprite: _SpriteT) -> None: ...
    def get_top_sprite(self) -> _SpriteT: ...
    def get_sprites_from_layer(self, layer: int) -> list[_SpriteT]: ...
    def switch_layer(self, layer1_nr: int, layer2_nr: int) -> None: ...

class LayeredDirty(LayeredUpdates[_DirtySpriteT]):
    def draw(
        self,
        surface: Surface,
        bgd: Surface | None = None,
        special_flags: int | None = None,
    ) -> list[FRect | Rect]: ...
    # clear breaks Liskov substitution principle in code
    def clear(self, surface: Surface, bgd: Surface) -> None: ...  # type: ignore[override]
    def repaint_rect(self, screen_rect: RectLike) -> None: ...
    def set_clip(self, screen_rect: RectLike | None = None) -> None: ...
    def get_clip(self) -> FRect | Rect: ...
    def set_timing_threshold(self, time_ms: SupportsFloat) -> None: ...
    @deprecated(
        "since 2.1.1. Use `pygame.sprite.LayeredDirty.set_timing_threshold` instead"
    )
    def set_timing_treshold(self, time_ms: SupportsFloat) -> None: ...

class GroupSingle(AbstractGroup[_SpriteT]):
    sprite: _SpriteT | None
    def __init__(self, sprite: _SpriteT | None = None) -> None: ...

def collide_rect(left: _HasRect, right: _HasRect) -> bool: ...

class collide_rect_ratio:
    ratio: float
    def __init__(self, ratio: float) -> None: ...
    def __call__(self, left: _HasRect, right: _HasRect) -> bool: ...

# Must have rect attribute, may optionally have radius attribute
_SupportsCollideCircle: TypeAlias = _HasRect

def collide_circle(
    left: _SupportsCollideCircle, right: _SupportsCollideCircle
) -> bool: ...

class collide_circle_ratio:
    ratio: float
    def __init__(self, ratio: float) -> None: ...
    def __call__(
        self, left: _SupportsCollideCircle, right: _SupportsCollideCircle
    ) -> bool: ...

# Arguments to collide_mask must either have mask or have image attribute, in
# addition to mandatorily having a rect attribute
_SupportsCollideMask: TypeAlias = _HasImageAndRect | _HasMaskAndRect

def collide_mask(
    left: _SupportsCollideMask, right: _SupportsCollideMask
) -> tuple[int, int] | None: ...

_HasRectT = TypeVar("_HasRectT", bound=_HasRect)

def spritecollide(
    sprite: _HasRectT,
    group: AbstractGroup[_SpriteT],
    dokill: bool,
    collided: Callable[[_HasRectT, _SpriteT], bool] | None = None,
) -> list[_SpriteT]: ...
def groupcollide(
    groupa: AbstractGroup[_SpriteT],
    groupb: AbstractGroup[_SpriteT2],
    dokilla: bool,
    dokillb: bool,
    collided: Callable[[_SpriteT, _SpriteT2], bool] | None = None,
) -> dict[_SpriteT, list[_SpriteT2]]: ...
def spritecollideany(
    sprite: _HasRectT,
    group: AbstractGroup[_SpriteT],
    collided: Callable[[_HasRectT, _SpriteT], bool] | None = None,
) -> _SpriteT | None: ...
