"""
Wrapper for a loaded scene with properties.
"""

from __future__ import annotations
import logging
from typing import TYPE_CHECKING, Any, Optional

import glm
import moderngl

import moderngl_window as mglw
from moderngl_window import geometry
from moderngl_window.meta import ProgramDescription
from moderngl_window.resources.programs import programs

from .material import Material
from .node import Node
from .programs import (
    ColorLightProgram,
    FallbackProgram,
    MeshProgram,
    TextureLightProgram,
    TextureProgram,
    TextureVertexColorProgram,
    VertexColorProgram,
)

logger = logging.getLogger(__name__)

if TYPE_CHECKING:
    from moderngl_window.scene import Camera, Material, Mesh, Node


class Scene:
    """Generic scene"""

    def __init__(self, name: Optional[str], **kwargs: Any):
        """Create a scene with a name.

        Args:
            name (str): Unique name or path for the scene
        """
        self.name = name
        self.root_nodes: list[Node] = []

        # References resources in the scene
        self.nodes: list[Node] = []
        self.materials: list[Material] = []
        self.meshes: list[Mesh] = []
        self.cameras: list[Camera] = []

        self.bbox_min: glm.vec3 = glm.vec3()
        self.bbox_max: glm.vec3 = glm.vec3()
        self.diagonal_size = 1.0

        self.bbox_vao = geometry.bbox()

        if self.ctx.extra is None:
            self.ctx.extra = {}

        # Load bbox program and cache in the context
        self.bbox_program = self.ctx.extra.get("DEFAULT_BBOX_PROGRAM")
        if not self.bbox_program:
            self.bbox_program = programs.load(
                ProgramDescription(path="scene_default/bbox.glsl"),
            )
            self.ctx.extra["DEFAULT_BBOX_PROGRAM"] = self.bbox_program

        # Load wireframe program and cache in the context
        self.wireframe_program = self.ctx.extra.get("DEFAULT_WIREFRAME_PROGRAM")
        if not self.wireframe_program:
            self.wireframe_program = programs.load(
                ProgramDescription(path="scene_default/wireframe.glsl"),
            )
            self.ctx.extra["DEFAULT_WIREFRAME_PROGRAM"] = self.wireframe_program

        self._matrix = glm.mat4()

    @property
    def ctx(self) -> moderngl.Context:
        """moderngl.Context: The current context"""
        return mglw.ctx()

    @property
    def matrix(self) -> glm.mat4:
        """glm.mat4x4: The current model matrix

        This property is settable.
        """
        return self._matrix

    @matrix.setter
    def matrix(self, matrix: glm.mat4) -> None:
        self._matrix = matrix
        for node in self.root_nodes:
            node.calc_model_mat(self._matrix)

    def draw(
        self,
        projection_matrix: Optional[glm.mat4],
        camera_matrix: Optional[glm.mat4],
        time: float = 0.0,
    ) -> None:
        """Draw all the nodes in the scene.

        Args:
            projection_matrix (ndarray): projection matrix (bytes)
            camera_matrix (ndarray): camera_matrix (bytes)
            time (float): The current time
        """
        for node in self.root_nodes:
            node.draw(
                projection_matrix=projection_matrix,
                camera_matrix=camera_matrix,
                time=time,
            )

        self.ctx.clear_samplers(0, 4)

    def draw_bbox(
        self,
        projection_matrix: Optional[glm.mat4] = None,
        camera_matrix: Optional[glm.mat4] = None,
        children: float = True,
        color: tuple[float, float, float] = (0.75, 0.75, 0.75),
    ) -> None:
        """Draw scene and mesh bounding boxes.

        Args:
            projection_matrix (glm.mat4): mat4 projection
            camera_matrix (glm.mat4): mat4 camera matrix
            children (bool): Will draw bounding boxes for meshes as well
            color (tuple): Color of the bounding boxes
        """
        projection_matrix = projection_matrix
        camera_matrix = camera_matrix

        # Scene bounding box
        self.bbox_program["m_proj"].write(projection_matrix)
        self.bbox_program["m_model"].write(self._matrix)
        self.bbox_program["m_cam"].write(camera_matrix)
        self.bbox_program["bb_min"].write(self.bbox_min)
        self.bbox_program["bb_max"].write(self.bbox_max)
        self.bbox_program["color"].value = color
        self.bbox_vao.render(self.bbox_program)

        if not children:
            return

        # Draw bounding box for children
        for node in self.root_nodes:
            node.draw_bbox(projection_matrix, camera_matrix, self.bbox_program, self.bbox_vao)

    def draw_wireframe(
        self,
        projection_matrix: Optional[glm.mat4] = None,
        camera_matrix: Optional[glm.mat4] = None,
        color: tuple[float, float, float, float] = (0.75, 0.75, 0.75, 1.0),
    ) -> None:
        """Render the scene in wireframe mode.

        Args:
            projection_matrix (ndarray): mat4 projection
            camera_matrix (ndarray): mat4 camera matrix
            children (bool): Will draw bounding boxes for meshes as well
            color (tuple): Color of the wireframes
        """
        projection_matrix = projection_matrix
        camera_matrix = camera_matrix

        self.wireframe_program["m_proj"].write(projection_matrix)
        self.wireframe_program["m_model"].write(self._matrix)
        self.wireframe_program["m_cam"].write(camera_matrix)
        self.wireframe_program["color"] = color

        # Draw bounding box for children
        self.ctx.wireframe = True

        for node in self.root_nodes:
            node.draw_wireframe(projection_matrix, camera_matrix, self.wireframe_program)

        self.ctx.wireframe = False

    def apply_mesh_programs(
        self, mesh_programs: Optional[list[MeshProgram]] = None, clear: bool = True
    ) -> None:
        """Applies mesh programs to meshes.
        If not mesh programs are passed in we assign default ones.

        Args:
            mesh_programs (list): List of mesh programs to assign
            clear (bool): Clear all assigned mesh programs
        """
        global DEFAULT_PROGRAMS

        if clear:
            for mesh in self.meshes:
                mesh.mesh_program = None

        if not mesh_programs:
            mesh_programs = self.ctx.extra.get("DEFAULT_PROGRAMS")
            if not mesh_programs:
                mesh_programs = [
                    TextureLightProgram(),
                    TextureProgram(),
                    VertexColorProgram(),
                    TextureVertexColorProgram(),
                    ColorLightProgram(),
                    FallbackProgram(),
                ]
                self.ctx.extra["DEFAULT_PROGRAMS"] = mesh_programs

        for mesh in self.meshes:
            for mesh_prog in mesh_programs:
                instance = mesh_prog.apply(mesh)
                if instance is not None:
                    if isinstance(instance, MeshProgram):
                        mesh.mesh_program = mesh_prog
                        break
                    else:
                        raise ValueError(
                            "apply() must return a MeshProgram instance, not {}".format(
                                type(instance)
                            )
                        )

            if not mesh.mesh_program:
                logger.warning("WARING: No mesh program applied to '%s'", mesh.name)

    def calc_scene_bbox(self) -> None:
        """Calculate scene bbox"""
        bbox_min: glm.vec3 | None = None
        bbox_max: glm.vec3 | None = None

        for node in self.root_nodes:
            bbox_min, bbox_max = node.calc_global_bbox(glm.mat4(), bbox_min, bbox_max)

        self.bbox_min = bbox_min
        self.bbox_max = bbox_max

        self.diagonal_size = glm.length(self.bbox_max - self.bbox_min)

    def get_center(self) -> glm.vec3:
        """Calculate the center of the scene using bounding boxes"""
        return self.bbox_min + (self.bbox_max - self.bbox_min) / 2.0

    def prepare(self) -> None:
        """prepare the scene for rendering.

        Calls ``apply_mesh_programs()`` assigning default meshprograms if needed
        and sets the model matrix.
        """
        self.apply_mesh_programs()
        # Recursively calculate model matrices
        self.matrix = glm.mat4()

    def find_node(self, name: Optional[str] = None) -> Optional[Node]:
        """Finds a :py:class:`~moderngl_window.scene.Node`

        Keyword Args:
            name (str): Case sensitive name
        Returns:
            A :py:class:`~moderngl_window.scene.Node` or ``None`` if not found.
        """
        for node in self.nodes:
            if node.name == name:
                return node

        return None

    def find_material(self, name: Optional[str] = None) -> Optional[Material]:
        """Finds a :py:class:`~moderngl_window.scene.Material`

        Keyword Args:
            name (str): Case sensitive material name
        Returns:
            A :py:class:`~moderngl_window.scene.Material` or ``None``
        """
        for mat in self.materials:
            if mat.name == name:
                return mat

        return None

    def release(self) -> None:
        """Destroys the scene data and vertex buffers"""
        self.destroy()

    def destroy(self) -> None:
        """Destroys the scene data and vertex buffers"""
        for mesh in self.meshes:
            if mesh.vao is not None:
                mesh.vao.release()
            # if mesh.mesh_program:
            #     mesh.mesh_program.program.release()

        for mat in self.materials:
            mat.release()

        self.meshes = []
        self.root_nodes = []

    def __str__(self) -> str:
        return "<Scene: {}>".format(self.name)

    def __repr__(self) -> str:
        return str(self)
