/*******************************************************************************
 * Copyright 2011 See AUTHORS file.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/

package com.badlogic.gdx.tests.g3d.shadows.system;

import java.util.EnumSet;
import java.util.Set;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Cubemap;
import com.badlogic.gdx.graphics.Cubemap.CubemapSide;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.graphics.g3d.RenderableProvider;
import com.badlogic.gdx.graphics.g3d.environment.BaseLight;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.environment.PointLight;
import com.badlogic.gdx.graphics.g3d.environment.SpotLight;
import com.badlogic.gdx.graphics.g3d.utils.ShaderProvider;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.tests.g3d.shadows.utils.AABBNearFarAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.BoundingSphereDirectionalAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.DirectionalAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.FixedShadowMapAllocator;
import com.badlogic.gdx.tests.g3d.shadows.utils.FrustumLightFilter;
import com.badlogic.gdx.tests.g3d.shadows.utils.LightFilter;
import com.badlogic.gdx.tests.g3d.shadows.utils.NearFarAnalyzer;
import com.badlogic.gdx.tests.g3d.shadows.utils.ShadowMapAllocator;
import com.badlogic.gdx.tests.g3d.shadows.utils.ShadowMapAllocator.ShadowMapRegion;
import com.badlogic.gdx.utils.Disposable;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.ObjectMap.Entries;

/** BaseShadowSystem allows to easily create custom shadow system.
 * @author realitix */
public abstract class BaseShadowSystem implements ShadowSystem, Disposable {
	/** This class handles camera and texture region.
	 * @author realitix */
	public static class LightProperties {
		public Camera camera;
		public TextureRegion region = new TextureRegion();

		public LightProperties (Camera camera) {
			this.camera = camera;
		}
	}

	/** This class handles LightProperties for each side of PointLight.
	 * @author realitix */
	public static class PointLightProperties {
		public ObjectMap<CubemapSide, LightProperties> properties = new ObjectMap<CubemapSide, LightProperties>(6);
	}

	/** Main camera */
	protected Camera camera;
	/** Renderable providers */
	protected Iterable<RenderableProvider> renderableProviders;
	/** Cameras linked with spot lights */
	protected ObjectMap<SpotLight, LightProperties> spotCameras = new ObjectMap<SpotLight, LightProperties>();
	/** Cameras linked with directional lights */
	protected ObjectMap<DirectionalLight, LightProperties> dirCameras = new ObjectMap<DirectionalLight, LightProperties>();
	/** Cameras linked with point lights */
	protected ObjectMap<PointLight, PointLightProperties> pointCameras = new ObjectMap<PointLight, PointLightProperties>();
	/** Analyzer of near and far for spot and point lights */
	protected NearFarAnalyzer nearFarAnalyzer;
	/** Allocator which choose where to render shadow map in texture */
	protected ShadowMapAllocator allocator;
	/** Analyzer which compute how to create the camera for directional light */
	protected DirectionalAnalyzer directionalAnalyzer;
	/** Filter that choose if light must be rendered */
	protected LightFilter lightFilter;
	/** Framebuffer used to render all the depth maps */
	protected FrameBuffer[] frameBuffers;
	/** Current pass in the depth process */
	protected int currentPass = -1;
	/** Iterators for cameras */
	protected Entries<SpotLight, LightProperties> spotCameraIterator;
	protected Entries<DirectionalLight, LightProperties> dirCameraIterator;
	protected Entries<PointLight, PointLightProperties> pointCameraIterator;
	/** Current side in the point light cubemap */
	protected int currentPointSide;
	protected PointLightProperties currentPointProperties;
	/** Shader providers used by this system */
	protected ShaderProvider[] passShaderProviders;
	protected ShaderProvider mainShaderProvider;
	/** Current light and properties during shadowmap generation */
	protected LightProperties currentLightProperties;
	protected BaseLight currentLight;

	/** Construct the system with the needed params.
	 * @param nearFarAnalyzer Analyzer of near and far
	 * @param allocator Allocator of shadow maps
	 * @param directionalAnalyzer Analyze directional light to create orthographic camera
	 * @param lightFilter Filter light to render */
	public BaseShadowSystem (NearFarAnalyzer nearFarAnalyzer, ShadowMapAllocator allocator,
		DirectionalAnalyzer directionalAnalyzer, LightFilter lightFilter) {
		this.nearFarAnalyzer = nearFarAnalyzer;
		this.allocator = allocator;
		this.directionalAnalyzer = directionalAnalyzer;
		this.lightFilter = lightFilter;
	}

	/** Construct the system with default values */
	public BaseShadowSystem () {
		this(new AABBNearFarAnalyzer(),
			new FixedShadowMapAllocator(FixedShadowMapAllocator.QUALITY_MED, FixedShadowMapAllocator.QUANTITY_MAP_MED),
			new BoundingSphereDirectionalAnalyzer(), new FrustumLightFilter());
	}

