"""Low-level ModernGL render units used by scenes.

Render objects own shaders, GL programs, uniforms, and vertex arrays. They do
not own scene orchestration, camera-slot policy, or backend interaction.
"""

import moderngl as mgl
import numpy as np

from ..core import System


def _type_module_name(value) -> str:
    return str(getattr(type(value), "__module__", ""))


def _is_moderngl_proxy(value) -> bool:
    module = _type_module_name(value)
    return (
        module == "moderngl"
        or module.startswith("moderngl.")
        or module == "mgl"
        or module.startswith("mgl.")
        or module == "_moderngl"
        or module.startswith("_moderngl.")
    )


def _is_moderngl_context_like(value) -> bool:
    if isinstance(value, mgl.Context):
        return True
    return _is_moderngl_proxy(value) and type(value).__name__ == "Context"


def _release_nested_resources(value, *, seen: set[int] | None = None) -> None:
    if value is None:
        return
    if seen is None:
        seen = set()
    object_id = id(value)
    if object_id in seen:
        return
    seen.add(object_id)

    if isinstance(value, (str, bytes, bytearray, bool, int, float, np.generic)):
        return
    if isinstance(value, np.ndarray):
        return
    if isinstance(value, dict):
        for nested in value.values():
            _release_nested_resources(nested, seen=seen)
        return
    if isinstance(value, (list, tuple, set, frozenset)):
        for nested in value:
            _release_nested_resources(nested, seen=seen)
        return
    if _is_moderngl_context_like(value):
        return

    release = getattr(value, "release", None)
    if callable(release):
        try:
            release()
        except Exception:
            pass
        return

    # ModernGL exposes proxy objects such as uniforms whose attributes point
    # back into the shared GL context. Treat them as opaque leaves unless they
    # expose an explicit owned-resource release method handled above.
    if _is_moderngl_proxy(value):
        return

    attributes = getattr(value, "__dict__", None)
    if not isinstance(attributes, dict):
        return
    for nested in attributes.values():
        _release_nested_resources(nested, seen=seen)


class Render:
    """Base class for one ModernGL draw unit."""

    VERTEX = """
                #version 330
                in vec3 in_spin;
                void main() {
                    gl_Position = vec4(in_spin, 1.0);
                }
            """
    GEOMETRY = None
    FRAGMENT = """
                #version 330
                out vec4 f_color;
                void main() {
                    f_color = vec4(1.0,1.0,1.0,1.0);
                }
            """

    def __init__(   
        self, 
        mglctx: mgl.Context = None
    ):
        """Create the GL program and bind discovered uniforms as attributes."""
        self._ctx = mglctx

        self._prog = self._ctx.program(
            vertex_shader=self.VERTEX,
            geometry_shader=self.GEOMETRY,
            fragment_shader=self.FRAGMENT,
        )

        self.populate_uniforms()

    def populate_uniforms(self):
        """Expose all program uniforms as direct Python attributes."""
        for u in self._prog:
            obj = self._prog[u]
            if isinstance(obj, mgl.Uniform):
                self.__dict__[u] = obj

    def render(self, **kwargs):
        """Render the unit.

        Subclasses are expected to override this with concrete draw calls.
        """
        self._ctx.clear(0.60, 0.60, 0.60, 1.0)

    def list_properties(self):
        """Return render-local properties exposed through the scene contract."""
        return []

    def _replace_owned_geometry(
        self,
        build_geometry,
        build_vao=None,
        *,
        mesh_attr: str = "mesh",
        vao_attr: str = "_vao",
    ) -> tuple[object, object]:
        """Atomically replace a live `(mesh, vao)` pair.

        The old geometry is kept alive until the new mesh and VAO both exist.
        If construction fails, any partially built new resources are released
        and the previous live geometry stays attached to the render.
        """
        previous_mesh = getattr(self, mesh_attr, None)
        previous_vao = getattr(self, vao_attr, None)
        next_mesh = None
        next_vao = None
        try:
            if build_vao is None:
                next_mesh, next_vao = build_geometry()
            else:
                next_mesh = build_geometry()
                next_vao = build_vao(next_mesh)
        except Exception:
            if next_vao is not None:
                _release_nested_resources(next_vao)
            if next_mesh is not None:
                _release_nested_resources(next_mesh)
            raise

        setattr(self, mesh_attr, next_mesh)
        setattr(self, vao_attr, next_vao)
        if previous_vao is not None:
            _release_nested_resources(previous_vao)
        if previous_mesh is not None:
            _release_nested_resources(previous_mesh)
        return next_mesh, next_vao

    def release(self) -> None:
        """Release GL objects owned directly or indirectly by this render."""
        vao = getattr(self, "_vao", None)
        if vao is not None:
            try:
                vao.release()
            except Exception:
                pass
            self._vao = None

        prog = getattr(self, "_prog", None)
        if prog is not None:
            try:
                prog.release()
            except Exception:
                pass
            self._prog = None

        for name, value in list(getattr(self, "__dict__", {}).items()):
            if name in {"_ctx", "_prog", "_vao"}:
                continue
            if name.startswith("u_"):
                self.__dict__[name] = None
                continue
            if isinstance(value, np.ndarray):
                self.__dict__[name] = None
                continue
            _release_nested_resources(value)


####################################################################################


class SystemRender(Render):
    """Render that depends on a :class:`topovec.core.System`."""

    def __init__(
        self, 
        mglctx: mgl.Context = None,
        system: System = None,
    ):
        super().__init__(mglctx=mglctx)
        self._system = system



####################################################################################

class StateRender(SystemRender):
    """Render that additionally depends on a GPU-backed state buffer."""
    def __init__(
        self,
        mglctx: mgl.Context = None,
        system: System = None,
        stateBuffer: np.ndarray = None,
    ):
        super().__init__(mglctx=mglctx, system=system)
        self._state = stateBuffer

        self._vao = self._ctx.vertex_array(
            self._prog,
            [
                (self._state.vbo, "3f", "in_spin"),
            ],
        )
