#!/usr/bin/env python
#! coding: utf-8
#! python3
# Python ish game layer à la GM
# Dirty as hay, if you learn python with this file, you will learn garbage.
# 2022-2024

import random
import os

#os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = '1'
import pygame
import pygame.freetype


# Basic message system
def info( message="!?" ) :
	color = ""
	reset = ""
	if __colorConsole__ :
		reset = colorama.Style.RESET_ALL
		color = colorama.Style.BRIGHT + colorama.Fore.CYAN
	print( f"{color}[info] {message}{reset}" )

def warning( message="!?" ) :
	color = ""
	reset = ""
	if __colorConsole__ :
		reset = colorama.Style.RESET_ALL
		color = colorama.Style.BRIGHT + colorama.Fore.YELLOW
	print( f"{color}[Warn] {message}{reset}" )

def error( message="!?" ) :
	color = ""
	reset = ""
	if __colorConsole__ :
		reset = colorama.Style.RESET_ALL
		color = colorama.Style.BRIGHT + colorama.Fore.RED
	print( f"\a{color}[ERR.] {message}{reset}" )

# Optional libraries
__colorConsole__ = True
try :
	import colorama
	colorama.init( autoreset=False )
	info( "color console output" )
except ImportError or ModuleNotFoundError :
	__colorConsole__ = False
	info( "monochrome console output" )


_internal_backgrounds = {}
_internal_sounds = []
_internal_musics = {}
_internal_fonts = []


#===== Computing things =====
#===== Game play =====
#===== User interaction =====
#===== Game graphics =====
#===== Sound and music =====
#----- Basic sound functions -----

# Plays the indicates sound once. If the sound is background music the current background music is stopped.
def sound_play( index=None ) :
	# if index in _internal_sounds :
		# snd = _internal_sounds[index]
		# if not snd.loaded :
			# pygame.mixer.music.load( snd.filename )
			# snd.loaded = True
		# pygame.mixer.music.play()
	pass

# Plays the indicates sound, looping continuously. If the sound is background music the current background music is stopped.
def sound_loop( index=None ) :
	pass

# Stops the indicates sound. If there are multiple sounds with this index playing simultaneously, all will be stopped.
def sound_stop( index=None ) :
	pass

# Stops all sounds.
def sound_stop_all() :
	for sndIndex in _internal_sounds :
		sound_stop( sndIndex )
	

# Returns whether (a copy of) the indicated sound is playing.
def sound_isplaying( index=None ) :
	pass

# Plays or loops the music. The current music is stopped beforehand.
def music_play( name=None, loop=False ) :
	global _internal_musics
	
	if music_exists( name ) :
		msk = _internal_musics[name]
		
		pygame.mixer.music.unload()
		if msk["intro"] != None :
			pygame.mixer.music.load( msk["intro"])
			pygame.mixer.music.play()
			pygame.mixer.music.queue( msk["file"], loops={True:-1, False:0}[loop] )
		else :
			pygame.mixer.music.load( msk["file"] )
			pygame.mixer.music.play( {True:-1, False:0}[loop])
	

# Plays the indicated music, looping continuously.
# The current music is stopped beforehand.
# You can use music_play( loop=True ) too
def music_loop( index=None ) :
	music_play( index, loop=True )

# Stops the music.
def music_stop() :
	pygame.mixer.music.stop()
	pygame.mixer.music.unload()

# Returns whether music is playing.
def music_isplaying() :
	return pygame.mixer.music.get_busy()

#----- Special effects -----
#----- 3D music -----
#----- CD music -----
#===== Splash screens, highscores and other pop-ups =====
#===== Resources =====
#===== Changing resources =====
#----- Sprites -----
#----- Sound -----

# Adds a sound resource to the game. fname is the name of the sound file.
# kind indicates the kind of sound (0=normal, 1=background, 2=3d, 3=mmplayer) preload indicates whether the sound should immediately be stored in audio memory (true or false).
# The function returns the index of the new sound, which can be used to play the sound. (-1 if an error occurred, e.g. the file does not exist).
def sound_add(fname,kind,preload) :
	pass

# Same as the previous function but this time a new sound is not created but the existing sound index is replaced, freeing the old sound.
# Returns whether correct.
def sound_replace(index,fname,kind,preload) :
	pass

