# -*- coding: utf-8 -*-
"""
UTc! module: src.rendering.shading

Purpose
=======
 Provides support for UTc!'s shading effects.
 
Legal
=====
 This code is based on, but is now highly dissimilar to, the LGPL code found
 at http://www.pygame.org/wiki/GLSLExample and
 http://www.pygame.org/wiki/GLSL_ARB_Example on February 9th, 2009.
 
 All other code, unless otherwise indicated, is original, and subject to the
 terms of the GPLv3, which is provided in COPYING.
 
 (C) Neil Tallim, 2009
"""
import ctypes
import os
import sys
import OpenGL.GL as GL

_gl = None #: The platform-native OpenGL C library.
try:
	from OpenGL import platform
	_gl = platform.OpenGL #For OpenGL-ctypes.
except ImportError:
	try:
		_gl = ctypes.cdll.LoadLibrary('libGL.so') #For PyOpenGL.
	except OSError:
		pass #Use the Windows fallback.
		
_WINDOWS_MODE = False #: True if the Windows-ARB workaround is in use.

RENDER_MODE_BASIC = 1 #: Indicates that basic polygons should be rendered.
RENDER_MODE_WIREFRAME = 2 #: Indicates that wireframe polygons should be rendered.
RENDER_MODE_CEL = 3 #: Indicates that polygons should be rendered with cel-shading.
RENDER_MODE_GRADIENT = 4 #: Indicates that polygons should be rendered with gradient-shading.
RENDER_MODE_FLAT = 5 #Indicates that polygons should be rendered with silhouette-shading.

GL_FRAGMENT_SHADER = 0x8B30 #: The int used to mark code as a fragment shader element.
GL_VERTEX_SHADER = 0x8B31 #: The int used to mark code as a vertex shader element.

VERTEX_PROGRAM_BODY = """
varying vec3 normal;
void main(){
	normal = normalize(gl_NormalMatrix * gl_Normal);
	gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
}""" #: The body-shading vertex element.
FRAGMENT_PROGRAM_CEL = """
const float quantizer = 4.0;
const float quantizer_chunk = 1.0 / quantizer;
const float quantizer_step = 1.0 / (2.0 * quantizer);

uniform float r;
uniform float g;
uniform float b;

varying vec3 normal;
void main(){
	float intensity;
	float applied_intensity;
	float final_intensity;
	vec3 light = normalize(gl_LightSource[0].position).xyz;
	
	//Calculate the nominal cel colour.
	intensity = dot(light, normal);
	applied_intensity = (intensity * quantizer) + 1.0;
	final_intensity = floor(applied_intensity) / quantizer;
	
	if(final_intensity > quantizer_chunk && final_intensity < 1.0 - quantizer_chunk){//Ignore edge colours.
		//Warning for puritans: intensity is recycled from this point forth for efficiency.
		intensity = mod(applied_intensity, 1.0);
		if(intensity < 0.075 || intensity > 0.925){//Determine whether this is in a border region and soften if necessary.
			if(intensity < 0.5){
				final_intensity = max(final_intensity - quantizer_step, 0.0);
			}else{
				final_intensity = min(final_intensity + quantizer_step, 1.0);
			}
		}
	}
	
	//Apply the fragment's colour.
	gl_FragColor = vec4(final_intensity * r, final_intensity * g, final_intensity * b, 1.0);
}""" #: The cel-shading fragment element.
FRAGMENT_PROGRAM_GRADIENT = """
const float minimum = 1.0 / 4.0;
const float denominator = 1.0 - minimum;

uniform float r;
uniform float g;
uniform float b;

varying vec3 normal;
void main(){
	float intensity;
	float applied_intensity;
	float final_intensity;
	vec3 light = normalize(gl_LightSource[0].position).xyz;
	
	intensity = ((dot(light, normal) - minimum) / denominator) + minimum;
	gl_FragColor = vec4(intensity * r, intensity * g, intensity * b, 1.0);
}""" #: The gradient-shading fragment element.
FRAGMENT_PROGRAM_FLAT = """
uniform float r;
uniform float g;
uniform float b;

void main(){
	gl_FragColor = vec4(r, g, b, 1.0);
}""" #: The sihouette-shading fragment element.

