import Matrix3D						from "awayjs-core/lib/geom/Matrix3D";
import Matrix3DUtils				from "awayjs-core/lib/geom/Matrix3DUtils";
import Rectangle					from "awayjs-core/lib/geom/Rectangle";
import AssetEvent					from "awayjs-core/lib/events/AssetEvent";
import FreeMatrixProjection			from "awayjs-core/lib/projections/FreeMatrixProjection";
import IProjection					from "awayjs-core/lib/projections/IProjection";

import IRenderer					from "../../IRenderer";
import Scene						from "../../display/Scene";
import Camera						from "../../display/Camera";
import DirectionalShadowMapper		from "../../materials/shadowmappers/DirectionalShadowMapper";
import Single2DTexture				from "../../textures/Single2DTexture";

class CascadeShadowMapper extends DirectionalShadowMapper
{
	public _pScissorRects:Rectangle[];
	private _pScissorRectsInvalid:boolean = true;
	private _splitRatios:number[];

	private _numCascades:number /*int*/;
	private _depthCameras:Array<Camera>;
	private _depthLenses:Array<FreeMatrixProjection>;

	private _texOffsetsX:Array<number>;
	private _texOffsetsY:Array<number>;

	private _nearPlaneDistances:number[];

	constructor(numCascades:number /*uint*/ = 3)
	{
		super();

		if (numCascades < 1 || numCascades > 4)
			throw new Error("numCascades must be an integer between 1 and 4");

		this._numCascades = numCascades;
		this.init();
	}

	public getSplitRatio(index:number /*uint*/):number
	{
		return this._splitRatios[index];
	}

	public setSplitRatio(index:number /*uint*/, value:number)
	{
		if (value < 0)
			value = 0;
		else if (value > 1)
			value = 1;

		if (index >= this._numCascades)
			throw new Error("index must be smaller than the number of cascades!");

		this._splitRatios[index] = value;
	}

	public getDepthProjections(partition:number /*uint*/):Matrix3D
	{
		return this._depthCameras[partition].viewProjection;
	}

	private init()
	{
		this._splitRatios = new Array<number>(this._numCascades);
		this._nearPlaneDistances = new Array<number>(this._numCascades);

		var s:number = 1;
		for (var i:number /*int*/ = this._numCascades - 1; i >= 0; --i) {
			this._splitRatios[i] = s;
			s *= .4;
		}

		this._texOffsetsX = Array<number>(-1, 1, -1, 1);
		this._texOffsetsY = Array<number>(1, 1, -1, -1);
		this._pScissorRects = new Array<Rectangle>(4);
		this._depthLenses = new Array<FreeMatrixProjection>();
		this._depthCameras = new Array<Camera>();

		for (i = 0; i < this._numCascades; ++i) {
			this._depthLenses[i] = new FreeMatrixProjection();
			this._depthCameras[i] = new Camera(this._depthLenses[i]);
		}
	}

	public _pSetDepthMapSize(value:number /*uint*/)
	{
		super._pSetDepthMapSize(value);

		this.invalidateScissorRects();
	}

	private invalidateScissorRects()
	{
		this._pScissorRectsInvalid = true;
	}

	public get numCascades():number /*int*/
	{
		return this._numCascades;
	}

	public set numCascades(value:number /*int*/)
	{
		if (value == this._numCascades)
			return;

		if (value < 1 || value > 4)
			throw new Error("numCascades must be an integer between 1 and 4");

		this._numCascades = value;
		this.invalidateScissorRects();
		this.init();
		this.dispatchEvent(new AssetEvent(AssetEvent.INVALIDATE, this));
	}

	public pDrawDepthMap(scene:Scene, target:Single2DTexture, renderer:IRenderer)
	{
		if (this._pScissorRectsInvalid)
			this.updateScissorRects();

		renderer.cullPlanes = this._pCullPlanes;
		renderer._iRenderCascades(this._pOverallDepthCamera, scene, target.image2D, this._numCascades, this._pScissorRects, this._depthCameras);
	}

	private updateScissorRects()
	{
		var half:number = this._pDepthMapSize*.5;

		this._pScissorRects[0] = new Rectangle(0, 0, half, half);
		this._pScissorRects[1] = new Rectangle(half, 0, half, half);
		this._pScissorRects[2] = new Rectangle(0, half, half, half);
		this._pScissorRects[3] = new Rectangle(half, half, half, half);

		this._pScissorRectsInvalid = false;
	}