# Deletes the indicated sound, freeing all memory associated with it.
# It can no longer be restored.
def sound_delete(index) :
	pass

def music_add( name, filename, intro=None ):
	global _internal_musics
	
	ret = None
	if not isinstance( name, str ) :
		error( f"music_delete: name must be a string, got '{name}' instead." )
	else :
		if len( name ) > 0 :
			if not os.path.exists( filename ) :
				warning( f"music_add: Unable to check file '{filename}'." )
			else :
				msk = {"file":filename, "intro":None}
				if intro != None :
					if not os.path.exists( intro ) :
						warning( f"music_add: Unable to check intro file '{intro}'." )
					else :
						msk["intro"] = intro
				_internal_musics[name] = msk
				ret = name
	
	return ret
	

# Delete the music with the given name
def music_delete( name ):
	global _internal_musics
	
	ret = None
	if not isinstance( name, str ) :
		error( f"music_delete: name must be a string, got '{name}' instead." )
	else :
		if name in _internal_musics :
			# No data is loaded until playing, so we can simply remove the entry
			del _internal_musics[name]
		else :
			warning( f"music_delete: '{name}' is not a loaded music." )
	
	return ret

# Checks if a music with this name already exists
def music_exists( name ):
	global _internal_musics
	
	ret = False
	if not isinstance( name, str ) :
		error( f"music_exists: name must be a string, got '{name}' instead." )
	else :
		if name in _internal_musics :
			ret = True
	
	return ret

#----- Backgrounds -----
def background_add( name, filename, removeback=False, smooth=False ) :
	global _internal_backgrounds
	
	ret = None
	if not isinstance( name, str ) :
		error( f"background_add: Unnacceptable name '{name}'." )
	else :
		if len( name ) > 0 :
			if not os.path.exists( filename ) :
				warning( f"background_add: Unable to check file '{filename}'." )
			else :
				bck = {"file":filename, "surface":None}
				bck["surface"] = pygame.image.load( bck["file"] )
				_internal_backgrounds[name] = bck
				ret = name
	
	return ret

def background_delete( name ):
	global _internal_backgrounds
	
	ret = None
	if not isinstance( name, str ) :
		error( f"background_delete: name must be a string, got '{name}' instead." )
	else :
		if name in _internal_backgrounds :
			del _internal_backgrounds[name]["surface"]
			del _internal_backgrounds[name]
		else :
			print( f"background_delete: '{name}' is not a loaded background" )
	
	return ret

def background_exists( name ):
	global _internal_backgrounds
	
	ret = False
	if not isinstance( name, str ) :
		error( f"background_delete: name must be a string, got '{name}' instead." )
	else :
		if name in _internal_backgrounds :
			ret = True
	
	return ret

#----- Fonts -----

def font_add( name=None, size=16, bold=False, italic=False, first=32, last=128 ) :
	global _internal_fonts
	
	# Size sanity check
	size = int( size )
	if size < 1 : size = 1
	
	# Font file check
	if name != None :
		if not os.path.exists( name ) :
			warning( f"Can't find file '{name}', fall back to default font." )
			name = None
	
	# Load it
	if name != None :
		# font = pygame.freetype.Font( name, size )
		font = pygame.font.Font( name, size )
	else :
		font = pygame.font.SysFont( None, size )
	
	_internal_fonts.append( font )
	return font

#----- Paths -----
#----- Scripts -----
#----- time lines -----
#----- Objects -----
#----- Rooms -----
#===== Files, registry, and executing programs =====
#===== Data structures =====
#===== Creating particles =====
#===== Multiplayer games =====
#===== Using DLL's =====
#===== 3D Graphics =====


#----- Timelines -----
#----- Objects -----
#----- Instances -----
#----- Rooms -----

#===== General Game Control =====
game_initialized = False
game_id = 0
game_save_id = game_id
game_display_name = "Great Mishmash"
game_project_name = "Undefined"
game_clock = 0

# Initialize
def game_init():
	global game_clock
	
	pygame.init()
	pygame.mixer.init()
	game_clock = pygame.time.Clock()
	
	WINDOW_SIZE = (800,600)
	surface_create( WINDOW_SIZE )
	window_set( WINDOW_SIZE )
	
	_draw_text_currentFont = font_add()
	
	game_initialized = True

