from OpenGL.GL import *
import numpy as np
from .shader import basic_shader
from PIL import Image
import ctypes
import math
import time

_default_shader = None
_last_bound_shader = [None]
_last_bound_vao = [None]

def get_default_shader():
    global _default_shader
    if _default_shader is None:
        _default_shader = basic_shader()
        _default_shader.uniforms = {
            "uModel": glGetUniformLocation(_default_shader.program, "uModel"),
            "uUVTiling": glGetUniformLocation(_default_shader.program, "uUVTiling"),
            "uUVOffset": glGetUniformLocation(_default_shader.program, "uUVOffset"),
            "uTexture": glGetUniformLocation(_default_shader.program, "uTexture"),
            "useTexture": glGetUniformLocation(_default_shader.program, "useTexture"),
        }
    return _default_shader

def safe_use_shader(shader):
    if _last_bound_shader[0] != shader:
        glUseProgram(shader.program)
        _last_bound_shader[0] = shader

def safe_bind_vao(vao):
    if _last_bound_vao[0] != vao:
        glBindVertexArray(vao)
        _last_bound_vao[0] = vao

def get_model_matrix(position, scale):
    model = np.identity(4, dtype=np.float32)
    model[0, 0], model[1, 1], model[2, 2] = scale
    model[:3, 3] = position  # poprawione na kolumnę 3
    return model