VERTEX_PROGRAM_RAINBOW = """
varying vec3 position;
void main(){
	vec3 ec_pos = vec3(gl_ModelViewMatrix * gl_Vertex);
	vec3 t_normal = normalize(gl_NormalMatrix * gl_Normal);
	vec3 light = normalize(normalize(gl_LightSource[0].position).xyz - ec_pos);
	position = normalize(reflect(-light, t_normal));
	
	gl_Position = ftransform();
}""" #: The rainbow-shading vertex element.
FRAGMENT_PROGRAM_RAINBOW = """
varying vec3 position;
void main(){
	gl_FragColor.rgb = position.xyz;
}""" #: The rainbow-shading fragment element.

VERTEX_PROGRAM_GLOSS = """
varying float n_by_l;
varying vec3 reflection;
varying vec3 view;

void main(){
	vec3 ec_pos = vec3(gl_ModelViewMatrix * gl_Vertex);
	vec3 t_normal = normalize(gl_NormalMatrix * gl_Normal);
	vec3 light = normalize(normalize(gl_LightSource[0].position).xyz - ec_pos);
	reflection = normalize(reflect(-light, t_normal));
	view = normalize(-ec_pos);
	n_by_l = (dot(light, t_normal) + 1.0) * 0.5;
	gl_Position = ftransform();
}""" #: The gloss-shading vertex element.
FRAGMENT_PROGRAM_GLOSS = """
vec3  surface_colour = vec3(%f, %f, %f);
vec3  warm_colour = vec3(%f, %f, %f);
vec3  cool_colour = vec3(%f, %f, %f);
float diffuse_warm = %f;
float diffuse_cool = %f;

varying float n_by_l;
varying vec3  reflection;
varying vec3  view;

void main(){
	vec3 cool = min(cool_colour + diffuse_cool * surface_colour, 1.0);
	vec3 warm = min(warm_colour + diffuse_warm * surface_colour, 1.0);
	vec3 applied = mix(cool, warm, n_by_l);
	float specular = max(dot(reflection, view), 0.0);
	specular = pow(specular, 32.0);
	gl_FragColor = vec4(min(applied + specular, 1.0), 1.0);
}""" #: The glood-shading fragment element.

_glCreateShader = None #: The function used to create a GLSL shader.
_glShaderSource = None #: The function used to map GLSL source to a shader.
_glShaderSource_argtypes = [ctypes.c_int, ctypes.c_int, ctypes.POINTER(ctypes.c_char_p), ctypes.POINTER(ctypes.c_int)] #: The types of arguments to pass when mapping shader source.
_glCompileShader = None #: The function used to compile GLSL source into a shader.
_glDeleteShader = None #: The function used to delete a GLSL shader.
_glCreateProgram = None #: The function used to create a GLSL program.
_glAttachShader = None #: The function used to attach a GLSL shader to a program.
_glLinkProgram = None #: The function used to link the GLSL shaders in a program together.
_glUseProgram = None #: The function used to activate a GLSL program.
_glGetUniformLocation = None #: The function used to get a handle to a uniform variable in a GLSL program.
_glUniform1f = None #: The function used to set a float value as a GLSL program's uniform.

