"""Explicit framebuffer render targets shared by headless and GUI backends."""

from __future__ import annotations

from typing import Iterable

import moderngl as mgl


def _normalize_size(size: Iterable[int]) -> tuple[int, int]:
    if size is None:
        return 1, 1
    width, height = size
    return max(1, int(width)), max(1, int(height))


class RenderTarget:
    """One explicit framebuffer target used by scene-level rendering."""

    def __init__(
        self,
        *,
        ctx: mgl.Context,
        fbo: mgl.Framebuffer,
        size: tuple[int, int],
        aspectratio: float | None = None,
    ):
        self._ctx = ctx
        self._fbo = fbo
        self._size = _normalize_size(size)
        self._aspectratio = (
            float(aspectratio)
            if aspectratio is not None
            else float(self._size[0]) / max(float(self._size[1]), 1.0)
        )

    @property
    def ctx(self) -> mgl.Context:
        return self._ctx

    @property
    def fbo(self) -> mgl.Framebuffer:
        return self._fbo

    @property
    def size(self) -> tuple[int, int]:
        return self._size

    @property
    def aspectratio(self) -> float:
        return self._aspectratio

    def bind(self) -> "RenderTarget":
        use = getattr(self._fbo, "use", None)
        if callable(use):
            use()
        if hasattr(self._ctx, "viewport"):
            self._ctx.viewport = (0, 0, *self._size)
        return self

    def clear_color(self, *color: float | Iterable[float]) -> None:
        if len(color) == 1 and not isinstance(color[0], (int, float)):
            rgba = tuple(float(v) for v in color[0])
        else:
            rgba = tuple(float(v) for v in color)
        self.bind()
        self._fbo.color_mask = (True, True, True, True)
        self._fbo.depth_mask = True
        if len(rgba) == 3:
            self._fbo.clear(*rgba, 1.0)
        else:
            self._fbo.clear(*rgba)

    def clear_depth(self, depth: float = 1.0) -> None:
        previous_color_mask = self._fbo.color_mask
        previous_depth_mask = self._fbo.depth_mask
        try:
            self.bind()
            self._fbo.color_mask = (False, False, False, False)
            self._fbo.depth_mask = True
            try:
                self._fbo.clear(0.0, 0.0, 0.0, 0.0, float(depth))
            except TypeError:
                self._fbo.clear()
        finally:
            self._fbo.color_mask = previous_color_mask
            self._fbo.depth_mask = previous_depth_mask

    def read_rgb(self) -> bytes:
        return self._fbo.read(components=3, alignment=1)

    @classmethod
    def from_current(
        cls,
        ctx: mgl.Context,
        *,
        size: tuple[int, int] | None = None,
        aspectratio: float | None = None,
    ) -> "RenderTarget":
        current_fbo = ctx.fbo
        resolved_size = _normalize_size(
            getattr(current_fbo, "size", None) if size is None else size
        )
        return cls(
            ctx=ctx,
            fbo=current_fbo,
            size=resolved_size,
            aspectratio=aspectratio,
        )


class OwnedRenderTarget(RenderTarget):
    """Framebuffer target owning its texture/depth attachments."""

    def __init__(
        self,
        ctx: mgl.Context,
        size: tuple[int, int],
        *,
        components: int = 4,
    ):
        self._color_texture: mgl.Texture | None = None
        self._depth_rbo: mgl.Renderbuffer | None = None
        self._framebuffer: mgl.Framebuffer | None = None
        self._components = int(components)
        self._ctx_owner = ctx
        self._allocate(size)

    def _allocate(self, size: tuple[int, int]) -> None:
        resolved_size = _normalize_size(size)
        self._color_texture = self._ctx_owner.texture(
            resolved_size,
            components=self._components,
        )
        self._color_texture.filter = (mgl.LINEAR, mgl.LINEAR)
        self._depth_rbo = self._ctx_owner.depth_renderbuffer(resolved_size)
        self._framebuffer = self._ctx_owner.framebuffer(
            color_attachments=[self._color_texture],
            depth_attachment=self._depth_rbo,
        )
        super().__init__(
            ctx=self._ctx_owner,
            fbo=self._framebuffer,
            size=resolved_size,
        )

    @property
    def color_texture(self) -> mgl.Texture:
        if self._color_texture is None:
            raise RuntimeError("Render target texture has already been released.")
        return self._color_texture

    def resize(self, size: tuple[int, int]) -> "OwnedRenderTarget":
        resolved_size = _normalize_size(size)
        if resolved_size == self.size:
            return self
        self.release()
        self._allocate(resolved_size)
        return self

    def release(self) -> None:
        framebuffer = self._framebuffer
        texture = self._color_texture
        depth_rbo = self._depth_rbo
        self._framebuffer = None
        self._color_texture = None
        self._depth_rbo = None
        if framebuffer is not None:
            framebuffer.release()
        if texture is not None:
            texture.release()
        if depth_rbo is not None:
            depth_rbo.release()
