"""
Protocol objects representing different implementations of the same classes.
"""

# Copyright (C) 2020 The Psycopg Team

from __future__ import annotations

from typing import TYPE_CHECKING, Any, Protocol, TypeAlias, Union
from collections.abc import Callable, Generator, Mapping, Sequence

from . import pq
from ._enums import PyFormat as PyFormat
from ._compat import LiteralString, Template, TypeVar

if TYPE_CHECKING:
    from . import sql
    from .rows import Row, RowMaker
    from .pq.abc import PGresult
    from .waiting import Ready, Wait
    from ._adapters_map import AdaptersMap
    from ._connection_base import BaseConnection

NoneType: type = type(None)

# An object implementing the buffer protocol
Buffer: TypeAlias = Union[bytes, bytearray, memoryview]

QueryNoTemplate: TypeAlias = Union[LiteralString, bytes, "sql.SQL", "sql.Composed"]
Query: TypeAlias = Union[QueryNoTemplate, Template]
Params: TypeAlias = Union[Sequence[Any], Mapping[str, Any]]
ConnectionType = TypeVar("ConnectionType", bound="BaseConnection[Any]")
PipelineCommand: TypeAlias = Callable[[], None]
DumperKey: TypeAlias = Union[type, tuple["DumperKey", ...]]
ConnParam: TypeAlias = Union[str, int, None]
ConnDict: TypeAlias = dict[str, ConnParam]
ConnMapping: TypeAlias = Mapping[str, ConnParam]


# Waiting protocol types

RV = TypeVar("RV")

PQGenConn: TypeAlias = Generator[tuple[int, "Wait"], "Ready | int", RV]
"""Generator for processes where the connection file number can change.

This can happen in connection and reset, but not in normal querying.
"""

PQGen: TypeAlias = Generator["Wait", "Ready | int", RV]
"""Generator for processes where the connection file number won't change.
"""


class WaitFunc(Protocol):
    """
    Wait on the connection which generated `PQgen` and return its final result.
    """

    def __call__(self, gen: PQGen[RV], fileno: int, interval: float = ...) -> RV: ...


# Adaptation types

DumpFunc: TypeAlias = Callable[[Any], Buffer | None]
LoadFunc: TypeAlias = Callable[[Buffer], Any]


class AdaptContext(Protocol):
    """
    A context describing how types are adapted.

    Example of `!AdaptContext` are `~psycopg.Connection`, `~psycopg.Cursor`,
    `~psycopg.adapt.Transformer`, `~psycopg.adapt.AdaptersMap`.

    Note that this is a `~typing.Protocol`, so objects implementing
    `!AdaptContext` don't need to explicitly inherit from this class.

    """

    @property
    def adapters(self) -> AdaptersMap:
        """The adapters configuration that this object uses."""
        ...

    @property
    def connection(self) -> BaseConnection[Any] | None:
        """The connection used by this object, if available.

        :rtype: `~psycopg.Connection` or `~psycopg.AsyncConnection` or `!None`
        """
        ...


class Dumper(Protocol):
    """
    Convert Python objects of type `!cls` to PostgreSQL representation.
    """

    format: pq.Format
    """
    The format that this class `dump()` method produces,
    `~psycopg.pq.Format.TEXT` or `~psycopg.pq.Format.BINARY`.

    This is a class attribute.
    """

    oid: int
    """The oid to pass to the server, if known; 0 otherwise (class attribute)."""

    def __init__(self, cls: type, context: AdaptContext | None = None): ...

    def dump(self, obj: Any) -> Buffer | None:
        """Convert the object `!obj` to PostgreSQL representation.

        :param obj: the object to convert.
        """
        ...

    def quote(self, obj: Any) -> Buffer:
        """Convert the object `!obj` to escaped representation.

        :param obj: the object to convert.
        """
        ...

    def get_key(self, obj: Any, format: PyFormat) -> DumperKey:
        """Return an alternative key to upgrade the dumper to represent `!obj`.

        :param obj: The object to convert
        :param format: The format to convert to

        Normally the type of the object is all it takes to define how to dump
        the object to the database. For instance, a Python `~datetime.date` can
        be simply converted into a PostgreSQL :sql:`date`.

        In a few cases, just the type is not enough. For example:

        - A Python `~datetime.datetime` could be represented as a
          :sql:`timestamptz` or a :sql:`timestamp`, according to whether it
          specifies a `!tzinfo` or not.

        - A Python int could be stored as several Postgres types: int2, int4,
          int8, numeric. If a type too small is used, it may result in an
          overflow. If a type too large is used, PostgreSQL may not want to
          cast it to a smaller type.

        - Python lists should be dumped according to the type they contain to
          convert them to e.g. array of strings, array of ints (and which
          size of int?...)

        In these cases, a dumper can implement `!get_key()` and return a new
        class, or sequence of classes, that can be used to identify the same
        dumper again. If the mechanism is not needed, the method should return
        the same `!cls` object passed in the constructor.

        If a dumper implements `get_key()` it should also implement
        `upgrade()`.

        """
        ...

    def upgrade(self, obj: Any, format: PyFormat) -> Dumper:
        """Return a new dumper to manage `!obj`.

        :param obj: The object to convert
        :param format: The format to convert to

        Once `Transformer.get_dumper()` has been notified by `get_key()` that
        this Dumper class cannot handle `!obj` itself, it will invoke
        `!upgrade()`, which should return a new `Dumper` instance, which will
        be reused for every objects for which `!get_key()` returns the same
        result.
        """
        ...


class Loader(Protocol):
    """
    Convert PostgreSQL values with type OID `!oid` to Python objects.
    """

    format: pq.Format
    """
    The format that this class `load()` method can convert,
    `~psycopg.pq.Format.TEXT` or `~psycopg.pq.Format.BINARY`.

    This is a class attribute.
    """

    def __init__(self, oid: int, context: AdaptContext | None = None): ...

    def load(self, data: Buffer) -> Any:
        """
        Convert the data returned by the database into a Python object.

        :param data: the data to convert.
        """
        ...


class Transformer(Protocol):
    types: tuple[int, ...] | None
    formats: list[pq.Format] | None

    def __init__(self, context: AdaptContext | None = None): ...

    @classmethod
    def from_context(cls, context: AdaptContext | None) -> Transformer: ...

    @property
    def connection(self) -> BaseConnection[Any] | None: ...

    @property
    def encoding(self) -> str: ...

    @property
    def adapters(self) -> AdaptersMap: ...

    @property
    def pgresult(self) -> PGresult | None: ...

    def set_pgresult(
        self,
        result: PGresult | None,
        *,
        set_loaders: bool = True,
        format: pq.Format | None = None,
    ) -> None: ...

    def set_dumper_types(self, types: Sequence[int], format: pq.Format) -> None: ...

    def set_loader_types(self, types: Sequence[int], format: pq.Format) -> None: ...

    def dump_sequence(
        self, params: Sequence[Any], formats: Sequence[PyFormat]
    ) -> Sequence[Buffer | None]: ...

    def as_literal(self, obj: Any) -> bytes: ...

    def get_dumper(self, obj: Any, format: PyFormat) -> Dumper: ...

    def load_rows(self, row0: int, row1: int, make_row: RowMaker[Row]) -> list[Row]: ...

    def load_row(self, row: int, make_row: RowMaker[Row]) -> Row: ...

    def load_sequence(self, record: Sequence[Buffer | None]) -> tuple[Any, ...]: ...

    def get_loader(self, oid: int, format: pq.Format) -> Loader: ...
