import array

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

from ..glbase import gl_buffer
from ..glbase import gl_fbo
from ..glbase import gl_geom
from ..glbase import gl_misc
from ..glbase import gl_program
from ..glbase import gl_texture
from .. import config
from ..math_util import *

from . import camera
from . import driver
from . import level
from . import particles
from . import render_common
from . import render_lowlevel



def _read_file( path:str ):
	with open( path, "r", encoding="utf-8" ) as file:
		return file.read()

def _dump_logs( prog:gl_program.Program, name:str ):
	if not config.is_debugging: return

	logs = [
		( obj.get_log_str(), obj.__class__.__name__ )
		for obj in prog.shaders + [prog]
	]
	total_len = sum([ len(log) for log, objname in logs ])
	if total_len == 0: return

	print(f"=== LOG FOR {name} ===")
	for log, objname in logs:
		if len(log) == 0: continue
		print(f"--- {objname} ({len(log)}) ---\n{log}\n")
	print()



light_pos = Vector3( 20.0,20.0, 50.0 )

class Renderer:
	def __init__( self:Self ):
		vao = gl_geom.VAO()
		vao.set_attrib( gl_geom.VertAttribInfo(
			0, gl_buffer.BufferGPU([ -1.0,-1.0, 3.0,-1.0, -1.0,3.0 ]), 2,GL_FLOAT,8
		) )
		vao.finalize()
		self.msh_fs_tri = gl_geom.Mesh( vao, GL_TRIANGLES,3 )

		vao = gl_geom.VAO()
		self.msh_quad_vbo = gl_buffer.BufferGPU([ NaN,NaN, NaN,NaN, NaN,NaN, NaN,NaN ])
		vao.set_attrib( gl_geom.VertAttribInfo( 0, self.msh_quad_vbo, 2,GL_FLOAT,8 ) )
		vao.finalize()
		self.msh_quad = gl_geom.Mesh( vao, GL_TRIANGLE_STRIP,4 )

		##TODO: remove
		#DX=2.0; DY=1.0
		#vertdata = [ 2+DX,2+DY,1, 1+DX,2+DY,1, 2+DX,3+DY,1 ]#, 1+DX,3+DY,1 ]
		#vao = gl_geom.VAO()
		#vao.set_attrib( gl_geom.VertAttribInfo(
		#	0, gl_buffer.BufferGPU(vertdata), 3,GL_FLOAT,12
		#) )
		#vao.finalize()
		#self.msh_extra_caster = gl_geom.Mesh( vao, GL_TRIANGLE_STRIP,len(vertdata)//3 )

		self.msh_marble = gl_misc.get_mesh_latlong_sphere(config.detail_marble)

		self.msh_gem = render_lowlevel.get_mesh_gem()

		self.msh_star = render_lowlevel.get_mesh_star()

		vao = gl_geom.VAO()
		vertdata = [ 0.00,0.00, 1.00,0.00, 0.00,1.00, 1.00,1.00 ]
		texcdata = [ 0.00,0.50, 0.25,0.50, 0.00,0.75, 0.25,0.75 ]
		vao.set_attrib( gl_geom.VertAttribInfo( 0, gl_buffer.BufferGPU(vertdata), 2,GL_FLOAT,8 ) )
		vao.set_attrib( gl_geom.VertAttribInfo( 1, gl_buffer.BufferGPU([0,0,1]*4), 3,GL_FLOAT,12 ) )
		vao.set_attrib( gl_geom.VertAttribInfo( 2, gl_buffer.BufferGPU(texcdata), 2,GL_FLOAT,8 ) )
		vao.finalize()
		self.msh_time = gl_geom.Mesh( vao, GL_TRIANGLE_STRIP,4 )

		self.ubo_data = array.array( "f", [0.0]*(16_384//4) )
		self.ubo = gl_buffer.BufferGPU( 16_384, gl_buffer.BUFFER_HINT.DynamicToGPU )

		if config.render_shadows and config.renderadv_shadow_mode==1:
			self.light_camera = camera.PerspectiveCamera( (config.renderadv_smap_res,)*2, 0.0, 0.0,Inf )
			self.fbo_shadowmap = gl_fbo.FBO2D( self.light_camera.res, 1 )
			self.shadowmap = gl_texture.Texture2D( self.fbo_shadowmap.res, GL_DEPTH_COMPONENT16 )
			self.shadowmap.set_filters( 1, 1 )
			self.shadowmap.set_depthmap_compare()
			self.fbo_shadowmap.set_attachment_depthstencil(self.shadowmap)

		self.reload_shaders()

		self.reload_screen_buffers()

	def reload_screen_buffers( self:Self ):
		self.fbo_gbuf = gl_fbo.FBO2D( config.window_res, config.window_msaa_nspp )
		self.gbuf_diff     = gl_texture.Texture2D.from_empty( config.window_res, GL_RGB16            , config.window_msaa_nspp )
		self.gbuf_spec     = gl_texture.Texture2D.from_empty( config.window_res, GL_RGB16            , config.window_msaa_nspp )
		self.gbuf_shin_bl  = gl_texture.Texture2D.from_empty( config.window_res, GL_RG16F            , config.window_msaa_nspp )
		self.gbuf_emiss    = gl_texture.Texture2D.from_empty( config.window_res, GL_RGB16F           , config.window_msaa_nspp )
		self.gbuf_normal   = gl_texture.Texture2D.from_empty( config.window_res, GL_RGB8_SNORM       , config.window_msaa_nspp )
		self.gbuf_depth    = gl_texture.Texture2D.from_empty( config.window_res, GL_DEPTH_COMPONENT24, config.window_msaa_nspp )
		self.fbo_gbuf.set_attachment_color( self.gbuf_diff   , 0 )
		self.fbo_gbuf.set_attachment_color( self.gbuf_spec   , 1 )
		self.fbo_gbuf.set_attachment_color( self.gbuf_shin_bl, 2 )
		self.fbo_gbuf.set_attachment_color( self.gbuf_emiss  , 3 )
		self.fbo_gbuf.set_attachment_color( self.gbuf_normal , 4 )
		self.fbo_gbuf.set_attachment_depthstencil(self.gbuf_depth)

		self.fbo_light = gl_fbo.FBO2D(config.window_res)
		self.buf_light = gl_texture.Texture2D.from_empty( config.window_res, GL_R16F )
		self.fbo_light.set_attachment_color( self.buf_light, 0 )

		gl_fbo.FBO2D.set_readdraw(None)

	def reload_shaders( self:Self ):
		hostdefs = f"""
		#define NSPP {config.window_msaa_nspp}u
		#define SHADMODE_SHADVOL 0
		#define SHADMODE_SMAP    1
		#define SHADMODE {config.renderadv_shadow_mode}
		#define SMAP_PCF_RT {config.renderadv_smap_pcf_rt}
		#define GBUF_TEXAA {config.renderadv_gbuf_texaa}
		"""
		ubo_str = _read_file( config.MAIN_DIR+"data/shader/ubo.glsl" )

		def shader_source( relpath:str ):
			source = _read_file( config.MAIN_DIR+relpath )
			source = source.replace( "#include <hostdefs>\n", hostdefs )
			source = source.replace( "#include \"data/shader/ubo.glsl\"\n", ubo_str )
			return source
		def shader_program( name:str, **kwargs ):
			shaders = []
			if "vert" in kwargs:
				shaders.append( gl_program.ShaderVert( shader_source(kwargs["vert"]) ) )
			if "geom" in kwargs:
				shaders.append( gl_program.ShaderGeom( shader_source(kwargs["geom"]) ) )
			if "frag" in kwargs:
				shaders.append( gl_program.ShaderFrag( shader_source(kwargs["frag"]) ) )
			prog = gl_program.Program( shaders, kwargs["attribs"],kwargs["outputs"] )
			_dump_logs( prog, name )
			return prog

		if config.render_shadows and config.renderadv_shadow_mode==0:
			self.prog_extrude_shadow = shader_program( "Shadow Generation",
				vert = "data/shader/extrude_shadow.vert",
				geom = "data/shader/extrude_shadow.geom",
				frag = "data/shader/extrude_shadow.frag",
				attribs={ "attrib_vert":0 }, outputs={ "frag_color":0 }
			)
		if config.render_shadows and config.renderadv_shadow_mode==1:
			self.prog_shadowmap = shader_program( "Shadowmap",
				vert = "data/shader/shadowmap.vert",
				attribs={ "attrib_vert":0 }, outputs={}
			)
		self.prog_gbuf = shader_program( "G-Buffer",
			vert = "data/shader/gbuf.vert",
			frag = "data/shader/gbuf.frag",
			attribs={ "attrib_vert":0, "attrib_norm":1, "attrib_texc":2, "attrib_vcol":3 },
			outputs={ "frag_diff_a":0, "frag_spec":1, "frag_shin_bl":2, "frag_emiss":3, "frag_normal":4 }
		)
		self.prog_shade = shader_program( "Lighting / Composite",
			vert = "data/shader/passthru_vert2f.vert",
			frag = "data/shader/shade_lighting.frag",
			attribs={ "attrib_vert":0 }, outputs={ "frag_color":0 }
		)
		self.prog_color = shader_program( "Draw Color",
			vert = "data/shader/passthru_vert2f.vert",
			frag = "data/shader/color.frag",
			attribs={ "attrib_vert":0 }, outputs={ "frag_color":0 }
		)
		self.prog_tex2D = shader_program( "Draw Texture",
			vert = "data/shader/passthru_vert2f.vert",
			frag = "data/shader/draw_buffer.frag",
			attribs={ "attrib_vert":0 }, outputs={ "frag_color":0 }
		)

	def prepare_level( self:Self ):
		def _vao( verts:list[float], norms:list[float], texcs:list[float] ):
			vao = gl_geom.VAO()
			vao.set_attrib(gl_geom.VertAttribInfo( 0, gl_buffer.BufferGPU(verts), 3,GL_FLOAT,12 ))
			vao.set_attrib(gl_geom.VertAttribInfo( 1, gl_buffer.BufferGPU(norms), 3,GL_FLOAT,12 ))
			vao.set_attrib(gl_geom.VertAttribInfo( 2, gl_buffer.BufferGPU(texcs), 2,GL_FLOAT, 8 ))
			vao.finalize()
			return vao

		#Floor
		SC = config.FLOOR_RADIUS
		vertdata = [ -SC,-SC,0.0, SC,-SC,0.0, -SC,SC,0.0, SC,SC,0.0 ]
		normdata = [ 0,0,1, 0,0,1, 0,0,1, 0,0,1 ]
		texcdata = [ -SC,-SC, SC,-SC, -SC,SC, SC,SC ]
		self.msh_floor = gl_geom.Mesh( _vao(vertdata,normdata,texcdata), GL_TRIANGLE_STRIP,4 )

		#Floor special tiles
		if len(driver.level.special_tiles) > 0:
			vertdata=[]; texcdata=[]
			for tile in driver.level.special_tiles:
				s0,t0, s1,t1 = render_lowlevel.get_special_tile_texcs(tile.type)
				x0,y0=tile.coord.xy; x1=x0+1.0; y1=y0+1.0
				vertdata += [ x0,y0,0, x1,y0,0, x1,y1,0, x0,y0,0, x1,y1,0, x0,y1,0 ]
				texcdata += [ s0,t0,   s1,t0,   s1,t1,   s0,t0,   s1,t1,   s0,t1   ]
			normdata = [ 0,0,1 ] * ( 6 * len(driver.level.special_tiles) )
			self.msh_special_tiles = gl_geom.Mesh(
				_vao(vertdata,normdata,texcdata), GL_TRIANGLES,len(vertdata)//3
			)
		else:
			self.msh_special_tiles = None

		#Walls
		def _msh_walls( type:level.WALL ):
			walls = [ wall for wall in driver.level.walls if wall.type==type ]
			if len(walls) == 0: return None

			vertdata=[]; texcdata=[]; normdata=[]
			def _vert( k:int, delta:Vector2 ):
				nonlocal vertdata, texcdata, normdata
				vert, norm, texc = render_lowlevel.WALL_GEOM_QUADS[k]
				vertdata += [ vert[0]+delta.x, vert[1]+delta.y, vert[2] ]
				texcdata += list(texc)
				normdata += list(norm)
			def _quad( k:int, delta:Vector2 ):
				_vert( 4*k, delta ); _vert( 4*k+1, delta ); _vert( 4*k+2, delta )
				_vert( 4*k, delta ); _vert( 4*k+2, delta ); _vert( 4*k+3, delta )
			for wall in walls:
				if not wall.hidden_sides[0]: _quad( 0, wall.coord.xy )
				if not wall.hidden_sides[1]: _quad( 1, wall.coord.xy )
				if not wall.hidden_sides[2]: _quad( 2, wall.coord.xy )
				#if not wall.hidden_sides[3]: _quad( 3, wall.coord.xy ) #not visible
				_quad( 4, wall.coord.xy )
			return gl_geom.Mesh( _vao(vertdata,normdata,texcdata), GL_TRIANGLES,len(vertdata)//3 )
		self.msh_walls_hedge = _msh_walls(level.WALL.HEDGE)
		self.msh_walls_metal = _msh_walls(level.WALL.METAL)
		self.msh_walls_laser = _msh_walls(level.WALL.LASER)

		if config.render_shadows and config.renderadv_shadow_mode==1:
			cam = self.light_camera
			cam.pos,cam.center, cam.vfov_rad, cam.clip_near,cam.clip_far = compute_frustrum_bound(
				light_pos,
				Vector3(driver.level.bound_lo,0.0), Vector3(driver.level.bound_hi,5.0)
			)

		self.pass_update_ubos_onload()

	#Update uniform data (per level or resize)
	def pass_update_ubos_onload( self:Self ):
		mapping = self.ubo.map( True,True, 0,4 )

		mapping[32].vec4f = Vector4( 0.0,0.0, config.window_res[0],config.window_res[1] ) #viewport
		mapping[40].vec4f = Vector4( light_pos, NaN )

		mapping[44].f = config.camera_near
		mapping[45].f = config.camera_far
		mapping[46].f = config.render_shadow_darkness
		if config.render_shadows and config.renderadv_shadow_mode==1:
			mapping[47].f = 1.0 / self.shadowmap.res[0]

			matr_PV_light = self.light_camera.get_projview_3D()
			mapping[48].matr4x4f = matr_PV_light.elems

		mapping[64].matr3x4f = Matrix.get_identity(( 3, 4 )).elems

	#Update uniform data (every frame)
	def pass_update_ubos_every( self:Self ):
		render_common.camera.res = tuple(config.window_res)
		matr_PV = render_common.camera.get_projview_3D()
		matr_PV_inv = Matrix.inverse(matr_PV)

		mapping = self.ubo.map( True,True, 0,4 )

		mapping[ 0].matr4x4f = matr_PV    .elems
		mapping[16].matr4x4f = matr_PV_inv.elems

		#mapping[32].vec4f = Vector4( 0.0,0.0, config.window_res[0],config.window_res[1] ) #viewport
		mapping[36].vec4f = Vector4( render_common.camera.pos, NaN )
		#mapping[40].vec4f = Vector4( light_pos, NaN )

		#mapping[44].f = config.camera_near
		#mapping[45].f = config.camera_far
		#mapping[46].f = config.render_shadow_darkness
		#if config.render_shadows and config.renderadv_shadow_mode==1:
		#	mapping[47].f = 1.0 / self.shadowmap.res[0]
		#
		#	matr_PV_light = self.light_camera.get_projview_3D()
		#	mapping[48].matr4x4f = matr_PV_light.elems

		#Model matrices.  Note that even though 3⨯4, padded to 4⨯4 because of alignment.
		#	First matr_M: identity
		#mapping[64].matr3x4f = Matrix.get_identity(( 3, 4 )).elems
		base = 80
		#	Items
		#		Time
		for item in driver.level.items:
			if item.type != level.ITEM.TPLUS60: continue
			matr_M = Matrix.get_translation( item.pos.x-0.5, item.pos.y-0.5, 0.002 )
			mapping[base].matr3x4f=matr_M.elems; base+=16
		#		Gems
		for item in driver.level.items:
			if item.type != level.ITEM.GEM: continue
			#matr_M = Matrix.get_translation(item.pos)
			#matr_M *= Matrix.get_rotation_rad( item.rot_rad, Vector3(0.05,0.15,1.0).normalized() )
			matr_M = Matrix.get_rotation_rad( item.rot_rad, Vector3(0.05,0.15,1.0).normalized() )
			Matrix.translate_ip( matr_M, item.pos )
			mapping[base].matr3x4f=matr_M.elems; base+=16
		#		Stars
		for item in driver.level.items:
			if item.type != level.ITEM.STAR: continue
			#matr_M = Matrix.get_translation(item.pos)
			#matr_M *= Matrix.get_rotation_rad( item.rot_rad, 0.2,0.1,1.0 )
			matr_M = Matrix.get_rotation_rad( item.rot_rad, 0.2,0.1,1.0 )
			Matrix.translate_ip( matr_M, item.pos )
			mapping[base].matr3x4f=matr_M.elems; base+=16
		#		Life-ups
		lifeup_radius = level.Item.LIFE_UP_SIZE_FACTOR * config.radius_self
		for item in driver.level.items:
			if item.type != level.ITEM.LIFE_UP: continue
			#matr_M = Matrix.get_translation(item.pos)
			#matr_M *= Matrix.get_rotation_rad( item.rot_rad, 0.0,0.0,1.0 )
			#matr_M *= Matrix.get_scaling( lifeup_radius, lifeup_radius, lifeup_radius )
			matr_M = Matrix.get_rotation_rad( item.rot_rad, 0.0,0.0,1.0 )
			Matrix.scale1_ip   ( matr_M, lifeup_radius )
			Matrix.translate_ip( matr_M, item.pos )
			mapping[base].matr3x4f=matr_M.elems; base+=16
		#	Marbles
		assert len(driver.level.marbles) <= 8
		for k, marble in enumerate(driver.level.marbles):
			#matr_M = Matrix.get_translation(marble.center)
			#matr_M *= marble.rot.calc_matr4x4()
			#matr_M *= Matrix.get_scaling( marble.radius, marble.radius, marble.radius )
			matr_M = marble.rot.calc_matr4x4()
			Matrix.scale1_ip   ( matr_M, marble.radius )
			Matrix.translate_ip( matr_M, marble.center )
			mapping[base].matr3x4f=matr_M.elems; base+=16
		#matr_M = Matrix.get_translation(4.0,3.0,2.0)
		#matr_M *= Matrix.get_scaling( 0.1,0.1,0.1 )
		#mapping[base].matr3x4f=matr_M.elems; base+=16

		#Vectors for particles
		assert base <= 2112
		base = 2112
		for particle in particles.particles:
			if particle.type == particles.PARTICLE.MARBLE_SELF:
				mapping[base].vec4f=Vector4( particle.pos, particle.radius ); base+=4
		for particle in particles.particles:
			if particle.type == particles.PARTICLE.MARBLE_EVIL:
				mapping[base].vec4f=Vector4( particle.pos, particle.radius ); base+=4

	#Pass 1: render G-buffer
	def pass_update_gbuffer( self:Self ):
		gl_fbo.FBO2D.set_draw(self.fbo_gbuf)
		glViewport( 0,0, config.window_res[0],config.window_res[1] )
		glClearColor( 0.0,0.0,0.0, 1.0 )
		glClear( GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT )

		gl_program.Program.use(self.prog_gbuf)
		self.prog_gbuf.pass_bind_ubo( self.ubo, 0, "UBO" )
		inst_offset = 0
		self.prog_gbuf.pass_1i( "inst_offset", inst_offset )
		self.prog_gbuf.pass_1b( "transform_with_matr_M", True )

		#Floor
		self.prog_gbuf.pass_4i( "config", 2, 1, 16*65535, 0 )
		self.prog_gbuf.pass_4f( "color_diff_a" , 0.2,0.2,0.2, 1.0 )
		self.prog_gbuf.pass_4f( "color_spec_bl", 0.2,0.2,0.2, 0.2 )
		self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.tile_floor, 0 )
		if config.is_debugging:
			self.prog_gbuf.validate_could_execute_now()
		self.msh_floor.draw()

		#Floor special tiles
		if self.msh_special_tiles:
			glDisable(GL_DEPTH_TEST)
			self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.tiles, 0 )
			self.msh_special_tiles.draw()
			glEnable(GL_DEPTH_TEST)

		#Hedge walls
		if self.msh_walls_hedge:
			self.prog_gbuf.pass_4i( "config", 2, 1, 4*65535, 0 )
			self.prog_gbuf.pass_4f( "color_diff_a" , 0.2,0.2,0.2, 1.0 )
			self.prog_gbuf.pass_4f( "color_spec_bl", 0.10,0.08,0.06, 0.2 )
			self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.wall_hedge, 0 )
			self.msh_walls_hedge.draw()

		#Metal walls
		if self.msh_walls_metal:
			self.prog_gbuf.pass_4i( "config", 2, 2, 16*65535, 0 )
			self.prog_gbuf.pass_4f( "color_diff_a" , 0.5,0.5,0.5, 1.0 )
			self.prog_gbuf.pass_4f( "color_spec_bl", 0.8,0.9,1.0, 0.7 )
			self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.wall_metal, 0 )
			self.msh_walls_metal.draw()

		#glDisable(GL_CULL_FACE)
		#self.msh_extra_caster.draw()
		#glEnable(GL_CULL_FACE)

		#Stuff with subsequent (non-identity) model matrices
		inst_offset += 1

		#Items
		#	Time
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.TPLUS60 ])
		if inst_count > 0:
			self.prog_gbuf.pass_4i( "config", 2, 1, 16*65535, 0 )
			self.prog_gbuf.pass_4f( "color_diff_a" , 0.2,0.2,0.2, 1.0 )
			self.prog_gbuf.pass_4f( "color_spec_bl", 0.2,0.2,0.2, 0.2 )
			self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.tiles, 0 )
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
			self.msh_time.draw(inst_count)
		#	Gems
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.GEM ])
		if inst_count > 0:
			self.prog_gbuf.pass_4i( "config", 3, 1, 32*65535, 0 )
			self.prog_gbuf.pass_4f( "color_diff_a" , 1.0,1.0,1.0, 0.5 )
			self.prog_gbuf.pass_4f( "color_spec_bl", 1.0,0.8,0.7, 0.8 )
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
			self.msh_gem.draw(inst_count)
		#	Stars
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.STAR ])
		if inst_count > 0:
			self.prog_gbuf.pass_4i( "config", 3, 1, 16*65535, 0 )
			self.prog_gbuf.pass_4f( "color_diff_a" , 0.25,0.25,0.25, 1.0 )
			self.prog_gbuf.pass_4f( "color_spec_bl", 1.00,0.80,0.40, 0.7 )
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
			self.msh_star.draw(inst_count)
		#	Life-up
		self.prog_gbuf.pass_4i( "config", 2, 1, 64*65535, 0 )
		self.prog_gbuf.pass_4f( "color_diff_a" , 1.0,1.0,1.0, 1.0 )
		self.prog_gbuf.pass_4f( "color_spec_bl", 1.0,1.0,1.0, 0.7 )
		self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.marble_self, 0 )
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.LIFE_UP ])
		if inst_count > 0:
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
			self.msh_marble.draw(inst_count)

		#Marbles
		if driver.level.marble_self != None:
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()
		self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.marble_evil, 0 )
		for marble in driver.level.marbles:
			if marble.type == level.MARBLE.SELF: continue
			self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()

		#Particles
		inst_offset = 0
		self.prog_gbuf.pass_1b( "transform_with_matr_M", False )
		#	Self
		inst_count = sum([ 1 for particle in particles.particles if particle.type==particles.PARTICLE.MARBLE_SELF ])
		self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.marble_self, 0 )
		self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)
		#	Evil
		inst_count = sum([ 1 for particle in particles.particles if particle.type==particles.PARTICLE.MARBLE_EVIL ])
		self.prog_gbuf.pass_bind_tex( "tex_diff", render_common.textures.marble_evil, 0 )
		self.prog_gbuf.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)

		#Emissive objects
		glDepthMask(GL_FALSE)
		inst_offset = 0
		self.prog_gbuf.pass_1i( "inst_offset", inst_offset )
		self.prog_gbuf.pass_1b( "transform_with_matr_M", True )
		self.prog_gbuf.pass_4i( "config", 0, 0, 0, 1 )
		self.prog_gbuf.pass_3f( "color_emiss", 1.0,0.3,0.3 )
		if driver.level.lasers_on:
			self.msh_walls_laser.draw()
		glDepthMask(GL_TRUE)

	#Pass 2 (opt A): render shadow casters into lighting buffer
	def pass_update_lighting_buffer( self:Self ):
		""" Render lighting buffer.

		First, render all shadow casters with a geometry shader that extrudes a shadow volume down
		to the ground plane.  Shadow volumes are only generated from back-faces (light's
		perspective), while only the back-faces (camera's perspective) are rasterized.

		In the fragment shader, we test the covered pixel to see if the position in the G-buffer is
		under the triangle (more precisely, if the 2D projection of the point is inside the 2D
		projection of the triangle).  That test can be with a circle to get soft shadows.

		Finally, the visibility is blended, by a minimum operation, into the lighting buffer.
		"""

		gl_fbo.FBO2D.set_draw(self.fbo_light)
		glClearColor( 1.0,1.0,1.0, 1.0 )
		glClear(GL_COLOR_BUFFER_BIT)

		if not config.render_shadows: return

		glCullFace(GL_FRONT)
		glDisable(GL_DEPTH_TEST)
		glBlendEquation(GL_MIN)

		gl_program.Program.use(self.prog_extrude_shadow)
		self.prog_extrude_shadow.pass_bind_tex( "gbuf_depth", self.gbuf_depth, 2 )
		self.prog_extrude_shadow.pass_bind_ubo( self.ubo, 0, "UBO" )
		inst_offset = 0
		self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset )
		self.prog_extrude_shadow.pass_1b( "transform_with_matr_M", True )
		if config.is_debugging:
			self.prog_extrude_shadow.validate_could_execute_now()

		#self.msh_extra_caster.draw()

		if self.msh_walls_hedge:
			self.msh_walls_hedge.draw()
		if self.msh_walls_metal:
			self.msh_walls_metal.draw()

		#Stuff with subsequent (non-identity) model matrices
		inst_offset += 1

		#Items
		#	Time
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.TPLUS60 ])
		inst_offset += inst_count
		#	Gems
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.GEM ])
		self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_gem.draw(inst_count)
		#	Stars
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.STAR ])
		self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_star.draw(inst_count)
		#	Life-ups
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.LIFE_UP ])
		self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)

		#Marbles
		if driver.level.marble_self != None:
			self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()
		for marble in driver.level.marbles:
			if marble.type == level.MARBLE.SELF: continue
			self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()

		#Particles
		inst_offset = 0
		self.prog_extrude_shadow.pass_1b( "transform_with_matr_M", False )
		inst_count = sum( [
			1 for particle in particles.particles
			if particle.type in ( particles.PARTICLE.MARBLE_SELF, particles.PARTICLE.MARBLE_EVIL )
		] )
		self.prog_extrude_shadow.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)

		glBlendEquation(GL_FUNC_ADD)
		glEnable(GL_DEPTH_TEST)
		glCullFace(GL_BACK)
	#Pass 2 (opt B): update light depthmap
	def pass_update_depthmap( self:Self ):
		if not config.render_shadows: return

		gl_fbo.FBO2D.set_draw(self.fbo_shadowmap)
		glViewport( 0,0, self.light_camera.res[0],self.light_camera.res[1] )
		glClear(GL_DEPTH_BUFFER_BIT)

		#glCullFace(GL_FRONT) #Need good contact shadows

		gl_program.Program.use(self.prog_shadowmap)
		self.prog_shadowmap.pass_bind_ubo( self.ubo, 0, "UBO" )
		inst_offset = 0
		self.prog_shadowmap.pass_1i( "inst_offset", inst_offset )
		self.prog_shadowmap.pass_1b( "transform_with_matr_M", True )
		if config.is_debugging:
			self.prog_shadowmap.validate_could_execute_now()

		#self.msh_extra_caster.draw()

		if self.msh_walls_hedge:
			self.msh_walls_hedge.draw()
		if self.msh_walls_metal:
			self.msh_walls_metal.draw()

		#Stuff with subsequent (non-identity) model matrices
		inst_offset += 1

		#Items
		#	Time
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.TPLUS60 ])
		inst_offset += inst_count
		#	Gems
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.GEM ])
		self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_gem.draw(inst_count)
		#	Stars
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.STAR ])
		self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_star.draw(inst_count)
		#	Life-ups
		inst_count = sum([ 1 for item in driver.level.items if item.type==level.ITEM.LIFE_UP ])
		self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)

		#Marbles
		if driver.level.marble_self != None:
			self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()
		for marble in driver.level.marbles:
			if marble.type == level.MARBLE.SELF: continue
			self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += 1
			self.msh_marble.draw()

		#Particles
		inst_offset = 0
		self.prog_shadowmap.pass_1b( "transform_with_matr_M", False )
		inst_count = sum( [
			1 for particle in particles.particles
			if particle.type in ( particles.PARTICLE.MARBLE_SELF, particles.PARTICLE.MARBLE_EVIL )
		] )
		self.prog_shadowmap.pass_1i( "inst_offset", inst_offset ); inst_offset += inst_count
		self.msh_marble.draw(inst_count)

		#glCullFace(GL_BACK)

	#Pass 3: shading
	def pass_shade( self:Self ):
		gl_fbo.FBO2D.set_draw(None)
		glViewport( 0,0, config.window_res[0],config.window_res[1] )
		#glClearColor( 1.0,1.0,1.0, 1.0 )
		glClear(GL_COLOR_BUFFER_BIT)

		glBlendFunc( GL_ONE, GL_ZERO )

		if config.RENDERADV_DISPLAY_MODE == 0: #game

			gl_program.Program.use(self.prog_shade)
			self.prog_shade.pass_bind_ubo( self.ubo, 0, "UBO" )
			self.prog_shade.pass_bind_tex( "gbuf_diff"   , self.gbuf_diff   , 0 )
			self.prog_shade.pass_bind_tex( "gbuf_spec"   , self.gbuf_spec   , 1 )
			self.prog_shade.pass_bind_tex( "gbuf_shin_bl", self.gbuf_shin_bl, 2 )
			self.prog_shade.pass_bind_tex( "gbuf_emiss"  , self.gbuf_emiss  , 3 )
			self.prog_shade.pass_bind_tex( "gbuf_normal" , self.gbuf_normal , 4 )
			self.prog_shade.pass_bind_tex( "gbuf_depth"  , self.gbuf_depth  , 5 )
			if   config.render_shadows and config.renderadv_shadow_mode==0:
				self.prog_shade.pass_bind_tex( "buf_light", self.buf_light, 6 )
			elif config.render_shadows and config.renderadv_shadow_mode==1:
				self.prog_shade.pass_bind_tex( "shadowmap", self.shadowmap, 6 )
			if config.is_debugging:
				self.prog_shade.validate_could_execute_now()

			self.msh_fs_tri.draw()

		elif config.RENDERADV_DISPLAY_MODE == 1: #vis normal
			gl_program.Program.use(self.prog_tex2D)
			self.prog_tex2D.pass_bind_tex( "tex", self.gbuf_normal, 0 )
			if config.is_debugging:
				self.prog_tex2D.validate_could_execute_now()

			self.msh_fs_tri.draw()

		#else disabled

		glBlendFunc( GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA )

	def draw_quad( self:Self,
		center:tuple[float,float], radius_x:float,radius_y:float, color:tuple[float,float,float,float]
	):
		gl_program.Program.use(self.prog_color)
		self.prog_color.pass_4f( "color", color )

		mapping = self.msh_quad_vbo.map( False,True, 0,8 )
		l = 2.0*(center[0]-radius_x)/config.window_res[0] - 1.0
		r = 2.0*(center[0]+radius_x)/config.window_res[0] - 1.0
		b = 2.0*(center[1]-radius_y)/config.window_res[1] - 1.0
		t = 2.0*(center[1]+radius_y)/config.window_res[1] - 1.0
		mapping[0].vec2f = ( l, b )
		mapping[1].vec2f = ( r, b )
		mapping[2].vec2f = ( l, t )
		mapping[3].vec2f = ( r, t )
		del mapping

		self.msh_quad.draw()

renderer:Renderer|None = None

def ensure_init():
	global renderer
	if not renderer:
		renderer = Renderer()

def ensure_deinit():
	global renderer
	if renderer:
		del renderer
		renderer = None



def render_frame( fps:float ):
	assert renderer
	if renderer.fbo_gbuf.res != tuple(config.window_res):
		renderer.pass_update_ubos_onload()
		renderer.reload_screen_buffers()

	renderer.pass_update_ubos_every()

	renderer.pass_update_gbuffer()

	if   config.render_shadows and config.renderadv_shadow_mode==0:
		renderer.pass_update_lighting_buffer()
	elif config.render_shadows and config.renderadv_shadow_mode==1:
		renderer.pass_update_depthmap()

	glDisable(GL_DEPTH_TEST)
	renderer.pass_shade()

	render_common.render_hud(fps)

	glEnable(GL_DEPTH_TEST)
