# ruff: noqa: UP006 UP007 UP045
# @omlish-lite
import io
import time
import typing as ta

from .errors import NeedMoreData
from .segmented import SegmentedBytesView
from .types import BytesLike
from .types import BytesView
from .types import BytesViewLike


##


class FileLikeRawBytesReader:
    """
    Adapter: file-like object -> RawBytesReader-style `read1`.

    This is intentionally permissive: it duck-types common file-like APIs.

    Notes:
      - If the object has `read1`, we use it.
      - Otherwise we fall back to `read`.
      - This is a *raw* reader: it makes no buffering guarantees beyond what the wrapped object provides.
    """

    def __init__(self, f: ta.Any) -> None:
        super().__init__()

        self._f = f

    def read1(self, n: int = -1, /) -> bytes:
        f = self._f
        if hasattr(f, 'read1'):
            return ta.cast(bytes, f.read1(n))
        return ta.cast(bytes, f.read(n))


class FileLikeBufferedBytesReader(FileLikeRawBytesReader):
    """
    Adapter: file-like object -> BufferedBytesReader-style `read/readall`.

    Notes:
      - Uses `readall` if present; otherwise uses `read()` with `-1`.
      - Does not impose additional buffering; it reflects the wrapped object's behavior.
    """

    def read(self, n: int = -1, /) -> bytes:
        return ta.cast(bytes, self._f.read(n))

    def readall(self) -> bytes:
        f = self._f
        if hasattr(f, 'readall'):
            return ta.cast(bytes, f.readall())
        return ta.cast(bytes, f.read())


class BytesBufferReaderAdapter:
    """
    Adapter: BytesBuffer -> file-like reader methods (`read1`, `read`, `readall`).

    This adapter is policy-driven for how it behaves when insufficient bytes are available. The core buffer is
    intentionally non-blocking; blocking behavior (if desired) must be provided via a `fill` callback that supplies more
    bytes into the buffer.

    `policy`:
      - 'raise': raise NeedMoreData if fewer than `n` bytes are available (for n >= 0).
      - 'return_partial': return whatever is available (possibly b'') up to `n`.
      - 'block': repeatedly call `fill()` until satisfied or until `fill()` signals EOF.

    `fill`:
      - Callable that writes more bytes into the underlying MutableBytesBuffer and returns:
          * True  -> made progress / more data may be available
          * False -> EOF / no more data will arrive
      - Only used when policy == 'block'.

    This adapter exists for interop with legacy code that expects file-like objects, but the core design remains
    independent from `io` and blocking semantics.
    """

    def __init__(
            self,
            buf: ta.Any,
            *,
            policy: ta.Literal['raise', 'return_partial', 'block'] = 'raise',
            fill: ta.Optional[ta.Callable[[], bool]] = None,
            block_sleep: ta.Union[ta.Callable[[], None], float, None] = None,
    ) -> None:
        super().__init__()

        self._buf = buf
        self._policy = policy
        self._fill = fill
        self._block_sleep = block_sleep

        if self._policy == 'block' and self._fill is None:
            raise ValueError('policy=block requires fill')

    def _on_block(self) -> None:
        if (bs := self._block_sleep) is None:
            return
        elif callable(bs):
            bs()
        else:
            time.sleep(bs)

    def read1(self, n: int = -1, /) -> bytes:
        return self.read(n)

    def read(self, n: int = -1, /) -> bytes:
        buf = self._buf

        if n is None or n < 0:
            return self.readall()

        if n == 0:
            return b''

        while True:
            ln = len(buf)
            if ln >= n:
                v = buf.split_to(n)
                return ta.cast(bytes, v.tobytes())

            if self._policy == 'return_partial':
                if ln == 0:
                    return b''
                v = buf.split_to(ln)
                return ta.cast(bytes, v.tobytes())

            if self._policy == 'raise':
                raise NeedMoreData

            # block
            if not ta.cast('ta.Callable[[], bool]', self._fill)():
                # EOF
                if ln == 0:
                    return b''
                v = buf.split_to(ln)
                return ta.cast(bytes, v.tobytes())

            self._on_block()

    def readall(self) -> bytes:
        buf = self._buf
        parts: ta.List[bytes] = []

        while True:
            ln = len(buf)
            if ln:
                v = buf.split_to(ln)
                parts.append(ta.cast(bytes, v.tobytes()))
                continue

            if self._policy == 'block':
                if not ta.cast('ta.Callable[[], bool]', self._fill)():
                    break
                self._on_block()
                continue

            break

        return b''.join(parts)


