package org.andengine.opengl.util;

import org.andengine.util.exception.AndEngineRuntimeException;
import org.andengine.util.math.MathConstants;

import android.opengl.Matrix;

/**
 * TODO Measure performance with inlined or native Matrix implementations.
 * 
 * (c) Zynga 2011
 *
 * @author Nicolas Gramlich <ngramlich@zynga.com>
 * @since 14:49:23 - 04.08.2011
 */
public class GLMatrixStack {
	// ===========================================================
	// Constants
	// ===========================================================

	public static final int GLMATRIXSTACK_DEPTH_MAX = 32;
	public static final int GLMATRIX_SIZE = 16;

	private static final int GLMATRIXSTACKOFFSET_UNDERFLOW = -1 * GLMatrixStack.GLMATRIX_SIZE;
	private static final int GLMATRIXSTACKOFFSET_OVERFLOW = GLMatrixStack.GLMATRIXSTACK_DEPTH_MAX * GLMatrixStack.GLMATRIX_SIZE;

	// ===========================================================
	// Fields
	// ===========================================================

	final float[] mMatrixStack = new float[GLMatrixStack.GLMATRIXSTACK_DEPTH_MAX * GLMatrixStack.GLMATRIX_SIZE];
	int mMatrixStackOffset;

	private final float[] mTemp = new float[2 * GLMatrixStack.GLMATRIX_SIZE];

	// ===========================================================
	// Constructors
	// ===========================================================

	public GLMatrixStack() {
		this.glLoadIdentity();
	}

	// ===========================================================
	// Constructors
	// ===========================================================

	// ===========================================================
	// Getter & Setter
	// ===========================================================

	public void getMatrix(final float[] pMatrix) {
		System.arraycopy(this.mMatrixStack, this.mMatrixStackOffset, pMatrix, 0, GLMatrixStack.GLMATRIX_SIZE);
	}

	// ===========================================================
	// Methods for/from SuperClass/Interfaces
	// ===========================================================

	// ===========================================================
	// Methods
	// ===========================================================

	public void glLoadIdentity() {
		Matrix.setIdentityM(this.mMatrixStack, this.mMatrixStackOffset);
	}

	public void glTranslatef(final float pX, final float pY, final float pZ) {
		Matrix.translateM(this.mMatrixStack, this.mMatrixStackOffset, pX, pY, pZ);
	}

	public void glRotatef(final float pAngle, final float pX, final float pY, final float pZ) {
		Matrix.setRotateM(this.mTemp, 0, pAngle, pX, pY, pZ);
		System.arraycopy(this.mMatrixStack, this.mMatrixStackOffset, this.mTemp, GLMatrixStack.GLMATRIX_SIZE, GLMatrixStack.GLMATRIX_SIZE);
		Matrix.multiplyMM(this.mMatrixStack, this.mMatrixStackOffset, this.mTemp, GLMatrixStack.GLMATRIX_SIZE, this.mTemp, 0);
	}

	public void glScalef(final float pScaleX, final float pScaleY, final float pScaleZ) {
		Matrix.scaleM(this.mMatrixStack, this.mMatrixStackOffset, pScaleX, pScaleY, pScaleZ);
	}

	public void glSkewf(final float pSkewX, final float pSkewY) {
		GLMatrixStack.setSkewM(this.mTemp, 0, pSkewX, pSkewY);
		System.arraycopy(this.mMatrixStack, this.mMatrixStackOffset, this.mTemp, GLMatrixStack.GLMATRIX_SIZE, GLMatrixStack.GLMATRIX_SIZE);
		Matrix.multiplyMM(this.mMatrixStack, this.mMatrixStackOffset, this.mTemp, GLMatrixStack.GLMATRIX_SIZE, this.mTemp, 0);
	}

	public void glOrthof(final float pLeft, final float pRight, final float pBottom, final float pTop, final float pZNear, final float pZFar) {
		Matrix.orthoM(this.mMatrixStack, this.mMatrixStackOffset, pLeft, pRight, pBottom, pTop, pZNear, pZFar);
	}

