import array
import os
import struct
import sys
from typing import Any, Iterable, Self

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

#"pip install pygame-ce"
with open( os.devnull, "w" ) as f:
	oldstdout=sys.stdout; sys.stdout=f; import pygame; sys.stdout=oldstdout

#"pip install numpy"
try:
	import numpy as np
	_GLFONT_HAS_NP = True
except ModuleNotFoundError:
	_GLFONT_HAS_NP = False

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

try:
	#(other basecode files, optional)
	from . import gl_buffer
	from . import gl_geom
	from . import gl_program
	_GLFONT_HAS_GEOM = True
except ModuleNotFoundError:
	_GLFONT_HAS_GEOM = False



class Glyph:
	METADATA_SZ = 8

	__slots__ = ( "codepoint", "surf", "bearing","advance", "s_lo","t_lo","s_hi","t_hi" )

	def __init__( self:Self,
		codepoint:int,
		surf_24bit:pygame.Surface, bearing:tuple[int,int], advance:int
	):
		self.codepoint = codepoint

		#8-bit surfaces also store a palette, so they're probably bigger as well as slower
		assert surf_24bit.get_bitsize() == 24
		self.surf = surf_24bit
		#https://freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-3
		self.bearing = bearing
		self.advance = advance

		self.s_lo=0.0; self.t_lo=0.0
		self.s_hi=1.0; self.t_hi=1.0

	@staticmethod
	def deserialize_metadata( data:bytes,offset:int, surf:pygame.Surface ):
		b0,b1,b2 = struct.unpack_from( "<BBB", data,offset )
		codepoint = (b2<<16) | (b1<<8) | b0
		x_bearing,y_bearing, advance, w,h = struct.unpack_from( "<bbbBB", data,offset+3 )
		return Glyph( codepoint, surf,(x_bearing,y_bearing),advance ), (w,h)
	@staticmethod
	def serialize_metadata( glyph:Self ):
		b0=glyph.codepoint&0xFF; b1=(glyph.codepoint>>8)&0xFF; b2=glyph.codepoint>>16
		return struct.pack( "<BBBbbbBB",
			b0,b1,b2, glyph.bearing[0],glyph.bearing[1],glyph.advance, glyph.surf.get_width(),glyph.surf.get_height()
		)

	def __str__( self:Self ):
		s = f"{{ {self.codepoint}"
		if chr(self.codepoint).isprintable():
			s += f"=\"{chr(self.codepoint)}\""
		s += f", bear=⟨{self.bearing[0]},{self.bearing[1]}⟩, "
		s += f"res=⟨{self.surf.get_width()},{self.surf.get_height()}⟩, adv={self.advance} }}"
		return s



