package min3d.core;

import java.nio.FloatBuffer;
import java.nio.IntBuffer;

import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
import javax.microedition.khronos.opengles.GL11;

import min3d.Min3d;
import min3d.Shared;
import min3d.animation.AnimationObject3d;
import min3d.vos.FrustumManaged;
import min3d.vos.Light;
import min3d.vos.RenderType;
import min3d.vos.TextureVo;
import android.app.ActivityManager;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLSurfaceView;
import android.opengl.GLU;
import android.opengl.GLUtils;
import android.util.Log;


public class Renderer implements GLSurfaceView.Renderer
{
	public static final int NUM_GLLIGHTS = 8;

	private GL10 _gl;
	private Scene _scene;
	private TextureManager _textureManager;

	private float _surfaceAspectRatio;
	
	private IntBuffer _scratchIntBuffer;
	private FloatBuffer _scratchFloatBuffer;
	private boolean _scratchB;
	

	// stats-related
	public static final int FRAMERATE_SAMPLEINTERVAL_MS = 1000; 
	private boolean _logFps = false;
	private long _frameCount = 0;
	private float _fps = 0;
	private long _timeLastSample;
	private ActivityManager _activityManager;
	private ActivityManager.MemoryInfo _memoryInfo;


	public Renderer(Scene $scene)
	{
		_scene = $scene;

		_scratchIntBuffer = IntBuffer.allocate(4);
		_scratchFloatBuffer = FloatBuffer.allocate(4);
		
		_textureManager = new TextureManager();
		Shared.textureManager(_textureManager); 
		
		_activityManager = (ActivityManager) Shared.context().getSystemService( Context.ACTIVITY_SERVICE );
		_memoryInfo = new ActivityManager.MemoryInfo();
	}

	public void onSurfaceCreated(GL10 $gl, EGLConfig eglConfig) 
	{
		Log.i(Min3d.TAG, "Renderer.onSurfaceCreated()");

		RenderCaps.setRenderCaps($gl);

		setGl($gl);

		reset();
		
		_scene.init();
	}

	public void clearScene()
	{
		_scene.clear();
	}

	public void onSurfaceChanged(GL10 gl, int w, int h) 
	{
		Log.i(Min3d.TAG, "Renderer.onSurfaceChanged()");

		setGl(_gl);
		_surfaceAspectRatio = (float)w / (float)h;
		
		_gl.glViewport(0, 0, w, h);
		_gl.glMatrixMode(GL10.GL_PROJECTION);
		_gl.glLoadIdentity();

		updateViewFrustrum();
	}
	
	public void onDrawFrame(GL10 gl)
	{
		// Update 'model'
		_scene.update();
		
		// Update 'view'
		drawSetup();
		drawScene();

		if (_logFps) doFps();
	}
	
	//
	
	/**
	 *  Accessor to the GL object, in case anything outside this class wants to do 
	 *  bad things with it :)
	 */
	public GL10 gl()
	{
		return _gl;
	}

	/**
	 * Returns last sampled framerate (logFps must be set to true) 
	 */
	public float fps()
	{
		return _fps;
	}
	/**
	 * Return available system memory in bytes
	 */
	public long availMem()
	{
		_activityManager.getMemoryInfo(_memoryInfo);
		return _memoryInfo.availMem;
	}
	
	protected void drawSetup()
	{
		// View frustrum
		
		if (_scene.camera().frustum.isDirty()) {
			updateViewFrustrum();
		}
		 
		// Camera 
		
		_gl.glMatrixMode(GL10.GL_MODELVIEW);
		_gl.glLoadIdentity();

		GLU.gluLookAt(_gl, 
			_scene.camera().position.x,_scene.camera().position.y,_scene.camera().position.z,
			_scene.camera().target.x,_scene.camera().target.y,_scene.camera().target.z,
			_scene.camera().upAxis.x,_scene.camera().upAxis.y,_scene.camera().upAxis.z);
		
		// Background color
		
		if (_scene.backgroundColor().isDirty())
		{
			_gl.glClearColor( 
				(float)_scene.backgroundColor().r() / 255f, 
				(float)_scene.backgroundColor().g() / 255f, 
				(float)_scene.backgroundColor().b() / 255f, 
				(float)_scene.backgroundColor().a() / 255f);
			_scene.backgroundColor().clearDirtyFlag();
		}
		
		_gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
		
		drawSetupLights();
		
		// Always on:
		_gl.glEnableClientState(GL10.GL_VERTEX_ARRAY);
	}
	