#
def game_end() :
	pygame.quit()

#
def game_restart(  ) :
	pass

#
def game_load(  ) :
	pass

#
def game_load_buffer(  ) :
	pass

#
def game_save(  ) :
	pass

#
def game_save_buffer(  ) :
	pass

#
def game_get_speed(  ) :
	pass

#
def game_set_speed(  ) :
	pass

#
def highscore_add() :
	pass

#
def highscore_name() :
	pass

#
def highscore_value() :
	pass

#
def highscore_clear() :
	pass

#
def draw_highscore() :
	pass

#
def cursor_sprite() :
	pass



#===== Movement And Collisions =====
#===== Drawing =====
_draw_surface_currentSurface = None

#----- Drawing shapes -----

# Clears the current surface in the given color
def draw_clear( color=(0,0,0) ) :
	global _draw_surface_currentSurface
	_draw_surface_currentSurface.fill( color )


# Clears the current surface in the given color and alpha value
def draw_clear_alpha( color=(0,0,0), alpha=0) :
	pass

# Draws a point at (x,y) in the current color.
def draw_point( coords=(0,0) ) : pass

# Draws a line from (x1,y1) to (x2,y2).
def draw_line( start=(0,0), end=(16,16) ) : pass

# Draws a line from (x1,y1) to (x2,y2) with width w.
def draw_line_width( start=(0,0), end=(16,16), width=1 ) : pass


# Draws a rectangle. outline indicates whether only the outline must be drawn (true) or it should be filled (false).
def draw_rectangle( start=(0,0), end=(16,16), outline=True ) :
	global _draw_surface_currentSurface
	global _draw_color_currentColor
	global _draw_color_currentAlpha
	
	width = 0
	if outline == True : width = 1
	
	color = _draw_color_currentColor
	
	if (_draw_color_currentAlpha != 1) :
		s = pygame.Surface( (end[0]-start[0], end[1]-start[1]) )
		s.set_alpha( int(_draw_color_currentAlpha*255) )
		s.fill( color )
		_draw_surface_currentSurface.blit( s, (start[0], start[1]) )
	else :
		pygame.draw.rect( _draw_surface_currentSurface, color, (start[0], start[1], end[0]-start[0], end[1]-start[1]), width=width )
	
#----- Colour And Alpha -----

c_aqua = (0,255,255)
c_black = (0,0,0)
c_blue = (0,0,255)
c_dkgray = (64,64,64)
c_fuchsia = (255,0,255)
c_gray = (128,128,128)
c_green = (0,128,0)
c_lime = (0,255,0)
c_ltgray = (192,192,192)
c_maroon = (128,0,0)
c_navy = (0,0,128)
c_olive = (128,128,0)
c_orange = (64,160,0)
c_purple = (128,0,128)
c_red = (255,0,0)
c_silver = (192,192,192)
c_teal = (0,128,128)
c_white = (255,255,255)
c_yellow = (255,255,0)

_draw_color_currentColor = c_black
_draw_color_currentAlpha = 1

#
def colour_get_blue() :
	pass

#
def colour_get_green() :
	pass

#
def colour_get_red() :
	pass

#
def colour_get_hue() :
	pass

#
def colour_get_saturation() :
	pass

#
def colour_get_value() :
	pass

#
def draw_getpixel() :
	pass

#
def draw_getpixel_ext() :
	pass

#
def draw_get_colour() :
	pass

#
def draw_get_alpha() :
	pass

#
def make_colour_hsv() :
	pass

#
def make_colour_rgb( r=0, g=0, b=0) :
	return (r,g,b)

#
def merge_colour() :
	pass


#
def draw_set_alpha( alpha=0 ) :
	global _draw_color_currentAlpha
	_draw_color_currentAlpha = alpha

#
def draw_set_color( color=(0,0,0) ) :
	global _draw_color_currentColor
	if len(color) == 4 :
		draw_set_alpha( color[3]/255 )
	_draw_color_currentColor = color[:3]