class Mesh:
    def __init__(self, vertices, shader=None):
        self.shader = shader or get_default_shader()
        if not hasattr(self.shader, "uniforms"):
            self.shader.uniforms = {
                "uModel": glGetUniformLocation(self.shader.program, "uModel"),
                "uUVTiling": glGetUniformLocation(self.shader.program, "uUVTiling"),
                "uUVOffset": glGetUniformLocation(self.shader.program, "uUVOffset"),
                "uTexture": glGetUniformLocation(self.shader.program, "uTexture"),
                "useTexture": glGetUniformLocation(self.shader.program, "useTexture"),
            }

        self.vertex_count = len(vertices) // 8
        self.position = np.zeros(3, dtype=np.float32)
        self.scale = np.ones(3, dtype=np.float32)
        self.texture = None
        self.uv_tiling = np.ones(2, dtype=np.float32)
        self.uv_offset = np.zeros(2, dtype=np.float32)

        self._last_position = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
        self._last_scale = np.array([np.nan, np.nan, np.nan], dtype=np.float32)

        self._uniforms_uploaded_once = False

        self._init_buffers(vertices)

    def copy(self):
        new = Mesh.__new__(Mesh)
        new.__dict__ = self.__dict__.copy()
        new.position = self.position.copy()
        new.scale = self.scale.copy()
        new._last_position = np.array([np.nan, np.nan, np.nan], dtype=np.float32)
        new._last_scale = np.array([np.nan, np.nan, np.nan], dtype=np.float32)

        self._uniforms_uploaded_once = False
        return new

    def is_in_fov(self, cam_pos, cam_dir, fov_deg):
        half_fov_rad = math.radians(fov_deg * 1.2)
        cam_dir = cam_dir / np.linalg.norm(cam_dir)
        to_obj = self.position - cam_pos
        dist = np.linalg.norm(to_obj)
        if dist == 0:
            return True
        to_obj_dir = to_obj / dist
        angle = math.acos(np.clip(np.dot(cam_dir, to_obj_dir), -1, 1))
        return angle <= half_fov_rad

    def _init_buffers(self, vertices):
        data = np.array(vertices, dtype=np.float32)
        self.vao = glGenVertexArrays(1)
        self.vbo = glGenBuffers(1)

        glBindVertexArray(self.vao)
        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
        glBufferData(GL_ARRAY_BUFFER, data.nbytes, data, GL_STATIC_DRAW)

        stride = 8 * 4
        glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(0))
        glEnableVertexAttribArray(0)
        glVertexAttribPointer(1, 3, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(12))
        glEnableVertexAttribArray(1)
        glVertexAttribPointer(2, 2, GL_FLOAT, GL_FALSE, stride, ctypes.c_void_p(24))
        glEnableVertexAttribArray(2)

        glBindBuffer(GL_ARRAY_BUFFER, 0)
        glBindVertexArray(0)

    def set_uv_transform(self, tiling=(1.0, 1.0), offset=(0.0, 0.0)):
        self.uv_tiling[:] = tiling
        self.uv_offset[:] = offset

    def upload_uniforms(self):
        if (not self._uniforms_uploaded_once or
                not (np.array_equal(self.position, self._last_position) and
                     np.array_equal(self.scale, self._last_scale))):
            model = get_model_matrix(self.position, self.scale)
            uniforms = self.shader.uniforms
            glUniformMatrix4fv(uniforms["uModel"], 1, GL_FALSE, model.T)
            glUniform2fv(uniforms["uUVTiling"], 1, self.uv_tiling)
            glUniform2fv(uniforms["uUVOffset"], 1, self.uv_offset)

            self._last_position = self.position.copy()
            self._last_scale = self.scale.copy()
            self._uniforms_uploaded_once = True

    def draw(self, cam_pos=None, cam_dir=None, fov_deg=70, view_proj_matrix=None, debug=False):
        if debug:
            print(f"Draw called for mesh at {self.position}")

        if cam_pos is not None:
            dist = np.linalg.norm(self.position - cam_pos)
            if dist > 600:  # ustaw granicę zależnie od sceny
                return

        # if cam_pos is not None and cam_dir is not None:
            # if not self.is_in_fov(cam_pos, cam_dir, fov_deg + 50):  # +10° tolerancji
                # return

        safe_use_shader(self.shader)
        self.upload_uniforms()

        uniforms = self.shader.uniforms
        if self.texture:
            glActiveTexture(GL_TEXTURE0)
            glBindTexture(GL_TEXTURE_2D, self.texture)
            glUniform1i(uniforms["uTexture"], 0)
            glUniform1i(uniforms["useTexture"], 1)
        else:
            glUniform1i(uniforms["useTexture"], 0)

        safe_bind_vao(self.vao)
        glDrawArrays(GL_TRIANGLES, 0, self.vertex_count)

        if debug:
            print(f"[DEBUG] Mesh at {self.position} został narysowany.")

    def set_texture(self, path):
        image = Image.open(path).transpose(Image.FLIP_TOP_BOTTOM).convert('RGBA')
        img_data = np.array(image, dtype=np.uint8)

        if self.texture:
            glDeleteTextures(1, [self.texture])

        self.texture = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.texture)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, image.width, image.height, 0,
                     GL_RGBA, GL_UNSIGNED_BYTE, img_data)
        glGenerateMipmap(GL_TEXTURE_2D)

        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)

    def set_position(self, x, y, z):
        self.position[:] = [x, y, z]

    def set_scale(self, x, y, z):
        self.scale[:] = [x, y, z]

    def scale_uniform(self, factor):
        self.scale[:] = factor

    def destroy(self):
        if hasattr(self, 'vao') and glDeleteVertexArrays:
            glDeleteVertexArrays(1, [self.vao])
            glDeleteBuffers(1, [self.vbo])
        if self.texture and glDeleteTextures:
            glDeleteTextures(1, [self.texture])


    # =========================
    # ======== models =========
    # =========================

    @staticmethod
    def from_obj(path, color=(1, 1, 1)):
        positions = []
        texcoords = []
        faces = []

        with open(path, "r") as f:
            for line in f:
                if line.startswith("v "):  # vertex position
                    parts = line.strip().split()
                    positions.append(tuple(map(float, parts[1:4])))
                elif line.startswith("vt "):  # texture coordinate
                    parts = line.strip().split()
                    texcoords.append(tuple(map(float, parts[1:3])))
                elif line.startswith("f "):  # face
                    parts = line.strip().split()[1:]
                    face = []
                    for p in parts:
                        vals = p.split('/')
                        vi = int(vals[0]) - 1
                        ti = int(vals[1]) - 1 if len(vals) > 1 and vals[1] else 0
                        face.append((vi, ti))
                    faces.append(face)

        verts = []
        for face in faces:
            if len(face) >= 3:
                for i in range(1, len(face) - 1):
                    for idx in [0, i, i + 1]:
                        vi, ti = face[idx]
                        pos = positions[vi]
                        uv = texcoords[ti] if texcoords else (0.0, 0.0)
                        verts.extend([*pos, *color, *uv])

        return Mesh(verts, basic_shader())

    @staticmethod
    def cube(color=(1, 1, 1)):
        # Pozycje wierzchołków sześcianu
        p = [(-0.5, -0.5, 0.5), (0.5, -0.5, 0.5), (0.5, 0.5, 0.5), (-0.5, 0.5, 0.5),  # front
             (-0.5, -0.5, -0.5), (0.5, -0.5, -0.5), (0.5, 0.5, -0.5), (-0.5, 0.5, -0.5)]  # back

        # UV mapowanie na jedną ścianę (powtarzane dla każdej)
        uv = [(0, 0), (1, 0), (1, 1), (0, 1)]

        # Indeksy do rysowania ścian (każda ściana: 2 trójkąty)
        faces = [
            (0, 1, 2, 3),  # front
            (5, 4, 7, 6),  # back
            (4, 0, 3, 7),  # left
            (1, 5, 6, 2),  # right
            (3, 2, 6, 7),  # top
            (4, 5, 1, 0)  # bottom
        ]

        verts = []
        for face in faces:
            idx = [face[0], face[1], face[2], face[0], face[2], face[3]]
            uv_idx = [0, 1, 2, 0, 2, 3]
            for i in range(6):
                pos = p[idx[i]]
                tex = uv[uv_idx[i]]
                verts.extend([*pos, *color, *tex])

        return Mesh(verts, basic_shader())

    @staticmethod
    def sphere(radius=0.5, lat_segments=16, lon_segments=16, color=(1, 1, 1)):
        verts = []
        for i in range(lat_segments):
            theta1 = np.pi * (i / lat_segments - 0.5)
            theta2 = np.pi * ((i + 1) / lat_segments - 0.5)
            for j in range(lon_segments):
                phi1 = 2 * np.pi * (j / lon_segments)
                phi2 = 2 * np.pi * ((j + 1) / lon_segments)

                def get_pos(theta, phi):
                    return (
                        radius * np.cos(theta) * np.cos(phi),
                        radius * np.sin(theta),
                        radius * np.cos(theta) * np.sin(phi)
                    )

                # Wierzchołki
                p1 = get_pos(theta1, phi1)
                p2 = get_pos(theta2, phi1)
                p3 = get_pos(theta2, phi2)
                p4 = get_pos(theta1, phi2)

                # UV mapping (prosty sferyczny)
                uv1 = (j / lon_segments, i / lat_segments)
                uv2 = (j / lon_segments, (i + 1) / lat_segments)
                uv3 = ((j + 1) / lon_segments, (i + 1) / lat_segments)
                uv4 = ((j + 1) / lon_segments, i / lat_segments)

                # Trójkąty
                for vtx, uv in zip([p1, p2, p3], [uv1, uv2, uv3]):
                    verts.extend([*vtx, *color, *uv])
                for vtx, uv in zip([p1, p3, p4], [uv1, uv3, uv4]):
                    verts.extend([*vtx, *color, *uv])
        return Mesh(verts, basic_shader())

    @staticmethod
    def capsule(radius=0.25, height=1.0, segments=16, color=(1, 1, 1)):
        verts = []
        half = height / 2

        # === Cylinder środkowy ===
        for j in range(segments):
            theta1 = 2 * np.pi * (j / segments)
            theta2 = 2 * np.pi * ((j + 1) / segments)

            x1, z1 = np.cos(theta1), np.sin(theta1)
            x2, z2 = np.cos(theta2), np.sin(theta2)

            p1 = (radius * x1, -half, radius * z1)
            p2 = (radius * x1, half, radius * z1)
            p3 = (radius * x2, half, radius * z2)
            p4 = (radius * x2, -half, radius * z2)

            uv = [(j / segments, 0.0), (j / segments, 0.5), ((j + 1) / segments, 0.5), ((j + 1) / segments, 0.0)]
            for vtx, tex in zip([p1, p2, p3], [uv[0], uv[1], uv[2]]):
                verts.extend([*vtx, *color, *tex])
            for vtx, tex in zip([p1, p3, p4], [uv[0], uv[2], uv[3]]):
                verts.extend([*vtx, *color, *tex])

        # === Półsfera górna ===
        for i in range(segments // 2):
            theta1 = (np.pi / 2) * (i / (segments // 2))
            theta2 = (np.pi / 2) * ((i + 1) / (segments // 2))
            for j in range(segments):
                phi1 = 2 * np.pi * (j / segments)
                phi2 = 2 * np.pi * ((j + 1) / segments)

                def pos(theta, phi):
                    return (
                        radius * np.cos(theta) * np.cos(phi),
                        radius * np.sin(theta) + half,
                        radius * np.cos(theta) * np.sin(phi)
                    )

                p1 = pos(theta1, phi1)
                p2 = pos(theta2, phi1)
                p3 = pos(theta2, phi2)
                p4 = pos(theta1, phi2)

                uv1 = (j / segments, 0.5 + (i / (segments * 2)))
                uv2 = (j / segments, 0.5 + ((i + 1) / (segments * 2)))
                uv3 = ((j + 1) / segments, 0.5 + ((i + 1) / (segments * 2)))
                uv4 = ((j + 1) / segments, 0.5 + (i / (segments * 2)))

                for vtx, tex in zip([p1, p2, p3], [uv1, uv2, uv3]):
                    verts.extend([*vtx, *color, *tex])
                for vtx, tex in zip([p1, p3, p4], [uv1, uv3, uv4]):
                    verts.extend([*vtx, *color, *tex])

        # === Półsfera dolna ===
        for i in range(segments // 2):
            theta1 = (np.pi / 2) * (i / (segments // 2))
            theta2 = (np.pi / 2) * ((i + 1) / (segments // 2))
            for j in range(segments):
                phi1 = 2 * np.pi * (j / segments)
                phi2 = 2 * np.pi * ((j + 1) / segments)

                def pos(theta, phi):
                    return (
                        radius * np.cos(theta) * np.cos(phi),
                        -radius * np.sin(theta) - half,
                        radius * np.cos(theta) * np.sin(phi)
                    )

                p1 = pos(theta1, phi1)
                p2 = pos(theta2, phi1)
                p3 = pos(theta2, phi2)
                p4 = pos(theta1, phi2)

                uv1 = (j / segments, 0.5 - (i / (segments * 2)))
                uv2 = (j / segments, 0.5 - ((i + 1) / (segments * 2)))
                uv3 = ((j + 1) / segments, 0.5 - ((i + 1) / (segments * 2)))
                uv4 = ((j + 1) / segments, 0.5 - (i / (segments * 2)))

                # UWAGA: zamieniona kolejność rysowania — PRAWIDŁOWY winding
                for vtx, tex in zip([p1, p3, p2], [uv1, uv3, uv2]):
                    verts.extend([*vtx, *color, *tex])
                for vtx, tex in zip([p1, p4, p3], [uv1, uv4, uv3]):
                    verts.extend([*vtx, *color, *tex])

        return Mesh(verts, basic_shader())

    @staticmethod
    def plane(size=1.0, color=(1, 1, 1)):
        hs = size / 2
        positions = [(-hs, 0, -hs), (hs, 0, -hs), (hs, 0, hs), (-hs, 0, hs)]
        uvs = [(0, 0), (1, 0), (1, 1), (0, 1)]
        indices = [(0, 1, 2), (0, 2, 3)]  # front
        back_indices = [(2, 1, 0), (3, 2, 0)]  # back side (odwrócone)

        verts = []
        for face in indices + back_indices:
            for idx in face:
                verts.extend([*positions[idx], *color, *uvs[idx]])
        return Mesh(verts, basic_shader())

    @staticmethod
    def water(
            size=10.0,
            resolution=64,
            wave_speed=0.03,
            wave_height=0.1,
            wave_scale=(0.3, 0.4),
            second_wave=True,
            color=(0.2, 0.5, 1.0),
            backface=True
    ):
        half = size / 2
        step = size / resolution
        verts = []

        for z in range(resolution):
            for x in range(resolution):
                x0 = -half + x * step
                x1 = x0 + step
                z0 = -half + z * step
                z1 = z0 + step

                u0, u1 = x / resolution, (x + 1) / resolution
                v0, v1 = z / resolution, (z + 1) / resolution

                pos = [(x0, 0.0, z0), (x1, 0.0, z0), (x1, 0.0, z1), (x0, 0.0, z1)]
                uv = [(u0, v0), (u1, v0), (u1, v1), (u0, v1)]

                # Indeksy dla frontu i ewentualnie backface
                indices = [(0, 1, 2), (0, 2, 3)]
                if backface:
                    indices += [(2, 1, 0), (3, 2, 0)]

                for a, b, c in indices:
                    for i in [a, b, c]:
                        verts.extend([*pos[i], *color, *uv[i]])

        mesh = Mesh(verts, basic_shader())
        mesh._water_size = size
        mesh._water_res = resolution
        mesh._water_time = 0.0
        mesh._wave_speed = wave_speed
        mesh._wave_height = wave_height
        mesh._wave_scale = wave_scale
        mesh._second_wave = second_wave
        mesh._water_verts = np.array(verts, dtype=np.float32).reshape(-1, 8)

        # Wysokości bazowe (y)
        mesh._water_initial_y = mesh._water_verts[:, 1].copy()

        # Dynamiczny buffer
        glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo)
        glBufferData(GL_ARRAY_BUFFER, mesh._water_verts.nbytes, mesh._water_verts.flatten(), GL_DYNAMIC_DRAW)
        glBindBuffer(GL_ARRAY_BUFFER, 0)

        def update_water():
            mesh._water_time += mesh._wave_speed
            verts = mesh._water_verts
            X = verts[:, 0]
            Z = verts[:, 2]

            # Fale
            verts[:, 1] = (
                                  np.sin(X * mesh._wave_scale[0] + mesh._water_time) +
                                  (np.cos(Z * mesh._wave_scale[1] + mesh._water_time) if mesh._second_wave else 0.0)
                          ) * (mesh._wave_height * 0.5)

            glBindBuffer(GL_ARRAY_BUFFER, mesh.vbo)
            glBufferSubData(GL_ARRAY_BUFFER, 0, verts.nbytes, verts.flatten())
            glBindBuffer(GL_ARRAY_BUFFER, 0)

        mesh.update_water = update_water
        return mesh

    def update_water(self):
        if not hasattr(self, '_original_vertices'):
            return

        verts = self._original_vertices.copy()
        t = time.time() - self._start_time

        for i in range(0, len(verts), 8):
            x = verts[i]
            z = verts[i + 2]
            verts[i + 1] = math.sin(x * 0.5 + t) * 0.2 + math.cos(z * 0.5 + t) * 0.2

        glBindBuffer(GL_ARRAY_BUFFER, self.vbo)
        glBufferSubData(GL_ARRAY_BUFFER, 0, verts.nbytes, verts)
        glBindBuffer(GL_ARRAY_BUFFER, 0)