	public void glPushMatrix() throws GLMatrixStackOverflowException {
		if (this.mMatrixStackOffset + GLMatrixStack.GLMATRIX_SIZE >= GLMatrixStack.GLMATRIXSTACKOFFSET_OVERFLOW) {
			throw new GLMatrixStackOverflowException();
		}

		System.arraycopy(this.mMatrixStack, this.mMatrixStackOffset, this.mMatrixStack, this.mMatrixStackOffset + GLMatrixStack.GLMATRIX_SIZE, GLMatrixStack.GLMATRIX_SIZE);
		this.mMatrixStackOffset += GLMatrixStack.GLMATRIX_SIZE;
	}

	public void glPopMatrix() {
		if (this.mMatrixStackOffset - GLMatrixStack.GLMATRIX_SIZE <= GLMatrixStack.GLMATRIXSTACKOFFSET_UNDERFLOW) {
			throw new GLMatrixStackUnderflowException();
		}

		this.mMatrixStackOffset -= GLMatrixStack.GLMATRIX_SIZE;
	}

	public void reset() {
		this.mMatrixStackOffset = 0;
		this.glLoadIdentity();
	}

	private static void setSkewM(final float[] pMatrixStack, final int pOffset, final float pSkewX, final float pSkewY) {
		pMatrixStack[pOffset + 0] = 1.0f;
		pMatrixStack[pOffset + 1] = (float) Math.tan(-MathConstants.DEG_TO_RAD * pSkewY);
		pMatrixStack[pOffset + 2] = 0.0f;
		pMatrixStack[pOffset + 3] = 0.0f;

		pMatrixStack[pOffset + 4] = (float) Math.tan(-MathConstants.DEG_TO_RAD * pSkewX);
		pMatrixStack[pOffset + 5] = 1.0f;
		pMatrixStack[pOffset + 6] = 0.0f;
		pMatrixStack[pOffset + 7] = 0.0f;

		pMatrixStack[pOffset + 8] = 0.0f;
		pMatrixStack[pOffset + 9] = 0.0f;
		pMatrixStack[pOffset + 10] = 1.0f;
		pMatrixStack[pOffset + 11] = 0.0f;

		pMatrixStack[pOffset + 12] = 0.0f;
		pMatrixStack[pOffset + 13] = 0.0f;
		pMatrixStack[pOffset + 14] = 0.0f;
		pMatrixStack[pOffset + 15] = 1.0f;
	}

	// ===========================================================
	// Inner and Anonymous Classes
	// ===========================================================

	public static class GLMatrixStackOverflowException extends AndEngineRuntimeException {
		// ===========================================================
		// Constants
		// ===========================================================

		private static final long serialVersionUID = -800847781599300100L;

		// ===========================================================
		// Fields
		// ===========================================================

		// ===========================================================
		// Constructors
		// ===========================================================

		public GLMatrixStackOverflowException() {

		}

		// ===========================================================
		// Getter & Setter
		// ===========================================================

		// ===========================================================
		// Methods for/from SuperClass/Interfaces
		// ===========================================================

		// ===========================================================
		// Methods
		// ===========================================================

		// ===========================================================
		// Inner and Anonymous Classes
		// ===========================================================
	}

	public static class GLMatrixStackUnderflowException extends AndEngineRuntimeException {
		// ===========================================================
		// Constants
		// ===========================================================

		private static final long serialVersionUID = -3268021423136372954L;

		// ===========================================================
		// Fields
		// ===========================================================

		// ===========================================================
		// Constructors
		// ===========================================================

		public GLMatrixStackUnderflowException() {

		}

		// ===========================================================
		// Getter & Setter
		// ===========================================================

		// ===========================================================
		// Methods for/from SuperClass/Interfaces
		// ===========================================================

		// ===========================================================
		// Methods
		// ===========================================================

		// ===========================================================
		// Inner and Anonymous Classes
		// ===========================================================
	}
}