def init():
	"""
	Finds the platform's GLSL functions and maps them so that they may be
	transparently invoked by this module.
	
	Standard Linux methods are tried first, looking first for true GLSL, then
	attempting to compromise on ARB. If both lookups fail, an indirect, PyOpenGL
	method is used; it's known to work exclusively on Windows systems, but it
	does work.
	
	@raise AttributeError: If even the Windows lookup method fails, implying that
	    the system lacks any usable shading API.
	"""
	global _glCreateShader
	global _glShaderSource
	global _glCompileShader
	global _glDeleteShader
	global _glCreateProgram
	global _glAttachShader
	global _glLinkProgram
	global _glUseProgram
	global _glGetUniformLocation
	global _glUniform1f
	
	try:
		_glCreateShader = _gl.glCreateShader
		_glShaderSource = _gl.glShaderSource
		_glCompileShader = _gl.glCompileShader
		_glDeleteShader = _gl.glDeleteShader
		_glCreateProgram = _gl.glCreateProgram
		_glAttachShader = _gl.glAttachShader
		_glLinkProgram = _gl.glLinkProgram
		_glUseProgram = _gl.glUseProgram
		_glGetUniformLocation = _gl.glGetUniformLocation
		_glUniform1f = _gl.glUniform1f
		
		_glShaderSource.argtypes = _glShaderSource_argtypes
	except AttributeError:
		try:
			sys.stderr.write("GLSL shading API not found; attempting to use ARB.\n")
			
			_glCreateShader = _gl.glCreateShaderObjectARB
			_glShaderSource = _gl.glShaderSourceARB
			_glCompileShader = _gl.glCompileShaderARB
			_glDeleteShader = _gl.glDeleteObjectARB
			_glCreateProgram = _gl.glCreateProgramObjectARB
			_glAttachShader = _gl.glAttachObjectARB
			_glLinkProgram = _gl.glLinkProgramARB
			_glUseProgram = _gl.glUseProgramObjectARB
			_glGetUniformLocation = _gl.glGetUniformLocationARB
			_glUniform1f = _gl.glUniform1fARB
			
			_glShaderSource.argtypes = _glShaderSource_argtypes
		except AttributeError:
			global _WINDOWS_MODE
			_WINDOWS_MODE = True
			sys.stderr.write("Attempting Windows ARB workaround.\n")
			
			_glCreateShader = GL.ARB.shader_objects.glCreateShaderObjectARB
			_glShaderSource = GL.ARB.shader_objects.glShaderSourceARB
			_glCompileShader = GL.ARB.shader_objects.glCompileShaderARB
			_glDeleteShader = GL.ARB.shader_objects.glDeleteObjectARB
			_glCreateProgram = GL.ARB.shader_objects.glCreateProgramObjectARB
			_glAttachShader = GL.ARB.shader_objects.glAttachObjectARB
			_glLinkProgram = GL.ARB.shader_objects.glLinkProgramARB
			_glUseProgram = GL.ARB.shader_objects.glUseProgramObjectARB
			_glGetUniformLocation = GL.ARB.shader_objects.glGetUniformLocationARB
			_glUniform1f = GL.ARB.shader_objects.glUniform1fARB
			
def compileProgram(vertex_shader, fragment_shader):
	"""
	Builds a GLSL program, given at least one shader.
	
	@type vertex_shader: int|None
	@param vertex_shader: A GLSL shader ID that points to a vertex shader. None
	    if there is no need for a vertex shader in the program.
	@type fragment_shader: int|None
	@param fragment_shader: A GLSL shader ID that points to a fragment shader.
	    None if there is no need for a vertex shader in the program.
	
	@rtype: int|None
	@return: A GLSL program ID if at least one shader was specified, or None
	   if nothing was provided.
	"""
	if vertex_shader is None and fragment_shader is None:
		return None
		
	program_id = _glCreateProgram()
	if not vertex_shader is None:
		_glAttachShader(program_id, vertex_shader)
	if not fragment_shader is None:
		_glAttachShader(program_id, fragment_shader)
	_glLinkProgram(program_id)
	return program_id
	
def compileShader(source, shader_type):
	"""
	Builds a GLSL shader, given shader source code and a constant that identifies
	the type of shader being constructed.
	
	@type source: basestring
	@param source: The source code to use in constructing this GLSL shader.
	@type shader_type: int
	@param shader_type: A constant that identifies the nature of the source code
	    being compiled.
	
	@rtype: int
	@return: A GLSL shader ID.
	"""
	shader_id = _glCreateShader(shader_type)
	if not _WINDOWS_MODE:
		source = ctypes.c_char_p(source)
		length = ctypes.c_int(-1)
		_glShaderSource(shader_id, 1, ctypes.byref(source), ctypes.byref(length))
	else:
		_glShaderSource(shader_id, source)
	_glCompileShader(shader_id)
	return shader_id
	
def deleteShader(shader):
	"""
	Destroys a GLSL shader, freeing the memory and handle it occupied.
	
	@type shader: int
	@param shader: The ID of the GLSL shader to delete.
	"""
	_glDeleteShader(shader)
	
def setUniform(program_id, uniform_name, value):
	"""
	Sets a uniform variable for use in a GLSL program.
	
	@type program_id: int
	@param program_id: The ID of the GLSL program in which the uniform variable
	    is to be set.
	@type uniform_name: basestring
	@param uniform_name: The name of the uniform variable to be set.
	@type value: float
	@param value: The number to assign to the uniform variable.
	"""
	uniform_id = _glGetUniformLocation(program_id, uniform_name)
	_glUniform1f(uniform_id, ctypes.c_float(value))
	
def useProgram(program_id=None):
	"""
	Loads a GLSL program for use in rendering polygons.
	
	@type program_id: int|None
	@param program_id: The ID of the GLSL program to load or None if the existing
	    program should be unloaded.
	"""
	if program_id is None:
		program_id = 0
	_glUseProgram(program_id)
	