class Font:
	__slots__ = (
		"font", "font_ascent", "font_descent",
		"_glyphs",
		"_surf", "_tex",
		"_geom", "_program", "_ubo_chars","_ubo_chars_arr", "_ubo_font","_ubo_font_chardata_inds",
		"_dirty"
	)
	CHUNK_NUM_CHARS_MAX = 16_384 // 8

	def __init__( self:Self, font:pygame.Font|None ):
		self.font:pygame.Font|None = font
		self.font_ascent :int = font.get_ascent () if font else -1
		self.font_descent:int = font.get_descent() if font else -1

		self._glyphs:dict[ int, Glyph ] = {}

		self._surf:pygame.Surface      |None = None
		self._tex :gl_texture.Texture2D|None = None
		if _GLFONT_HAS_GEOM and glDrawElementsInstanced:
			self._geom                  :gl_geom.Mesh|None = None
			self._program               :gl_program.Program|None = None
			self._ubo_chars             :gl_buffer.BufferGPU|None = None
			self._ubo_chars_arr         :Any|None = None
			self._ubo_font              :gl_buffer.BufferGPU|None = None
			self._ubo_font_chardata_inds:dict[int,int] = {} #codepoint onto char-data-index in font UBO
		self._dirty = True

	def set_backing( self:Self, font:pygame.Font ):
		self.font = font
		self.font_ascent  = font.get_ascent ()
		self.font_descent = font.get_descent()

	@staticmethod
	def _crop_glyph( glyph:Glyph, rect:pygame.Rect|None=None, shrink_advance:bool=False ):
		if rect == None:
			rect = gl_texture.surface_calc_data_rect( glyph.surf, lambda color: color[0]!=0 )
		glyph.bearing = ( glyph.bearing[0]+rect.x, glyph.bearing[1]-rect.y )
		if shrink_advance:
			glyph.advance -= glyph.surf.get_width() - rect.w
		glyph.surf = glyph.surf.subsurface(rect)

	@staticmethod
	def create_from_font_file( font_path:str, size_px:int ):
		return Font( pygame.font.Font( font_path, size_px ) )
	@staticmethod
	def create_from_sysfont(
		font_paths: str | bytes | Iterable[str|bytes] | None,
		size_px: int,
		bold:bool=False, italic:bool=False
	):
		return Font( pygame.font.SysFont( font_paths, size_px, bold,italic ) )
	@staticmethod
	def load_from_font_img(
		fontimg_path:str,
		ncells:tuple[int,int], codepoints:int|Iterable[int]=0, data_ichan:int=0,
		baseline_shift:int = 0
	):
		""" Create from a bitmap font image.

		Note does not automatically crop glyphs.  This is possible, but it's not clear how the
		advance should be updated, if at all.  To handle this, call `.crop_glyphs(⋯)` afterward with
		the desired parameters.

		Args:
			fontimg_path:
				Path to the font image.
			ncells:
				Number of cells on each axis.
			codepoints:
				How to interpret codepoints from the image.  If an integer, then is the first
				codepoint in the image, and is incremented row-major for the whole image.  If
				iterable, then is a list of codepoints to extract row-major from the image (may be
				less than the maximum number of cells).  If " " is not generated, a blank glyph will
				be created automatically.
			data_ichan:
				The index of the image channel containing the data (default red).
			baseline_shift:
				The baseline cannot be detected, so by default it is at the bottom.  It can be
				shifted upward by this amount.

		Returns:
			The new font.
		"""

		assert ncells[0]>0 and ncells[1]>0
		assert data_ichan in ( 0,1,2, 3 )

		img = pygame.image.load(fontimg_path).convert(32)
		w, h = img.get_size()
		res_tile = [ w//ncells[0], h//ncells[1] ]

		if type(codepoints) == int:
			codepoints = range( codepoints, codepoints+ncells[1]*ncells[0], 1 )
		assert len(codepoints) <= ncells[1]*ncells[0]

		font = Font(None)

		i=0; j=0
		for codepoint in codepoints:
			imgsub = img.subsurface(( i*res_tile[0],j*res_tile[1], res_tile[0],res_tile[1] ))

			glyph_surf = pygame.Surface( res_tile, depth=24 )
			dst = pygame.surfarray.pixels3d(glyph_surf)
			if data_ichan < 3: src_arr = pygame.surfarray.pixels3d    (imgsub)[:,:,data_ichan]
			else             : src_arr = pygame.surfarray.pixels_alpha(imgsub)[:,:           ]
			dst[:,:,0] = src_arr
			dst[:,:,1] = src_arr
			dst[:,:,2] = src_arr

			font._glyphs[codepoint] = Glyph( codepoint, glyph_surf,(0,res_tile[1]-baseline_shift),res_tile[0] )

			i += 1
			if i == ncells[0]:
				i=0; j+=1

		if ord(" ") not in font._glyphs.keys():
			glyph_surf = pygame.Surface( res_tile, depth=24 )
			font._glyphs[codepoint] = Glyph( codepoint, glyph_surf,(0,res_tile[1]-baseline_shift),res_tile[0] )

		return font
	@staticmethod
	def load_from_font_map( fontmap_path:str ):
		font = Font(None)

		img = pygame.image.load(fontmap_path)
		def _b( i:int, j:int ): return img.get_at(( i, j ))[0]

		res_tile = struct.unpack_from( "<HH", bytes([ _b(0,0),_b(1,0), _b(2,0),_b(3,0) ]) )
		num_pixels_tile = res_tile[1] * res_tile[0]

		i=0; j=0
		ii=4; jj=0
		def _next():
			nonlocal i,j, ii,jj, nx
			b = _b( i*res_tile[0]+ii, j*res_tile[0]+jj )
			ii += 1
			if ii == res_tile[0]:
				ii=0; jj+=1
				if jj == res_tile[1]:
					jj=0; i+=1
					if i == nx:
						i=0; j+=1
			return b
		nx, ny = struct.unpack_from( "<HH", bytes([ _next(),_next(), _next(),_next() ]) )
		font.font_ascent  = struct.unpack_from( "<h", bytes([ _next(), _next() ]) )[0]
		font.font_descent = struct.unpack_from( "<h", bytes([ _next(), _next() ]) )[0]
		num_glyphs = struct.unpack_from( "<I", bytes([ _next(), _next(), _next(), _next() ]) )[0]

		len_metadata = 16 + num_glyphs*Glyph.METADATA_SZ
		num_tiles_metadata = ( len_metadata + num_pixels_tile-1 ) // num_pixels_tile

		glyphs_metadata = bytes([ _next() for _ in range(num_glyphs*Glyph.METADATA_SZ) ])

		i=num_tiles_metadata%nx; j=num_tiles_metadata//nx
		for ig in range(num_glyphs):
			surf = img.subsurface(( i*res_tile[0],j*res_tile[1], res_tile[0],res_tile[1] )).copy()
			glyph, res = Glyph.deserialize_metadata( glyphs_metadata,ig*14, surf )
			font._glyphs[glyph.codepoint] = glyph
			i += 1
			if i == nx:
				i=0; j+=1

		return font
	@staticmethod
	def save_to_font_img( glfont:Self, fontimg_path:str ):
		#Note don't necessarily have glyphs for everything
		codepoint_min = min( glfont._glyphs.keys() )
		codepoint_max = max( glfont._glyphs.keys() )
		codepoints = range( codepoint_min, codepoint_max+1, 1 )

		min_lo = (  999_999,  999_999 )
		max_hi = ( -999_999, -999_999 )
		for glyph in glfont._glyphs.values():
			w, h = glyph.surf.get_size()
			if w==0 or h==0: continue
			lo = ( glyph.bearing[0]  , glyph.bearing[1]-h )
			hi = ( glyph.bearing[0]+w, glyph.bearing[1]   )
			min_lo = ( min(min_lo[0],lo[0]), min(min_lo[1],lo[1]) )
			max_hi = ( max(max_hi[0],hi[0]), max(max_hi[1],hi[1]) )
		res_tile = ( max_hi[0]-min_lo[0], max_hi[1]-min_lo[1] )

		nx=1; ny=1
		while True:
			if ny*nx >= len(codepoints): break
			nx *= 2
			if ny*nx >= len(codepoints): break
			ny *= 2

		img = pygame.Surface(( nx*res_tile[0], ny*res_tile[1] ))
		i=0; j=0
		for codepoint in codepoints:
			if codepoint in glfont._glyphs.keys():
				glyph = glfont._glyphs[codepoint]
				img.blit( glyph.surf, (
					i*res_tile[0] + glyph.bearing[0] - min_lo[0],
					j*res_tile[1] - glyph.bearing[1] + max_hi[1]
				) )
			i += 1
			if i == nx:
				i=0; j+=1

		img = gl_texture.surface_grayscale_convert8(img)
		pygame.image.save( img, fontimg_path )

		return img, (nx,ny), codepoints, 0
	@staticmethod
	def save_to_font_map( glfont:Self, fontmap_path:str ) -> pygame.Surface:
		glfont._regen_if_necessary()
		pygame.image.save( glfont._surf, fontmap_path )
		return glfont._surf
	#Load and then save fontmap, useful for testing
	@staticmethod
	def load_and_save_font_map( fontmap_path_in:str, fontmap_path_out:str ):
		font = Font.load_from_font_map(fontmap_path_in)
		Font.save_to_font_map( font, fontmap_path_out )

	def ensure( self:Self, text:str ):
		for ch in text:
			ich = ord(ch)
			if ich not in self._glyphs.keys():
				self.rerender(ich)
				self._dirty = True

	def crop_glyphs( self:Self,
		rect:pygame.Rect|None=None, codepoints:list[int]|None=None,
		shift_bearings:bool=True, shrink_advances:bool=False
	):
		if codepoints == None:
			codepoints = [ codepoint for codepoint in self._glyphs.keys() ]
		for codepoint in codepoints:
			glyph = self._glyphs[codepoint]
			Font._crop_glyph( glyph, rect, shrink_advances )
		if shift_bearings:
			bearing_min_x = 999_999
			for codepoint in codepoints:
				w, h = self._glyphs[codepoint].surf.get_size()
				if w==0 or h==0: continue
				bearing_x = self._glyphs[codepoint].bearing[0]
				bearing_min_x = min( bearing_min_x, bearing_x )
			for codepoint in codepoints:
				bearing = self._glyphs[codepoint].bearing
				self._glyphs[codepoint].bearing = ( bearing[0]-bearing_min_x, bearing[1] )

	def rerender( self:Self, codepoint:int, crop_glyphs_tight:bool=True ):
		if self.font == None:
			raise RuntimeError("No backing available, as font was loaded.  Use `.set_backing(⋯)`.")

		ch = chr(codepoint)
		if ch.isprintable():
			glyph_surf = self.font.render( ch, True, (255,255,255), (0,0,0) ).convert(24)
			#The bounding box seems somewhat wrong (e.g. sometimes `minx`/`miny` are negative), but we
			#	have to at least use the advance.
			#	See also: https://freetype.org/freetype2/docs/glyphs/glyphs-3.html#section-3

			#minx,maxx,miny,maxy, advance = self.font.metrics(ch)[0]
			advance = self.font.metrics(ch)[0][4]

			glyph = Glyph( codepoint, glyph_surf,(0,self.font_ascent),advance )
			if crop_glyphs_tight: Font._crop_glyph(glyph)
			#pygame.image.save( glyph.surf, f"font_render{ch}_rect({rect.x},{rect.y},{rect.w},{rect.h}).png" )
		else:
			glyph_surf = pygame.Surface( (0,0), depth=24 )
			glyph = Glyph( codepoint, glyph_surf,(0,0),0 )
		#if ch == "T":
		#	glyph.surf.fill((255,0,0))
		#	pygame.draw.rect(glyph.surf,(0,255,0),(1,1,glyph.surf.get_width()-2,glyph.surf.get_height()-2),0)

		self._glyphs[codepoint] = glyph

	def _regen_if_necessary( self:Self ):
		if not self._dirty: return

		glyphs = list( self._glyphs.values() )
		glyphs.sort( key = lambda glyph: glyph.codepoint )
		num_glyphs = len(glyphs)

		res_tile = ( 4, 0 ) #Must be at least 4 for tile res
		for glyph in glyphs:
			w, h = glyph.surf.get_size()
			res_tile = ( max(res_tile[0],w), max(res_tile[1],h) )
		num_pixels_tile = res_tile[1] * res_tile[0]

		metadata = struct.pack( "<HHHHhhI", #nx,ny filled in later
			res_tile[0],res_tile[1], 0,0, self.font_ascent,self.font_descent, num_glyphs
		)
		for glyph in glyphs: metadata += Glyph.serialize_metadata(glyph)
		num_tiles_metadata = ( len(metadata) + num_pixels_tile-1 ) // num_pixels_tile

		num_tiles = num_tiles_metadata + num_glyphs
		nx=1; ny=1
		while True:
			if ny*nx >= num_tiles: break
			nx *= 2
			if ny*nx >= num_tiles: break
			ny *= 2
		#metadata[4:8] = struct.pack( "<HH", nx, ny )
		metadata = metadata[0:4] + struct.pack( "<HH", nx, ny ) + metadata[8:]

		self._surf = pygame.Surface(( nx*res_tile[0], ny*res_tile[1] ))
		i=0; j=0
		ii=0; jj=0
		for b in metadata:
			self._surf.set_at( (i*res_tile[0]+ii,j*res_tile[1]+jj), (int(b),)*3 )
			ii += 1
			if ii == res_tile[0]:
				ii=0; jj+=1
				if jj == res_tile[1]:
					jj=0; i+=1
					if i == nx:
						i=0; j+=1
		i=num_tiles_metadata%nx; j=num_tiles_metadata//nx
		w, h = self._surf.get_size()
		for glyph in glyphs:
			pos = ( i*res_tile[0], j*res_tile[1] )
			self._surf.blit( glyph.surf, pos )
			glyph.s_lo =        pos[0]                         /w
			glyph.t_lo = 1.0 - (pos[1]+glyph.surf.get_height())/h
			glyph.s_hi =       (pos[0]+glyph.surf.get_width ())/w
			glyph.t_hi = 1.0 -  pos[1]                         /h

			i += 1
			if i == nx:
				i=0; j+=1

		self._surf = gl_texture.surface_grayscale_convert8(self._surf)

		alpha_surf = pygame.Surface( self._surf.get_size(), pygame.SRCALPHA )
		alpha_surf.fill(( 255,255,255, 0 ))
		pygame.surfarray.pixels_alpha(alpha_surf)[:,:] = pygame.surfarray.pixels2d(self._surf)[:,:]

		self._tex = gl_texture.Texture2D.from_surf(alpha_surf)
		self._tex.set_filters( 2, 1 )
		self._tex.regen_mips()

		if _GLFONT_HAS_GEOM and glDrawElementsInstanced:
			if self._geom == None:
				vbo = gl_buffer.BufferGPU([ 0.0,0.0, 1.0,0.0, 0.0,1.0, 1.0,1.0 ])
				vao = gl_geom.VAO()
				vao.set_attrib(gl_geom.VertAttribInfo( 0, vbo, 2,GL_FLOAT, 8 ))
				vao.finalize()
				self._geom = gl_geom.Mesh( vao, GL_TRIANGLE_STRIP,4 )

				#UBO can store at least 16kiB, and 16 KiB is small today, so that's a good buffer
				#	size for character parameter uploads.
				assert Font.CHUNK_NUM_CHARS_MAX == 16_384 // 8
				self._ubo_chars = gl_buffer.BufferGPU( 16_384, gl_buffer.BUFFER_HINT.StreamToGPU )
				if _GLFONT_HAS_NP:
					self._ubo_chars_arr = np.empty( 16_384//8, dtype="f" )
				else:
					self._ubo_chars_arr = array.array( "f", [ float("nan") ]*(16_384//8) )

			ubo_font_size_req = sizeof(GLfloat)*8 * len(self._glyphs)
			if self._ubo_font!=None and self._ubo_font.size_bytes<ubo_font_size_req:
				self._ubo_font = None

			if self._ubo_font == None:
				#UBO can store at least 16kiB = 4k floats = 512 characters; allocate at least that.
				ubo_size = min( 16_384, ubo_font_size_req )
				self._ubo_font = gl_buffer.BufferGPU(ubo_size)

				self._program = gl_program.Program( [
					gl_program.ShaderVert(
						"""#version 140 // GL 3.1

						uniform vec3 drawy_and_viewportfactor;

						struct CharInfo { vec2 texc_lo; vec2 texc_hi; vec2 bearing; vec2 size; };
						/*layout( binding=0 )*/ uniform UBO_Font
						{
							CharInfo font["""+str( ubo_size // (8*sizeof(GLfloat)) )+"""];
						};
						/*layout( binding=1 )*/ uniform UBO_Chars
						{
							vec2 drawxs_and_inds["""+str(Font.CHUNK_NUM_CHARS_MAX)+"""];
						};

						/*layout( location=0 )*/ in vec2 attrib_vert;

						out vec2 texc;

						void main()
						{
							vec2 drawx_and_ind = drawxs_and_inds[gl_InstanceID];
							CharInfo char_info = font[uint( drawx_and_ind.y )];

							texc = mix( char_info.texc_lo,char_info.texc_hi, attrib_vert );

							float x0 = drawx_and_ind[0]            + char_info.bearing[0];
							float y1 = drawy_and_viewportfactor[0] + char_info.bearing[1];
							float x1 = x0 + char_info.size.x;
							float y0 = y1 - char_info.size.y;
							vec2 lo=vec2(x0,y0), hi=vec2(x1,y1);
							vec2 coord = mix( lo,hi, attrib_vert );

							//coord*2.0/viewport.zw - vec2(1.0)
							#if __VERSION__ >= 400
								coord = fma( coord,drawy_and_viewportfactor.yz, vec2(-1.0) );
							#else
								coord = coord*drawy_and_viewportfactor.yz + vec2(-1.0);
							#endif
							gl_Position = vec4( coord, 0.0, 1.0 );
						}
						"""
					),
					gl_program.ShaderFrag(
						"""#version 140 // GL 3.1

						uniform sampler2D font_tex;
						uniform vec4 text_color;

						in  vec2 texc ;
						out vec4 frag_color;

						void main()
						{
							frag_color = texture2D( font_tex, texc ) * text_color;
						}
						"""
					)
				], { "attrib_vert":0 }, { "frag_color":0 } )

			ubo_font_data:list[float] = []
			for chardata_ind, glyph in enumerate( self._glyphs.values() ):
				ubo_font_data += [
					glyph.s_lo,glyph.t_lo, glyph.s_hi,glyph.t_hi, # 4 floats
					glyph.bearing[0], glyph.bearing[1], # 2 floats
					glyph.surf.get_width(), glyph.surf.get_height() # 2 ints (cast to floats)
				]
				self._ubo_font_chardata_inds[glyph.codepoint] = chardata_ind
			self._ubo_font.data_write(ubo_font_data)

		self._dirty = False

	def render( self:Self,
		text:str, pos:tuple[float,float], color:tuple[float,float,float,float]=( 1.0,1.0,1.0, 1.0 )
	):
		""" Render the given `text` at the starting baseline 2D coordinate `pos`.

		Note that the coordinate is the baseline, so the font can render above and below this.

		Certain MSAA modes (e.g. MSAA 16⨯ on my machine) may make this blurry, and we cannot fix
		that here.  Some samples are on the pixel edge, so raster rules assign them to neighbor
		pixels even though the quads are pixel-aligned.  We can nudge the geometry and texture
		coordinates to compensate:
			delX=-0.5/16.0; delY=+0.5/16.0; delS=delX/self._tex.w; delT=delY/self._tex.h
		But this isn't enough because for this mode there are actually 4 shader invocations, which
		will cause interpolation in the texture accesses (and we need to leave filtering on for our
		textures so that scaling, 3D, etc. works).  Recommendation: disable MSAA while calling this.

		Note that if only GL 2 is available, assumes a 2D projection is active.
		"""
		self.ensure(text)
		self._regen_if_necessary()
		x, y = pos
		if _GLFONT_HAS_GEOM and glDrawElementsInstanced:
			gl_program.Program.use(self._program)
			_,_, vpw,vph = glGetFloatv(GL_VIEWPORT)
			self._program.pass_3f( "drawy_and_viewportfactor", y, 2.0/vpw,2.0/vph )
			self._program.pass_4f( "text_color", color )
			self._program.get_loc_unif("UBO_Font.font")
			self._program.pass_bind_ubo( self._ubo_font , 0, "UBO_Font"  )
			self._program.pass_bind_ubo( self._ubo_chars, 1, "UBO_Chars" )
			self._program.pass_bind_tex( "font_tex", self._tex, 0 )

			ich0 = 0
			while ich0 < len(text):
				chunk_num_chars = min( Font.CHUNK_NUM_CHARS_MAX, len(text)-ich0 )

				for k in range(chunk_num_chars):
					ch = text[ ich0 + k ]

					codepoint = ord(ch)
					glyph        = self._glyphs                [codepoint]
					chardata_ind = self._ubo_font_chardata_inds[codepoint]

					self._ubo_chars_arr[ 2*k     ] = x
					self._ubo_chars_arr[ 2*k + 1 ] = float(chardata_ind)

					x += glyph.advance

				self._ubo_chars.data_write( self._ubo_chars_arr, 0,8*chunk_num_chars )
				self._geom.draw(chunk_num_chars)

				ich0 += Font.CHUNK_NUM_CHARS_MAX
		else:
			glEnable(GL_TEXTURE_2D)
			glColor4f( *color )
			gl_texture.TextureBase.bind(self._tex)
			glBegin(GL_QUADS)
			for ch in text:
				glyph = self._glyphs[ ord(ch) ]
				w, h = glyph.surf.get_size()
				x0=x+glyph.bearing[0]; y1=y+glyph.bearing[1]; x1=x0+w; y0=y1-h
				glTexCoord2f( glyph.s_lo, glyph.t_lo ); glVertex3f( x0, y0, 0.0 )
				glTexCoord2f( glyph.s_hi, glyph.t_lo ); glVertex3f( x1, y0, 0.0 )
				glTexCoord2f( glyph.s_hi, glyph.t_hi ); glVertex3f( x1, y1, 0.0 )
				glTexCoord2f( glyph.s_lo, glyph.t_hi ); glVertex3f( x0, y1, 0.0 )
				x += glyph.advance
			glEnd()

	def render_approx_width( self:Self, text:str ):
		self.ensure(text)
		x = 0
		for ch in text:
			glyph = self._glyphs[ ord(ch) ]
			x += glyph.advance
		return x

	def render_gl2_bounds  ( self:Self, text:str, pos:tuple[float,float] ):
		self.ensure(text)
		self._regen_if_necessary()
		x, y = pos
		for ch in text:
			glyph = self._glyphs[ ord(ch) ]
			w, h = glyph.surf.get_size()
			x0=x+glyph.bearing[0]; y1=y+glyph.bearing[1]; x1=x0+w; y0=y1-h
			glBegin(GL_LINE_LOOP)
			glVertex3f( x0, y0, 0.0 )
			glVertex3f( x1, y0, 0.0 )
			glVertex3f( x1, y1, 0.0 )
			glVertex3f( x0, y1, 0.0 )
			glEnd()
			x += glyph.advance
	def render_gl2_bearings( self:Self, text:str, pos:tuple[float,float] ):
		self.ensure(text)
		self._regen_if_necessary()
		glBegin(GL_LINES)
		x, y = pos
		for ch in text:
			glyph = self._glyphs[ ord(ch) ]
			glVertex3f( x                 , y                 , 0.0 )
			glVertex3f( x+glyph.bearing[0], y+glyph.bearing[1], 0.0 )
			x += glyph.advance
		glEnd()
	def render_gl2_map     ( self:Self, pos:tuple[float,float], sc:float=1.0 ):
		glBegin(GL_QUADS)
		x0=pos[0]; y0=pos[1]; x1=x0+sc*self._tex.res[0]; y1=y0+sc*self._tex.res[1]
		glTexCoord2f( 0.0, 0.0 ); glVertex3f( x0, y0, 0.0 )
		glTexCoord2f( 1.0, 0.0 ); glVertex3f( x1, y0, 0.0 )
		glTexCoord2f( 1.0, 1.0 ); glVertex3f( x1, y1, 0.0 )
		glTexCoord2f( 0.0, 1.0 ); glVertex3f( x0, y1, 0.0 )
		glEnd()