	public pUpdateDepthProjection(camera:Camera)
	{
		var matrix:Matrix3D;
		var projection:IProjection = camera.projection;
		var projectionNear:number = projection.near;
		var projectionRange:number = projection.far - projectionNear;

		this.pUpdateProjectionFromFrustumCorners(camera, camera.projection.frustumCorners, this._pMatrix);
		this._pMatrix.appendScale(.96, .96, 1);
		this._pOverallDepthProjection.matrix = this._pMatrix;
		this.pUpdateCullPlanes(camera);

		for (var i:number /*int*/ = 0; i < this._numCascades; ++i) {
			matrix = this._depthLenses[i].matrix;

			this._nearPlaneDistances[i] = projectionNear + this._splitRatios[i]*projectionRange;
			this._depthCameras[i].transform.matrix3D = this._pOverallDepthCamera.transform.matrix3D;

			this.updateProjectionPartition(matrix, this._splitRatios[i], this._texOffsetsX[i], this._texOffsetsY[i]);

			this._depthLenses[i].matrix = matrix;
		}
	}

	private updateProjectionPartition(matrix:Matrix3D, splitRatio:number, texOffsetX:number, texOffsetY:number)
	{
		var raw:Float32Array = Matrix3DUtils.RAW_DATA_CONTAINER;
		var xN:number, yN:number, zN:number;
		var xF:number, yF:number, zF:number;
		var minX:number = Number.POSITIVE_INFINITY, minY:number = Number.POSITIVE_INFINITY, minZ:number;
		var maxX:number = Number.NEGATIVE_INFINITY, maxY:number = Number.NEGATIVE_INFINITY, maxZ:number = Number.NEGATIVE_INFINITY;
		var i:number /*uint*/ = 0;

		while (i < 12) {
			xN = this._pLocalFrustum[i];
			yN = this._pLocalFrustum[i + 1];
			zN = this._pLocalFrustum[i + 2];
			xF = xN + (this._pLocalFrustum[i + 12] - xN)*splitRatio;
			yF = yN + (this._pLocalFrustum[i + 13] - yN)*splitRatio;
			zF = zN + (this._pLocalFrustum[i + 14] - zN)*splitRatio;
			if (xN < minX)
				minX = xN;
			if (xN > maxX)
				maxX = xN;
			if (yN < minY)
				minY = yN;
			if (yN > maxY)
				maxY = yN;
			if (zN > maxZ)
				maxZ = zN;
			if (xF < minX)
				minX = xF;
			if (xF > maxX)
				maxX = xF;
			if (yF < minY)
				minY = yF;
			if (yF > maxY)
				maxY = yF;
			if (zF > maxZ)
				maxZ = zF;
			i += 3;
		}

		minZ = 1;

		var w:number = (maxX - minX);
		var h:number = (maxY - minY);
		var d:number = 1/(maxZ - minZ);

		if (minX < 0)
			minX -= this._pSnap; // because int() rounds up for < 0
		if (minY < 0)
			minY -= this._pSnap;
		minX = Math.floor(minX/this._pSnap)*this._pSnap;
		minY = Math.floor(minY/this._pSnap)*this._pSnap;

		var snap2:number = 2*this._pSnap;
		w = Math.floor(w/snap2 + 1)*snap2;
		h = Math.floor(h/snap2 + 1)*snap2;

		maxX = minX + w;
		maxY = minY + h;

		w = 1/w;
		h = 1/h;

		raw[0] = 2*w;
		raw[5] = 2*h;
		raw[10] = d;
		raw[12] = -(maxX + minX)*w;
		raw[13] = -(maxY + minY)*h;
		raw[14] = -minZ*d;
		raw[15] = 1;
		raw[1] = raw[2] = raw[3] = raw[4] = raw[6] = raw[7] = raw[8] = raw[9] = raw[11] = 0;

		matrix.copyRawDataFrom(raw);
		matrix.appendScale(.96, .96, 1);
		matrix.appendTranslation(texOffsetX, texOffsetY, 0);
		matrix.appendScale(.5, .5, 1);
	}

	get _iNearPlaneDistances():Array<number>
	{
		return this._nearPlaneDistances;
	}
}

export default CascadeShadowMapper;