	protected void drawSetupLights()
	{
		// GL_LIGHTS enabled/disabled based on enabledDirty list
		for (int glIndex = 0; glIndex < NUM_GLLIGHTS; glIndex++)
		{
			if (_scene.lights().glIndexEnabledDirty()[glIndex] == true)
			{
				if (_scene.lights().glIndexEnabled()[glIndex] == true) 
				{
					_gl.glEnable(GL10.GL_LIGHT0 + glIndex);
					
					// make light's properties dirty to force update
					_scene.lights().getLightByGlIndex(glIndex).setAllDirty();
				} 
				else 
				{
					_gl.glDisable(GL10.GL_LIGHT0 + glIndex);
				}
				
				_scene.lights().glIndexEnabledDirty()[glIndex] = false; // clear dirtyflag
			}
		}
		
		// Lights' properties 

		Light[] lights = _scene.lights().toArray();
		for (int i = 0; i < lights.length; i++)
		{
			Light light = lights[i];
			
			if (light.isDirty()) // .. something has changed
			{
				// Check all of Light's properties for dirty 
				
				int glLightId = GL10.GL_LIGHT0 + _scene.lights().getGlIndexByLight(light);
				
				if (light.position.isDirty())
				{
					light.commitPositionAndTypeBuffer();
					_gl.glLightfv(glLightId, GL10.GL_POSITION, light._positionAndTypeBuffer);
					light.position.clearDirtyFlag();
				}
				if (light.ambient.isDirty()) 
				{
					light.ambient.commitToFloatBuffer();
					_gl.glLightfv(glLightId, GL10.GL_AMBIENT, light.ambient.floatBuffer());
					light.ambient.clearDirtyFlag();
				}
				if (light.diffuse.isDirty()) 
				{
					light.diffuse.commitToFloatBuffer();
					_gl.glLightfv(glLightId, GL10.GL_DIFFUSE, light.diffuse.floatBuffer());
					light.diffuse.clearDirtyFlag();
				}
				if (light.specular.isDirty())
				{
					light.specular.commitToFloatBuffer();
					_gl.glLightfv(glLightId, GL10.GL_SPECULAR, light.specular.floatBuffer());
					light.specular.clearDirtyFlag();
				}
				if (light.emissive.isDirty())
				{
					light.emissive.commitToFloatBuffer();
					_gl.glLightfv(glLightId, GL10.GL_EMISSION, light.emissive.floatBuffer());
					light.emissive.clearDirtyFlag();
				}

				if (light.direction.isDirty())
				{
					light.direction.commitToFloatBuffer();
					_gl.glLightfv(glLightId, GL10.GL_SPOT_DIRECTION, light.direction.floatBuffer());
					light.direction.clearDirtyFlag();
				}
				if (light._spotCutoffAngle.isDirty())
				{
					_gl.glLightf(glLightId, GL10.GL_SPOT_CUTOFF, light._spotCutoffAngle.get());
				}
				if (light._spotExponent.isDirty())
				{
					_gl.glLightf(glLightId, GL10.GL_SPOT_EXPONENT, light._spotExponent.get());
				}

				if (light._isVisible.isDirty()) 
				{
					if (light.isVisible()) {
						_gl.glEnable(glLightId);
					} else {
						_gl.glDisable(glLightId);
					}
					light._isVisible.clearDirtyFlag();
				}

				if (light._attenuation.isDirty())
				{
					_gl.glLightf(glLightId, GL10.GL_CONSTANT_ATTENUATION, light._attenuation.getX());
					_gl.glLightf(glLightId, GL10.GL_LINEAR_ATTENUATION, light._attenuation.getY());
					_gl.glLightf(glLightId, GL10.GL_QUADRATIC_ATTENUATION, light._attenuation.getZ());
				}
				
				light.clearDirtyFlag();
			}
		}
	}

	protected void drawScene()
	{
		if(_scene.fogEnabled() == true) {
			_gl.glFogf(GL10.GL_FOG_MODE, _scene.fogType().glValue());
			_gl.glFogf(GL10.GL_FOG_START, _scene.fogNear());
			_gl.glFogf(GL10.GL_FOG_END, _scene.fogFar());
			_gl.glFogfv(GL10.GL_FOG_COLOR, _scene.fogColor().toFloatBuffer() );
			_gl.glEnable(GL10.GL_FOG);
		} else {
			_gl.glDisable(GL10.GL_FOG);
		}

		for (int i = 0; i < _scene.children().size(); i++)
		{
			Object3d o = _scene.children().get(i);
			if(o.animationEnabled())
			{
				((AnimationObject3d)o).update();
			}
			drawObject(o);
		}
	}
	
