from typing import Callable, Self

#"pip install PyOpenGL"
from OpenGL.GL  import *
from OpenGL.GLU import *

#(another basecode file, required)
from . import gl_buffer



class Vertex_V_N_X:
	__slots__ = ( "vert", "norm", "texc" )
	def __init__( self:Self ):
		self.vert = None
		self.norm = None
		self.texc = None



class DisplayList:
	__slots__ = ( "_dlist", )

	def __init__( self:Self, draw_fn:Callable[[],None] ):
		self._dlist = glGenLists(1)
		glNewList( self._dlist, GL_COMPILE )
		draw_fn()
		glEndList()
	def __del__( self:Self ):
		glDeleteLists( self._dlist, 1 )

	def call( self:Self ):
		glCallList(self._dlist)



class VertAttribInfo:
	__slots__ = ( "vao_slot", "buffer", "num_chan", "data_type", "stride", "offset" )

	#E.g. `VertAttribInfo( 0, vbo_vert, 3,GL_FLOAT, 3*sizeof(GLfloat) )`
	def __init__( self:Self,
		vao_slot:int,
		buffer:gl_buffer.BufferGPU, num_chan:int,data_type:GLenum, stride:int, offset:int=0
	):
		self.vao_slot = vao_slot
		self.buffer = buffer
		self.num_chan = num_chan
		self.data_type = data_type
		self.stride = stride
		self.offset = offset

class VAO:
	__slots__ = ( "_handle", "attribs", "indices", "_finalized" )

	def __init__( self:Self ):
		self._handle = glGenVertexArrays(1)
		assert self._handle!=0, "Invalid handle; probably forgot to bind a context!"

		self.attribs:dict[int,VertAttribInfo] = {}
		self.indices:tuple[gl_buffer.BufferGPU,GLenum]|None = None

		self._finalized = False
	def __del__( self:Self ):
		glDeleteVertexArrays( 1, [self._handle] )

	@staticmethod
	def bind( vao:Self|None = None ):
		glBindVertexArray( vao._handle if vao else 0 )

	def set_attrib( self:Self, attrib_descr:VertAttribInfo ):
		self.attribs[attrib_descr.vao_slot] = attrib_descr
	def set_indices( self:Self, buffer:gl_buffer.BufferGPU, ind_type:GLenum=GL_UNSIGNED_INT ):
		self.indices = ( buffer, ind_type )

	def finalize( self:Self ):
		VAO.bind(self)
		for vao_slot, attrib in self.attribs.items():
			glEnableVertexAttribArray(vao_slot)

			glBindBuffer( GL_ARRAY_BUFFER, attrib.buffer._handle )
			glVertexAttribPointer( vao_slot,
				attrib.num_chan, attrib.data_type, False,
				attrib.stride,
				ctypes.c_void_p(attrib.offset) #Note cannot just pass in int; fails silently.
			)
		if self.indices:
			glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, self.indices[0]._handle )

		self._finalized = True



class Mesh:
	__slots__ = ( "vao", "elem_mode","num_verts_or_inds" )

	def __init__( self:Self, vao:VAO, elem_mode:GLenum, num_verts_or_inds:int ):
		assert isinstance( vao, VAO )

		assert num_verts_or_inds > 0
		if   elem_mode in ( GL_POINTS, GL_PATCHES ): pass
		elif elem_mode in ( GL_LINE_STRIP, GL_LINE_LOOP ): assert num_verts_or_inds >= 2
		elif elem_mode == GL_LINES: assert num_verts_or_inds % 2 == 0
		elif elem_mode == GL_LINE_STRIP_ADJACENCY: assert num_verts_or_inds >= 4
		elif elem_mode == GL_LINES_ADJACENCY: assert num_verts_or_inds % 4 == 0
		elif elem_mode == GL_TRIANGLES: assert num_verts_or_inds % 3 == 0
		elif elem_mode in ( GL_TRIANGLE_STRIP, GL_TRIANGLE_FAN ): assert num_verts_or_inds >= 3
		elif elem_mode in ( GL_TRIANGLE_STRIP_ADJACENCY, GL_TRIANGLES_ADJACENCY ):
			pass # not sure assert num_verts_or_inds >= 6
		else: raise ValueError("Invalid element mode or mismatch with number of vertices/indices!")

		self.vao = vao
		self.elem_mode         = elem_mode
		self.num_verts_or_inds = num_verts_or_inds

	#Note instancing requires GL 3.1+
	def draw( self:Self, num_instances:int=1 ):
		assert self.vao._finalized, "Forgot to call `.finalize()` on VAO before use!"
		VAO.bind(self.vao)
		if   num_instances == 1:
			if self.vao.indices == None:
				glDrawArrays(
					self.elem_mode, 0,self.num_verts_or_inds
				)
			else:
				glDrawElements(
					self.elem_mode, self.num_verts_or_inds,self.vao.indices[1], None
				)
		elif num_instances > 0:
			if self.vao.indices == None:
				glDrawArraysInstanced(
					self.elem_mode, 0,self.num_verts_or_inds, num_instances
				)
			else:
				glDrawElementsInstanced(
					self.elem_mode, self.num_verts_or_inds,self.vao.indices[1], None, num_instances
				)
