"""Boundary mesh rendering: OpenGL VAO/VBO for walkmesh and other boundary geometry."""

from __future__ import annotations

import ctypes
import math

from typing import TYPE_CHECKING

import numpy as np

from pykotor.gl.compat import has_pyopengl, missing_constant, missing_gl_func, safe_gl_error_module
from utility.common.geometry import Vector3

HAS_PYOPENGL = has_pyopengl()
gl_error = safe_gl_error_module()

if HAS_PYOPENGL:
    from OpenGL.GL import (  # pyright: ignore[reportMissingImports]
        glGenBuffers,
        glGenVertexArrays,
        glVertexAttribPointer,
    )
    from OpenGL.GL.shaders import GL_FALSE  # pyright: ignore[reportMissingImports]
    from OpenGL.raw.GL.ARB.tessellation_shader import (
        GL_TRIANGLES,  # pyright: ignore[reportMissingImports]
    )
    from OpenGL.raw.GL.ARB.vertex_shader import GL_FLOAT  # pyright: ignore[reportMissingImports]
    from OpenGL.raw.GL.VERSION.GL_1_0 import (
        GL_UNSIGNED_SHORT,  # pyright: ignore[reportMissingImports]
    )
    from OpenGL.raw.GL.VERSION.GL_1_1 import glDrawElements  # pyright: ignore[reportMissingImports]
    from OpenGL.raw.GL.VERSION.GL_1_5 import (  # pyright: ignore[reportMissingImports]
        GL_ARRAY_BUFFER,
        GL_ELEMENT_ARRAY_BUFFER,
        GL_STATIC_DRAW,
        glBindBuffer,
        glBufferData,
    )
    from OpenGL.raw.GL.VERSION.GL_2_0 import (
        glEnableVertexAttribArray,  # pyright: ignore[reportMissingImports]
    )
    from OpenGL.raw.GL.VERSION.GL_3_0 import (
        glBindVertexArray,  # pyright: ignore[reportMissingImports]
    )
else:
    glGenBuffers = missing_gl_func("glGenBuffers")
    glGenVertexArrays = missing_gl_func("glGenVertexArrays")
    glVertexAttribPointer = missing_gl_func("glVertexAttribPointer")
    glDrawElements = missing_gl_func("glDrawElements")
    glBindBuffer = missing_gl_func("glBindBuffer")
    glBufferData = missing_gl_func("glBufferData")
    glEnableVertexAttribArray = missing_gl_func("glEnableVertexAttribArray")
    glBindVertexArray = missing_gl_func("glBindVertexArray")
    GL_FALSE = missing_constant("GL_FALSE")
    GL_TRIANGLES = missing_constant("GL_TRIANGLES")
    GL_FLOAT = missing_constant("GL_FLOAT")
    GL_UNSIGNED_SHORT = missing_constant("GL_UNSIGNED_SHORT")
    GL_ARRAY_BUFFER = missing_constant("GL_ARRAY_BUFFER")
    GL_ELEMENT_ARRAY_BUFFER = missing_constant("GL_ELEMENT_ARRAY_BUFFER")
    GL_STATIC_DRAW = missing_constant("GL_STATIC_DRAW")

if TYPE_CHECKING:
    from glm import mat4

    from pykotor.gl.scene import Scene
    from pykotor.gl.shader import Shader


class Boundary:
    def __init__(
        self,
        scene: Scene,
        vertices: list[Vector3],
    ):
        self._scene: Scene = scene

        vertices_np, elements_np = self._build_nd(vertices)
        self._vertex_data: np.ndarray = vertices_np
        self._index_data: np.ndarray = elements_np
        self._face_count: int = len(elements_np)

        if HAS_PYOPENGL:
            self._vao = glGenVertexArrays(1)
            self._vbo = glGenBuffers(1)
            self._ebo = glGenBuffers(1)
            glBindVertexArray(self._vao)

            glBindBuffer(GL_ARRAY_BUFFER, self._vbo)
            glBufferData(GL_ARRAY_BUFFER, len(vertices_np) * 4, vertices_np, GL_STATIC_DRAW)

            glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, self._ebo)
            glBufferData(GL_ELEMENT_ARRAY_BUFFER, elements_np.nbytes, elements_np, GL_STATIC_DRAW)

            glEnableVertexAttribArray(1)
            glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, 12, ctypes.c_void_p(0))

            glBindBuffer(GL_ARRAY_BUFFER, 0)
            glBindVertexArray(0)
        else:
            self._vao = 0
            self._vbo = 0
            self._ebo = 0

    @classmethod
    def from_circle(
        cls,
        scene: Scene,
        radius: float,
        smoothness: int = 10,
    ) -> Boundary:
        vertices: list[Vector3] = []
        for i in range(smoothness):
            x = math.cos(i / smoothness * math.pi / 2)
            y = math.sin(i / smoothness * math.pi / 2)
            vertices.append(Vector3(x, y, 0) * radius)
        for i in range(smoothness):
            x = math.cos(i / smoothness * math.pi / 2 + math.pi / 2)
            y = math.sin(i / smoothness * math.pi / 2 + math.pi / 2)
            vertices.append(Vector3(x, y, 0) * radius)
        for i in range(smoothness):
            x = math.cos(i / smoothness * math.pi / 2 + math.pi / 2 * 2)
            y = math.sin(i / smoothness * math.pi / 2 + math.pi / 2 * 2)
            vertices.append(Vector3(x, y, 0) * radius)
        for i in range(smoothness):
            x = math.cos(i / smoothness * math.pi / 2 + math.pi / 2 * 3)
            y = math.sin(i / smoothness * math.pi / 2 + math.pi / 2 * 3)
            vertices.append(Vector3(x, y, 0) * radius)
        return Boundary(scene, vertices)

    def draw(self, shader: Shader, transform: mat4):
        if not HAS_PYOPENGL:
            raise gl_error.NullFunctionError("PyOpenGL is unavailable.")
        shader.set_matrix4("model", transform)
        glBindVertexArray(self._vao)
        glDrawElements(GL_TRIANGLES, self._face_count, GL_UNSIGNED_SHORT, None)

    def vertex_blob(self) -> bytes:
        """Interleaved vertex data (position only; UVs are zero-filled)."""
        vertex_count = len(self._vertex_data) // 3
        blob = np.zeros((vertex_count, 7), dtype=np.float32)
        blob[:, 0:3] = self._vertex_data.reshape(vertex_count, 3)
        return blob.tobytes()

    @property
    def index_data(self) -> bytes:
        return self._index_data.tobytes()

    def _build_nd(self, vertices: list[Vector3]) -> tuple[np.ndarray, np.ndarray]:
        npvertices = []
        for vertex in vertices:
            npvertices.extend([*vertex, *Vector3(vertex.x, vertex.y, vertex.z + 2)])

        npfaces: list[int] = []
        count = len(vertices) * 2
        for i, _vertex in enumerate(vertices):
            index1 = i * 2
            index2 = i * 2 + 2 if i * 2 + 2 < count else 0
            index3 = i * 2 + 1
            index4 = (i * 2 + 2) + 1 if (i * 2 + 2) + 1 < count else 1
            npfaces.extend([index1, index2, index3, index2, index4, index3])
        return np.array(npvertices, dtype="float32"), np.array(npfaces, dtype="int16")