	//boolean customResult = o.customRenderer(_gl); 
	//if (customResult) return;


	protected void drawObject(Object3d $o)
	{
		if ($o.isVisible() == false) return;		

		// Various per-object settings:
		
		// Normals

		if ($o.hasNormals() && $o.normalsEnabled()) {
			$o.vertices().normals().buffer().position(0);
			_gl.glNormalPointer(GL10.GL_FLOAT, 0, $o.vertices().normals().buffer());
			_gl.glEnableClientState(GL10.GL_NORMAL_ARRAY);
		}
		else {
			_gl.glDisableClientState(GL10.GL_NORMAL_ARRAY);
		}
		
		// Is lighting enabled for object...
		
		/*
		// *** this version not working properly on emulator - why not? ***
		_scratchIntBuffer.position(0);
		_gl.glGetIntegerv(GL10.GL_LIGHTING, _scratchIntBuffer);
		if (useLighting != _scratchIntBuffer.get(0))
		{
			if (useLighting == 1) {
				_gl.glEnable(GL10.GL_LIGHTING);
			} else {
				_gl.glDisable(GL10.GL_LIGHTING);
			}
		}
		*/
		
		boolean useLighting = (_scene.lightingEnabled() && $o.hasNormals() && $o.normalsEnabled() && $o.lightingEnabled());
		if (useLighting) {
			_gl.glEnable(GL10.GL_LIGHTING);
		} else {
			_gl.glDisable(GL10.GL_LIGHTING);
		}
		
		// Shademodel
		
		_gl.glGetIntegerv(GL11.GL_SHADE_MODEL, _scratchIntBuffer);
		if ($o.shadeModel().glConstant() != _scratchIntBuffer.get(0)) {
			_gl.glShadeModel($o.shadeModel().glConstant());
		}
		
		// Colors: either per-vertex, or per-object

		if ($o.hasVertexColors() && $o.vertexColorsEnabled()) {
			$o.vertices().colors().buffer().position(0);
			_gl.glColorPointer(4, GL10.GL_UNSIGNED_BYTE, 0, $o.vertices().colors().buffer());
			_gl.glEnableClientState(GL10.GL_COLOR_ARRAY); 
		}
		else {
			_gl.glColor4f(
				(float)$o.defaultColor().r / 255f, 
				(float)$o.defaultColor().g / 255f, 
				(float)$o.defaultColor().b / 255f, 
				(float)$o.defaultColor().a / 255f
			);
			_gl.glDisableClientState(GL10.GL_COLOR_ARRAY);
		}
		
		// Colormaterial
		
		_gl.glGetIntegerv(GL10.GL_COLOR_MATERIAL, _scratchIntBuffer);
		_scratchB = (_scratchIntBuffer.get(0) != 0);
		if ($o.colorMaterialEnabled() != _scratchB) {
			if ($o.colorMaterialEnabled())
				_gl.glEnable(GL10.GL_COLOR_MATERIAL);
			else
				_gl.glDisable(GL10.GL_COLOR_MATERIAL);
		}
		
		// Point size
		
		if ($o.renderType() == RenderType.POINTS) 
		{
			if ($o.pointSmoothing()) 
				_gl.glEnable(GL10.GL_POINT_SMOOTH);
			else
				_gl.glDisable(GL10.GL_POINT_SMOOTH);
			
			_gl.glPointSize($o.pointSize());
		}

		// Line properties
		
		if ($o.renderType() == RenderType.LINES || $o.renderType() == RenderType.LINE_STRIP || $o.renderType() == RenderType.LINE_LOOP) 
		{
			if ( $o.lineSmoothing() == true) {
				_gl.glEnable(GL10.GL_LINE_SMOOTH);
			}
			else {
				_gl.glDisable(GL10.GL_LINE_SMOOTH);
			}

			_gl.glLineWidth($o.lineWidth());
		}

		// Backface culling 
		
		if ($o.doubleSidedEnabled()) {
		    _gl.glDisable(GL10.GL_CULL_FACE);
		} 
		else {
		    _gl.glEnable(GL10.GL_CULL_FACE);
		}
		

		drawObject_textures($o);

		
		// Matrix operations in modelview

		_gl.glPushMatrix();
		
		_gl.glTranslatef($o.position().x, $o.position().y, $o.position().z);
		
		_gl.glRotatef($o.rotation().x, 1,0,0);
		_gl.glRotatef($o.rotation().y, 0,1,0);
		_gl.glRotatef($o.rotation().z, 0,0,1);
		
		_gl.glScalef($o.scale().x, $o.scale().y, $o.scale().z);
		
		// Draw

		$o.vertices().points().buffer().position(0);
		_gl.glVertexPointer(3, GL10.GL_FLOAT, 0, $o.vertices().points().buffer());

		if (! $o.ignoreFaces())
		{
			int pos, len;
			
			if (! $o.faces().renderSubsetEnabled()) {
				pos = 0;
				len = $o.faces().size();
			}
			else {
				pos = $o.faces().renderSubsetStartIndex() * FacesBufferedList.PROPERTIES_PER_ELEMENT;
				len = $o.faces().renderSubsetLength();
			}

			$o.faces().buffer().position(pos);

			_gl.glDrawElements(
					$o.renderType().glValue(),
					len * FacesBufferedList.PROPERTIES_PER_ELEMENT, 
					GL10.GL_UNSIGNED_SHORT, 
					$o.faces().buffer());
		}
		else
		{
			_gl.glDrawArrays($o.renderType().glValue(), 0, $o.vertices().size());
		}
		
		//
		// Recurse on children
		//
		
		if ($o instanceof Object3dContainer)
		{
			Object3dContainer container = (Object3dContainer)$o;
			
			for (int i = 0; i < container.children().size(); i++)
			{
				Object3d o = container.children().get(i);
				drawObject(o);
			}
		}
		
		// Restore matrix
		
		_gl.glPopMatrix();
	}
	