class BytesBufferWriterAdapter:
    """
    Adapter: file-like writer sink <- BytesBuffer / bytes-like.

    This is intentionally small and dumb: it exists to bridge into code expecting a `.write(...)`
    method on an object.

    If given a BytesView-like object, it writes segment-by-segment to avoid materializing copies
    when the sink can accept multiple writes efficiently.
    """

    def __init__(self, f: ta.Any) -> None:
        super().__init__()

        self._f = f

    def write(self, data: ta.Any) -> int:
        f = self._f

        if isinstance(data, (bytes, bytearray, memoryview)):
            b = data.tobytes() if isinstance(data, memoryview) else bytes(data)
            return ta.cast(int, f.write(b))

        if isinstance(data, BytesViewLike):
            total = 0
            for mv in data.segments():
                total += ta.cast(int, f.write(bytes(mv)))
            return total

        if isinstance(data, BytesView):
            b = data.tobytes()
            return ta.cast(int, f.write(b))

        raise TypeError(data)


class BytesIoBytesBuffer:
    """
    BytesBuffer/MutableBytesBuffer implementation backed by `io.BytesIO`, using `getbuffer()`.

    This exists primarily for interoperability with code that already produces/consumes `BytesIO`,
    and to demonstrate how `getbuffer()` can expose a non-copying `memoryview` of internal storage.

    Caveat (important):
      - Any exported `memoryview` from `BytesIO.getbuffer()` can pin the BytesIO against resizing.
      - If a caller holds onto a view and we attempt to grow/resize internally, `BytesIO` may raise
        `BufferError`. We surface that as a RuntimeError.

    This backing is therefore best suited for controlled scenarios; it is *not* the recommended
    default buffer backend for pynetty (segmented/bytearray backends are more predictable).
    """

    def __init__(self) -> None:
        super().__init__()

        self._bio = io.BytesIO()
        self._rpos = 0

        # reserve/commit state
        self._resv: ta.Optional[bytearray] = None
        self._resv_len = 0

    def __len__(self) -> int:
        return self._bio.getbuffer().nbytes - self._rpos

    def peek(self) -> memoryview:
        mv = self._bio.getbuffer()
        return mv[self._rpos:]

    def segments(self) -> ta.Sequence[memoryview]:
        mv = self._bio.getbuffer()
        seg = mv[self._rpos:]
        return (seg,) if len(seg) else ()

    def advance(self, n: int, /) -> None:
        if n < 0 or n > len(self):
            raise ValueError(n)
        self._rpos += n
        # Optional compaction heuristic: if we've consumed a lot, rebuild a smaller BytesIO.
        # This may fail if someone holds a getbuffer() view (BufferError).
        if self._rpos and self._rpos >= 65536 and self._rpos >= (self._bio.getbuffer().nbytes // 2):
            try:
                remaining = self._bio.getbuffer()[self._rpos:].tobytes()
                self._bio = io.BytesIO(remaining)
                self._rpos = 0
            except BufferError as e:
                raise RuntimeError('BytesIO buffer is pinned by an exported view') from e

    def split_to(self, n: int, /) -> SegmentedBytesView:
        if n < 0 or n > len(self):
            raise ValueError(n)
        mv = self._bio.getbuffer()
        out = mv[self._rpos:self._rpos + n]
        self._rpos += n
        return SegmentedBytesView((out,))

    def write(self, data: BytesLike, /) -> None:
        if not data:
            return
        if isinstance(data, memoryview):
            data = data.tobytes()
        try:
            self._bio.seek(0, io.SEEK_END)
            self._bio.write(ta.cast(bytes, data))
        except BufferError as e:
            raise RuntimeError('BytesIO buffer is pinned by an exported view') from e

    def reserve(self, n: int, /) -> memoryview:
        if n < 0:
            raise ValueError(n)
        if self._resv is not None:
            raise RuntimeError('outstanding reserve')
        b = bytearray(n)
        self._resv = b
        self._resv_len = n
        return memoryview(b)

    def commit(self, n: int, /) -> None:
        if self._resv is None:
            raise RuntimeError('no outstanding reserve')
        if n < 0 or n > self._resv_len:
            raise ValueError(n)
        b = self._resv
        self._resv = None
        self._resv_len = 0
        if n:
            self.write(memoryview(b)[:n])
