from OpenGL.GL import *
from OpenGL.GLU import *
from PIL import Image
import os


class Texture:
    def __init__(self, texture_path=None):
        self.texture_id = None
        self.texture_path = texture_path
        self.width = 0
        self.height = 0
        if texture_path:
            self.load(texture_path)
    
    def load(self, texture_path):
        if not os.path.exists(texture_path):
            raise FileNotFoundError(f"Texture file not found: {texture_path}")
        
        self.texture_path = texture_path
        image = Image.open(texture_path)
        image = image.convert('RGBA')
        self.width, self.height = image.size
        image_data = image.tobytes()
        
        self.texture_id = glGenTextures(1)
        glBindTexture(GL_TEXTURE_2D, self.texture_id)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR)
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.width, self.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data)
        glBindTexture(GL_TEXTURE_2D, 0)
    
    def bind(self):
        if self.texture_id:
            glEnable(GL_TEXTURE_2D)
            glBindTexture(GL_TEXTURE_2D, self.texture_id)
    
    def unbind(self):
        glBindTexture(GL_TEXTURE_2D, 0)
        glDisable(GL_TEXTURE_2D)


class BaseModel:
    def __init__(self):
        self.texture = None
    
    def set_texture(self, texture_path):
        self.texture = Texture(texture_path)
    
    def render_with_texture(self):
        if self.texture:
            self.texture.bind()
        self.render()
        if self.texture:
            self.texture.unbind()


class CubeModel(BaseModel):
    def __init__(self, size=1.0):
        super().__init__()
        self.size = size
        self.vertices = [
            # Front face
            (-size, -size, size), (size, -size, size), (size, size, size), (-size, size, size),
            # Back face
            (-size, -size, -size), (-size, size, -size), (size, size, -size), (size, -size, -size),
            # Top face
            (-size, size, -size), (-size, size, size), (size, size, size), (size, size, -size),
            # Bottom face
            (-size, -size, -size), (size, -size, -size), (size, -size, size), (-size, -size, size),
            # Right face
            (size, -size, -size), (size, size, -size), (size, size, size), (size, -size, size),
            # Left face
            (-size, -size, -size), (-size, -size, size), (-size, size, size), (-size, size, -size)
        ]
        
        # Texture coordinates for each face
        self.tex_coords = [
            (0, 0), (1, 0), (1, 1), (0, 1),  # Front
            (0, 0), (1, 0), (1, 1), (0, 1),  # Back
            (0, 0), (1, 0), (1, 1), (0, 1),  # Top
            (0, 0), (1, 0), (1, 1), (0, 1),  # Bottom
            (0, 0), (1, 0), (1, 1), (0, 1),  # Right
            (0, 0), (1, 0), (1, 1), (0, 1),  # Left
        ]
        
        self.colors = [
            (1.0, 0.0, 0.0), (0.0, 1.0, 0.0), (0.0, 0.0, 1.0),
            (1.0, 1.0, 0.0), (1.0, 0.0, 1.0), (0.0, 1.0, 1.0),
        ]
        
        self.faces = [
            (0, 1, 2, 3), (4, 5, 6, 7), (8, 9, 10, 11),
            (12, 13, 14, 15), (16, 17, 18, 19), (20, 21, 22, 23),
        ]
    
    def render(self):
        if self.texture:
            glEnable(GL_TEXTURE_2D)
            self.texture.bind()
            glColor3f(1.0, 1.0, 1.0)
        
        glBegin(GL_QUADS)
        for i, face in enumerate(self.faces):
            if not self.texture:
                color = self.colors[i % len(self.colors)]
                glColor3fv(color)
            for j, vertex_index in enumerate(face):
                if self.texture:
                    glTexCoord2fv(self.tex_coords[vertex_index])
                glVertex3fv(self.vertices[vertex_index])
        glEnd()
        
        if self.texture:
            glDisable(GL_TEXTURE_2D)


class SphereModel(BaseModel):
    def __init__(self, radius=1.0, slices=20, stacks=20):
        super().__init__()
        self.radius = radius
        self.slices = slices
        self.stacks = stacks
    
    def render(self):
        glColor3f(0.5, 0.5, 1.0)
        if self.texture:
            glEnable(GL_TEXTURE_2D)
            self.texture.bind()
        
        quadric = gluNewQuadric()
        if self.texture:
            gluQuadricTexture(quadric, GL_TRUE)
        gluSphere(quadric, self.radius, self.slices, self.stacks)
        gluDeleteQuadric(quadric)
        
        if self.texture:
            glDisable(GL_TEXTURE_2D)