#Non standard GM
def sanitize_color( color=None ):
	
	# Default to black
	if color == None : color = 0
	
	# String families (Hex, HTML and stringified lists)
	if isinstance( color, str ) :
		if (color[0]=='(') and (color[-1]==')') and color.count(',') in (2,3) :
			color = [int(c) for c in color[1:-1].split(',')]
		else:
			#TODO: scan for $ or # for hex color, and maybe some html names
			color = (0,0,0)
	
	# RGB integer
	if isinstance( color, int ) :
		make_color_rgb( (color % 256), ((color/256)%256), ((color/65536)%256) )
	
	# multiple values
	if isinstance( color, tuple ) or isinstance( color, list ) :
		if len( color ) not in [3,4] :
			color = (0,0,0)
	
	return color


#----- GPU Control -----
#----- Mipmapping -----
#----- Basic Forms -----
#----- Sprites And Tiles -----

def draw_background( background=None, x=0, y=0 ) :
	# width = background_get_width( background )
	# height = background_get_height( background )
	width, height = background_get_size( background )
	draw_background_general( background, 0,0,width,height, x,y, 1,1, 0, c_white,c_white,c_white,c_white, 1 )
	


def draw_background_general( background=None, left=0,top=0, width=16,height=16, x=0,y=0, xscale=1,yscale=1, rotation=0, c1=c_white,c2=c_white,c3=c_white,c4=c_white, alpha=1) :
	global _internal_backgrounds
	
	if background in _internal_backgrounds :
		surface_get_target().blit( _internal_backgrounds[background]["surface"], (x,y) )
	

#----- Text -----
fa_left = "left"
fa_center = "center"
fa_right = "right"
fa_top = "top"
fa_middle = "middle"
fa_bottom = "bottom"

_draw_text_currentFont = None
_draw_text_hAlign = fa_left
_draw_text_vAlign = fa_top

#
def draw_set_font( font=None ) :
	global _internal_fonts
	global _draw_text_currentFont
	
	if font not in _internal_fonts : font = _internal_fonts[0]
	
	_draw_text_currentFont = font
	

#
def draw_set_halign( halign=fa_left ) :
	global _draw_text_hAlign
	
	if halign not in [fa_left, fa_center, fa_right] :
		halign = fa_left
	
	_draw_text_hAlign = halign
	

#
def draw_set_valign( valign=fa_top ) :
	global _draw_text_vAlign
	
	if valign not in [fa_top, fa_middle, fa_bottom] :
		valign = fa_top
	
	_draw_text_vAlign = valign
	

#
def draw_get_font( ) :
	global _draw_text_currentFont
	return _draw_text_currentFont

#
def draw_get_halign( ) :
	global _draw_text_hAlign
	return _draw_text_hAlign

#
def draw_get_valign( ) :
	global _draw_text_vAlign
	return _draw_text_vAlign


#
def draw_text( coords=(0,0), string="Text" ) :
	draw_text_color( coords, string, _draw_color_currentColor, _draw_color_currentAlpha )

#
def draw_text_ext( ) :
	pass

#
def draw_text_color( coords=(0,0), string="Text", colors=( c_black, c_black, c_white, c_white), alpha=0.5 ) :
	global _draw_surface_currentSurface
	global _draw_text_currentFont
	global _draw_text_hAlign
	global _draw_text_vAlign
	
	error = None
	
	# Normalization and sanity checks
	
	if _draw_surface_currentSurface == None : error = "No surface"
	if _draw_text_currentFont == None : error = "No Font"
	
	if isinstance( coords, tuple ) or isinstance( coords, list ) :
		x,y = coords
	else :
		error = "Malformed coords"
	
	if not isinstance( string, str ) :
		string = str( string )
	
	if isinstance( colors, tuple ) or isinstance( colors, list ) :
		#TODO find a way to do it cleanly. Use sanitize_color()
		if len( colors ) == 3 :
			c = colors
			colors = (c,c,c,c)
		
	
	
	if error == None :
		
		textImage = _draw_text_currentFont.render( string, False, colors[0])
		
		# Compute coordinates
		if _draw_text_hAlign == fa_center :
			w,h = textImage.get_size()
			x -= w/2
		
		if _draw_text_hAlign == fa_right :
			w,h = textImage.get_size()
			x += w
		
		if _draw_text_vAlign == fa_middle :
			w,h = textImage.get_size()
			y -= h/2
		
		if _draw_text_vAlign == fa_bottom :
			w,h = textImage.get_size()
			y += h
		
		_draw_surface_currentSurface.blit( textImage, (x,y) )
		
	else :
		error( error )
	