	/** Initialize framebuffers and shader providers. You should call super.init() in subclass. */
	@Override
	public void init () {
		frameBuffers = new FrameBuffer[getPassQuantity()];
		passShaderProviders = new ShaderProvider[getPassQuantity()];

		for (int i = 0; i < getPassQuantity(); i++) {
			init(i);
		}
	};

	/** Initialize pass n */
	protected abstract void init (int n);

	/** getPassQuantity should return at leat one. */
	@Override
	public abstract int getPassQuantity ();

	@Override
	public ShaderProvider getPassShaderProvider (int n) {
		return passShaderProviders[n];
	}

	@Override
	public ShaderProvider getShaderProvider () {
		return mainShaderProvider;
	}

	@Override
	public void addLight (SpotLight spot) {
		PerspectiveCamera camera = new PerspectiveCamera(spot.cutoffAngle, 0, 0);
		camera.position.set(spot.position);
		camera.direction.set(spot.direction);
		camera.near = 1;
		camera.far = 100;
		camera.up.set(camera.direction.y, camera.direction.z, camera.direction.x);

		spotCameras.put(spot, new LightProperties(camera));
	}

	@Override
	public void addLight (DirectionalLight dir) {
		OrthographicCamera camera = new OrthographicCamera();
		camera.direction.set(dir.direction);
		camera.near = 1;
		camera.far = 100;

		dirCameras.put(dir, new LightProperties(camera));
	}

	@Override
	public void addLight (PointLight point) {
		addLight(point, EnumSet.of(CubemapSide.PositiveX, CubemapSide.NegativeX, CubemapSide.PositiveY, CubemapSide.NegativeY,
			CubemapSide.PositiveZ, CubemapSide.NegativeZ));
	}

	@Override
	public void addLight (PointLight point, Set<CubemapSide> sides) {
		PointLightProperties plProperty = new PointLightProperties();
		for (int i = 0; i < 6; i++) {
			CubemapSide cubemapSide = Cubemap.CubemapSide.values()[i];
			if (sides.contains(cubemapSide)) {
				PerspectiveCamera camera = new PerspectiveCamera(90, 0, 0);
				camera.position.set(point.position);
				camera.direction.set(cubemapSide.direction);
				camera.up.set(cubemapSide.up);
				camera.near = 1;
				camera.far = 100;

				LightProperties p = new LightProperties(camera);
				plProperty.properties.put(cubemapSide, p);
			}
		}
		pointCameras.put(point, plProperty);
	}

	@Override
	public void removeLight (SpotLight spot) {
		spotCameras.remove(spot);
	}

	@Override
	public void removeLight (DirectionalLight dir) {
		dirCameras.remove(dir);
	}

	@Override
	public void removeLight (PointLight point) {
		pointCameras.remove(point);
	}

	@Override
	public boolean hasLight (SpotLight spot) {
		if (spotCameras.containsKey(spot)) return true;
		return false;
	}

	@Override
	public boolean hasLight (DirectionalLight dir) {
		if (dirCameras.containsKey(dir)) return true;
		return false;
	}

	@Override
	public boolean hasLight (PointLight point) {
		if (pointCameras.containsKey(point)) return true;
		return false;
	}

	@Override
	public void update () {
		for (ObjectMap.Entry<SpotLight, LightProperties> e : spotCameras) {
			e.value.camera.position.set(e.key.position);
			e.value.camera.direction.set(e.key.direction);
			nearFarAnalyzer.analyze(e.key, e.value.camera, renderableProviders);
		}

		for (ObjectMap.Entry<DirectionalLight, LightProperties> e : dirCameras) {
			directionalAnalyzer.analyze(e.key, e.value.camera, camera).update();
		}

		for (ObjectMap.Entry<PointLight, PointLightProperties> e : pointCameras) {
			for (ObjectMap.Entry<CubemapSide, LightProperties> c : e.value.properties) {
				c.value.camera.position.set(e.key.position);
				nearFarAnalyzer.analyze(e.key, c.value.camera, renderableProviders);
			}
		}
	}

	@Override
	public <T extends RenderableProvider> void begin (Camera camera, final Iterable<T> renderableProviders) {
		if (this.renderableProviders != null || this.camera != null) throw new GdxRuntimeException("Call end() first.");

		this.camera = camera;
		this.renderableProviders = (Iterable<RenderableProvider>)renderableProviders;
	}

	@Override
	public void begin (int n) {
		if (n >= passShaderProviders.length)
			throw new GdxRuntimeException("Pass " + n + " doesn't exist in " + getClass().getName());

		currentPass = n;
		spotCameraIterator = spotCameras.iterator();
		dirCameraIterator = dirCameras.iterator();
		pointCameraIterator = pointCameras.iterator();
		currentPointSide = 6;

		beginPass(n);
	}