class CustomModel(BaseModel):
    def __init__(self):
        super().__init__()
        self.vertices = []
        self.faces = []
        self.tex_coords = []
        self.colors = []
        self.use_texture = False
        self.normals = []
    
    def add_vertex(self, x, y, z, tex_u=0, tex_v=0, color=(1.0, 1.0, 1.0)):
        self.vertices.append((x, y, z))
        self.tex_coords.append((tex_u, tex_v))
        self.colors.append(color)
        return len(self.vertices) - 1
    
    def add_face(self, vertex_indices, color=None):
        self.faces.append((vertex_indices, color))
    
    def add_triangle(self, v1, v2, v3, color=(1.0, 1.0, 1.0)):
        self.faces.append(([v1, v2, v3], color))
    
    def add_quad(self, v1, v2, v3, v4, color=(1.0, 1.0, 1.0)):
        self.faces.append(([v1, v2, v3, v4], color))
    
    def load_obj(self, file_path):
        """Load a 3D model from OBJ file format"""
        if not os.path.exists(file_path):
            raise FileNotFoundError(f"OBJ file not found: {file_path}")
        
        self.vertices = []
        self.tex_coords = []
        self.normals = []
        self.faces = []
        self.colors = []
        
        temp_vertices = []
        temp_tex_coords = []
        temp_normals = []
        
        with open(file_path, 'r') as f:
            for line in f:
                line = line.strip()
                if not line or line.startswith('#'):
                    continue
                
                parts = line.split()
                if not parts:
                    continue
                
                if parts[0] == 'v':
                    # Vertex: v x y z
                    x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
                    temp_vertices.append((x, y, z))
                
                elif parts[0] == 'vt':
                    # Texture coordinate: vt u v
                    u, v = float(parts[1]), float(parts[2])
                    temp_tex_coords.append((u, v))
                
                elif parts[0] == 'vn':
                    # Normal: vn x y z
                    x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
                    temp_normals.append((x, y, z))
                
                elif parts[0] == 'f':
                    # Face: f v1/vt1/vn1 v2/vt2/vn2 v3/vt3/vn3
                    face_vertices = []
                    face_tex_coords = []
                    face_normals = []
                    
                    for face_part in parts[1:]:
                        indices = face_part.split('/')
                        
                        # Vertex index (required)
                        v_idx = int(indices[0]) - 1  # OBJ indices are 1-based
                        face_vertices.append(v_idx)
                        
                        # Texture coordinate index (optional)
                        if len(indices) > 1 and indices[1]:
                            vt_idx = int(indices[1]) - 1
                            face_tex_coords.append(vt_idx)
                        
                        # Normal index (optional)
                        if len(indices) > 2 and indices[2]:
                            vn_idx = int(indices[2]) - 1
                            face_normals.append(vn_idx)
                    
                    # Store face
                    self.faces.append((face_vertices, None))
        
        # Copy vertices
        self.vertices = temp_vertices[:]
        
        # Setup texture coordinates (match vertex count)
        self.tex_coords = [(0, 0)] * len(self.vertices)
        for face_idx, (face_vertices, _) in enumerate(self.faces):
            for i, v_idx in enumerate(face_vertices):
                if i < len(face_tex_coords) and v_idx < len(self.tex_coords):
                    vt_idx = face_tex_coords[i]
                    if vt_idx < len(temp_tex_coords):
                        self.tex_coords[v_idx] = temp_tex_coords[vt_idx]
        
        # Setup default colors
        self.colors = [(1.0, 1.0, 1.0)] * len(self.vertices)
        
        print(f"[CustomModel] Loaded OBJ: {len(self.vertices)} vertices, {len(self.faces)} faces")
        return True
    
    def set_texture(self, texture_path):
        super().set_texture(texture_path)
        self.use_texture = True
    
    def render(self):
        if self.use_texture and self.texture:
            glEnable(GL_TEXTURE_2D)
            self.texture.bind()
            glColor3f(1.0, 1.0, 1.0)
        
        for face_indices, face_color in self.faces:
            if len(face_indices) == 3:
                glBegin(GL_TRIANGLES)
            elif len(face_indices) == 4:
                glBegin(GL_QUADS)
            else:
                glBegin(GL_POLYGON)
            
            if not self.use_texture and face_color:
                glColor3fv(face_color)
            
            for i, vertex_index in enumerate(face_indices):
                if self.use_texture and self.texture:
                    glTexCoord2fv(self.tex_coords[vertex_index])
                if not self.use_texture and not face_color and i < len(self.colors):
                    glColor3fv(self.colors[vertex_index])
                glVertex3fv(self.vertices[vertex_index])
            
            glEnd()
        
        if self.use_texture and self.texture:
            glDisable(GL_TEXTURE_2D)