#
def draw_text_transformed( ) :
	pass

#
def draw_text_ext_colour( ) :
	pass

#
def draw_text_ext_transformed( ) :
	pass

#
def draw_text_transformed_colour( ) :
	pass

#
def draw_text_ext_transformed_colour( ) :
	pass


#----- Primitives And Vertex Formats -----
#----- Surfaces -----

_internal_surfaces = []

#
def surface_exists() :
	pass

# Creates a surface of the indicated width and height.
# Returns the id of the surface, which must be used in all further calls.
# Note that the surface will not be cleared. 
def surface_create( size=(128,128) ) :
	return surface_create_ext( size, color=None, mode=pygame.SRCALPHA )

#
def surface_create_ext( size=(128,128), color=None, mode=pygame.SRCALPHA ) :
	global _internal_surfaces
	
	surface = pygame.Surface( size, mode )
	_internal_surfaces.append( surface )
	
	if color != None :
		surface_set_target( surface )
		draw_clear( color )
		surface_reset_target()
	
	return surface

#
def surface_resize() :
	pass

#
def surface_set_target( surface=None ) :
	global _internal_surfaces
	global _draw_surface_currentSurface
	
	if surface not in _internal_surfaces : surface = _internal_surfaces[0]
	
	_draw_surface_currentSurface = surface
	


#
def surface_get_target() :
	global _draw_surface_currentSurface
	
	return _draw_surface_currentSurface
	


# Resets the drawing target to the normal screen.
def surface_reset_target() :
	global _internal_surfaces
	global _draw_surface_currentSurface
	
	_draw_surface_currentSurface = _internal_surfaces[0]
	

#----- Lighting -----
#----- Particles -----
#----- Textures -----
#----- Shaders -----
#----- Video Playback -----
#===== Cameras And Display =====

def window_set( size=(640,480), flags=pygame.SHOWN ) :
	global _internal_surfaces
	global _draw_surface_currentSurface
	
	pygame.display.quit()
	pygame.display.init()
	
	_internal_surfaces[0] = pygame.display.set_mode( size, flags )
	surface_reset_target()
	

#===== Ressources =====
#----- Background -----

# Returns whether a background with the given index exists.
def background_exists( index=None ):
	pass

# Returns the name of the background with the given index.
def background_get_name( index=None ):
	pass

# Returns the width of the background with the given index.
def background_get_width( name=None ):
	global _internal_backgrounds
	pass

# Returns the height of the background with the given index.
def background_get_height( name=None ):
	global _internal_backgrounds
	pass

#
def background_get_size( name=None ):
	return ( background_get_width( name ), background_get_height( name ) )

# In certain situations you might want to save the bitmap corresponding the background to a file. For this the following function can be used:
# Saves the background ind to the file with the name fname. This must be a .png file.
def background_save( index=None, fname=None ):
	pass

#===== Game Input =====
#===== Data Structures =====
#===== Strings =====
#===== Maths And Numbers =====
#===== Physics =====
#===== Asynchronous Functions =====
#===== Networking =====
#===== File Handling =====
#===== Buffers =====
#===== Debugging =====
#===== Garbage Collection =====

# Miscelaneous

# Choose one item of a list at random
def choose( l=None ) :
	r = None
	if l != None :
		r = l[ random.randrange( 0, len(l) ) ]
	return r


# Initialisation
if __name__=="__main__":
	
	game_init()
	draw_set_color( (255,255,255) )
	
	bigFont = font_add( None, 64 )
	smlFont = font_add( None, 16 )
	
	running = True
	while running:
		
		# Event handling, gets all event from the event queue
		for event in pygame.event.get():
			if event.type == pygame.QUIT:
				running = False
			
		
		# Draw
		draw_clear()
		
		draw_set_font( bigFont )
		draw_text( (8,8), "GM" )
		
		draw_set_font( smlFont )
		draw_text( (8,48), "is meant to be used as a lib." )
		
		pygame.display.flip()
		game_clock.tick(60)
		pygame.event.poll()
	