	private void drawObject_textures(Object3d $o)
	{
		// iterate thru object's textures
		
		for (int i = 0; i < RenderCaps.maxTextureUnits(); i++)
		{
			_gl.glActiveTexture(GL10.GL_TEXTURE0 + i);
			_gl.glClientActiveTexture(GL10.GL_TEXTURE0 + i); 

			if ($o.hasUvs() && $o.texturesEnabled())
			{
				$o.vertices().uvs().buffer().position(0);
				_gl.glTexCoordPointer(2, GL10.GL_FLOAT, 0, $o.vertices().uvs().buffer());

				TextureVo textureVo = ((i < $o.textures().size())) ? textureVo = $o.textures().get(i) : null;

				if (textureVo != null)
				{
					// activate texture
					int glId = _textureManager.getGlTextureId(textureVo.textureId);
					_gl.glBindTexture(GL10.GL_TEXTURE_2D, glId);
				    _gl.glEnable(GL10.GL_TEXTURE_2D);
					_gl.glEnableClientState(GL10.GL_TEXTURE_COORD_ARRAY);

					int minFilterType = _textureManager.hasMipMap(textureVo.textureId) ? GL10.GL_LINEAR_MIPMAP_NEAREST : GL10.GL_NEAREST; 
					_gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, minFilterType);
					_gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); // (OpenGL default)
					
					// do texture environment settings
					for (int j = 0; j < textureVo.textureEnvs.size(); j++)
					{
						_gl.glTexEnvx(GL10.GL_TEXTURE_ENV, textureVo.textureEnvs.get(j).pname, textureVo.textureEnvs.get(j).param);
					}
					
					// texture wrapping settings
					_gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_S, (textureVo.repeatU ? GL10.GL_REPEAT : GL10.GL_CLAMP_TO_EDGE));
					_gl.glTexParameterx(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_WRAP_T, (textureVo.repeatV ? GL10.GL_REPEAT : GL10.GL_CLAMP_TO_EDGE));		

					// texture offset, if any
					if (textureVo.offsetU != 0 || textureVo.offsetV != 0)
					{
						_gl.glMatrixMode(GL10.GL_TEXTURE);
						_gl.glLoadIdentity();
						_gl.glTranslatef(textureVo.offsetU, textureVo.offsetV, 0);
						_gl.glMatrixMode(GL10.GL_MODELVIEW); // .. restore matrixmode
					}
				}
				else
				{
					_gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
				    _gl.glDisable(GL10.GL_TEXTURE_2D);
					_gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
				}
			}
			else
			{
				_gl.glBindTexture(GL10.GL_TEXTURE_2D, 0);
			    _gl.glDisable(GL10.GL_TEXTURE_2D);
				_gl.glDisableClientState(GL10.GL_TEXTURE_COORD_ARRAY);
			}
		}
	}
	
	/**
	 * Used by TextureManager
	 */
	int uploadTextureAndReturnId(Bitmap $bitmap, boolean $generateMipMap) /*package-private*/
	{
		int glTextureId;
		
		int[] a = new int[1];
		_gl.glGenTextures(1, a, 0); // create a 'texture name' and put it in array element 0
		glTextureId = a[0];
		_gl.glBindTexture(GL10.GL_TEXTURE_2D, glTextureId);
		
		if($generateMipMap && _gl instanceof GL11) {
			_gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_GENERATE_MIPMAP, GL11.GL_TRUE);
		} else {
			_gl.glTexParameterf(GL11.GL_TEXTURE_2D, GL11.GL_GENERATE_MIPMAP, GL11.GL_FALSE);
		}

		// 'upload' to gpu
		GLUtils.texImage2D(GL10.GL_TEXTURE_2D, 0, $bitmap, 0);
		
		return glTextureId;
	}
	

	/**
	 * Used by TextureManager
	 */
	void deleteTexture(int $glTextureId) /*package-private*/
	{
		int[] a = new int[1];
		a[0] = $glTextureId;
		_gl.glDeleteTextures(1, a, 0);
	}
	
	protected void updateViewFrustrum()
	{
		FrustumManaged vf = _scene.camera().frustum;
		float n = vf.shortSideLength() / 2f;

		float lt, rt, btm, top;
		
		lt  = vf.horizontalCenter() - n*_surfaceAspectRatio;
		rt  = vf.horizontalCenter() + n*_surfaceAspectRatio;
		btm = vf.verticalCenter() - n*1; 
		top = vf.verticalCenter() + n*1;

		if (_surfaceAspectRatio > 1) {
			lt *= 1f/_surfaceAspectRatio;
			rt *= 1f/_surfaceAspectRatio;
			btm *= 1f/_surfaceAspectRatio;
			top *= 1f/_surfaceAspectRatio;
		}
		
		_gl.glMatrixMode(GL10.GL_PROJECTION);
		_gl.glLoadIdentity();
		_gl.glFrustumf(lt,rt, btm,top, vf.zNear(), vf.zFar());
		
		vf.clearDirtyFlag();
	}

	/**
	 * If true, framerate and memory is periodically calculated and Log'ed,
	 * and gettable thru fps() 
	 */
	public void logFps(boolean $b)
	{
		_logFps = $b;
		
		if (_logFps) { // init
			_timeLastSample = System.currentTimeMillis();
			_frameCount = 0;
		}
	}
	
	private void setGl(GL10 $gl)
	{
		_gl = $gl;
	}
	
	private void doFps()
	{
		_frameCount++;

		long now = System.currentTimeMillis();
		long delta = now - _timeLastSample;
		if (delta >= FRAMERATE_SAMPLEINTERVAL_MS)
		{
			_fps = _frameCount / (delta/1000f); 

			_activityManager.getMemoryInfo(_memoryInfo);
			Log.v(Min3d.TAG, "FPS: " + Math.round(_fps) + ", availMem: " + Math.round(_memoryInfo.availMem/1048576) + "MB");

			_timeLastSample = now;
			_frameCount = 0;
		}
	}
	
	private void reset()
	{
		// Reset TextureManager
		Shared.textureManager().reset();

		// Do OpenGL settings which we are using as defaults, or which we will not be changing on-draw
		
	    // Explicit depth settings
		_gl.glEnable(GL10.GL_DEPTH_TEST);									
		_gl.glClearDepthf(1.0f);
		_gl.glDepthFunc(GL10.GL_LESS);										
		_gl.glDepthRangef(0,1f);											
		_gl.glDepthMask(true);												

		// Alpha enabled
		_gl.glEnable(GL10.GL_BLEND);										
		_gl.glBlendFunc(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA); 	
		
		// "Transparency is best implemented using glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) 
		// with primitives sorted from farthest to nearest."

		// Texture
		_gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MIN_FILTER, GL10.GL_NEAREST); // (OpenGL default is GL_NEAREST_MIPMAP)
		_gl.glTexParameterf(GL10.GL_TEXTURE_2D, GL10.GL_TEXTURE_MAG_FILTER, GL10.GL_LINEAR); // (is OpenGL default)
		
		// CCW frontfaces only, by default
		_gl.glFrontFace(GL10.GL_CCW);
	    _gl.glCullFace(GL10.GL_BACK);
	    _gl.glEnable(GL10.GL_CULL_FACE);
	    
	    // Disable lights by default
	    for (int i = GL10.GL_LIGHT0; i < GL10.GL_LIGHT0 + NUM_GLLIGHTS; i++) {
	    	_gl.glDisable(i);
	    }

		//
		// Scene object init only happens here, when we get GL for the first time
		//
	}
}