	/** Begin pass n.
	 * @param n Pass number */
	protected void beginPass (int n) {
		frameBuffers[n].begin();
	};

	@Override
	public void end () {
		this.camera = null;
		this.renderableProviders = null;
		currentPass = -1;
	}

	@Override
	public void end (int n) {
		if (currentPass != n) throw new GdxRuntimeException("Begin " + n + " must be called before end " + n);
		endPass(n);
	}

	/** End pass n.
	 * @param n Pass number */
	protected void endPass (int n) {
		frameBuffers[n].end();
	}

	@Override
	public Camera next () {
		LightProperties lp = nextDirectional();
		if (lp != null) return interceptCamera(lp);

		lp = nextSpot();
		if (lp != null) return interceptCamera(lp);

		lp = nextPoint();
		if (lp != null) return interceptCamera(lp);

		return null;
	}

	/** Allows to return custom camera if needed.
	 * @param lp Returned LightProperties
	 * @return Camera */
	protected Camera interceptCamera (LightProperties lp) {
		return lp.camera;
	}

	protected LightProperties nextDirectional () {
		if (!dirCameraIterator.hasNext()) return null;

		ObjectMap.Entry<DirectionalLight, LightProperties> e = dirCameraIterator.next();
		currentLight = e.key;
		currentLightProperties = e.value;
		LightProperties lp = e.value;
		processViewport(lp, false);
		return lp;
	}

	protected LightProperties nextSpot () {
		if (!spotCameraIterator.hasNext()) return null;

		ObjectMap.Entry<SpotLight, LightProperties> e = spotCameraIterator.next();
		currentLight = e.key;
		currentLightProperties = e.value;
		LightProperties lp = e.value;

		if (!lightFilter.filter(spotCameras.findKey(lp, true), lp.camera, this.camera)) {
			return nextSpot();
		}

		processViewport(lp, true);
		return lp;
	}

	protected LightProperties nextPoint () {
		if (!pointCameraIterator.hasNext() && currentPointSide > 5) return null;

		if (currentPointSide > 5) currentPointSide = 0;

		if (currentPointSide == 0) {
			ObjectMap.Entry<PointLight, PointLightProperties> e = pointCameraIterator.next();
			currentLight = e.key;
			currentPointProperties = e.value;
		}

		if (currentPointProperties.properties.containsKey(Cubemap.CubemapSide.values()[currentPointSide])) {
			LightProperties lp = currentPointProperties.properties.get(Cubemap.CubemapSide.values()[currentPointSide]);
			currentLightProperties = lp;
			currentPointSide += 1;

			if (!lightFilter.filter(pointCameras.findKey(currentPointProperties, true), lp.camera, this.camera)) {
				return nextPoint();
			}

			processViewport(lp, true);
			return lp;
		}

		currentPointSide += 1;
		return nextPoint();
	}

	/** Set viewport according to allocator.
	 * @param lp LightProperties to process.
	 * @param cameraViewport Set camera viewport if true. */
	protected void processViewport (LightProperties lp, boolean cameraViewport) {
		Camera camera = lp.camera;
		ShadowMapRegion r = allocator.nextResult(currentLight);

		if (r == null) return;

		TextureRegion region = lp.region;
		region.setTexture(frameBuffers[currentPass].getColorBufferTexture());

		// We don't use HdpiUtils
		// gl commands related to shadow map size and not to screen size
		Gdx.gl.glViewport(r.x, r.y, r.width, r.height);
		Gdx.gl.glScissor(r.x + 1, r.y + 1, r.width - 2, r.height - 2);
		region.setRegion(r.x, r.y, r.width, r.height);

		if (cameraViewport) {
			camera.viewportHeight = r.height;
			camera.viewportWidth = r.width;
			camera.update();
		}
	}

	public ObjectMap<DirectionalLight, LightProperties> getDirectionalCameras () {
		return dirCameras;
	}

	public ObjectMap<SpotLight, LightProperties> getSpotCameras () {
		return spotCameras;
	}

	public ObjectMap<PointLight, PointLightProperties> getPointCameras () {
		return pointCameras;
	}

	public Texture getTexture (int n) {
		if (n >= getPassQuantity()) throw new GdxRuntimeException("Can't get texture " + n);
		return frameBuffers[n].getColorBufferTexture();
	}

	public LightProperties getCurrentLightProperties () {
		return currentLightProperties;
	}

	public BaseLight getCurrentLight () {
		return currentLight;
	}

	public int getCurrentPass () {
		return currentPass;
	}

	@Override
	public void dispose () {
		for (int i = 0; i < getPassQuantity(); i++) {
			frameBuffers[i].dispose();
			passShaderProviders[i].dispose();
		}
		mainShaderProvider.dispose();
	}
}
