/*******************************************************************************
 * 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;

import com.badlogic.gdx.Application.ApplicationType;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.graphics.Cubemap;
import com.badlogic.gdx.graphics.Cubemap.CubemapSide;
import com.badlogic.gdx.graphics.g3d.Environment;
import com.badlogic.gdx.graphics.g3d.Model;
import com.badlogic.gdx.graphics.g3d.ModelBatch;
import com.badlogic.gdx.graphics.g3d.ModelInstance;
import com.badlogic.gdx.graphics.g3d.Renderable;
import com.badlogic.gdx.graphics.g3d.Shader;
import com.badlogic.gdx.graphics.g3d.attributes.ColorAttribute;
import com.badlogic.gdx.graphics.g3d.attributes.CubemapAttribute;
import com.badlogic.gdx.graphics.g3d.environment.DirectionalLight;
import com.badlogic.gdx.graphics.g3d.model.Animation;
import com.badlogic.gdx.graphics.g3d.shaders.BaseShader;
import com.badlogic.gdx.graphics.g3d.shaders.DefaultShader;
import com.badlogic.gdx.graphics.g3d.utils.AnimationController;
import com.badlogic.gdx.graphics.g3d.utils.DefaultShaderProvider;
import com.badlogic.gdx.graphics.glutils.FacedCubemapData;
import com.badlogic.gdx.math.Quaternion;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.math.collision.BoundingBox;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.ui.List;
import com.badlogic.gdx.scenes.scene2d.utils.ClickListener;
import com.badlogic.gdx.tests.g3d.shaders.MultiPassShader;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.GdxRuntimeException;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.StringBuilder;

public class ShaderCollectionTest extends BaseG3dHudTest {
	/** Desktop only: Set this to an absolute path to load the shader files from an alternative location. */
	final static String hotLoadFolder = null;
	/** Desktop only: Set this to an absolute path to save the generated shader files. */
	final static String tempFolder = System.getProperty("java.io.tmp");

	protected String shaders[] = new String[] {"<default>", "depth", "gouraud", "phong", "normal", "fur", "cubemap", "reflect",
		"test"};

	protected String environments[] = new String[] {"<none>", "debug", "environment_01", "environment_02"};

	protected String materials[] = new String[] {"diffuse_green", "badlogic_normal", "brick01", "brick02", "brick03",
		"chesterfield", "cloth01", "cloth02", "elephant01", "elephant02", "fur01", "grass01", "metal01", "metal02", "mirror01",
		"mirror02", "moon01", "plastic01", "stone01", "stone02", "wood01", "wood02"};

	public static class TestShaderProvider extends DefaultShaderProvider {
		public boolean error = false;
		public String name = "default";

		public void clear () {
			for (final Shader shader : shaders)
				shader.dispose();
			shaders.clear();
		}

		public boolean revert () {
			if (config.vertexShader == null || config.fragmentShader == null) return false;
			config.vertexShader = null;
			config.fragmentShader = null;
			clear();
			return true;
		}

		@Override
		public Shader getShader (Renderable renderable) {
			try {
				return super.getShader(renderable);
			} catch (Throwable e) {
				if (tempFolder != null && Gdx.app.getType() == ApplicationType.Desktop)
					Gdx.files.absolute(tempFolder).child(name + ".log.txt").writeString(e.getMessage(), false);
				if (!revert()) {
					Gdx.app.error("ShaderCollectionTest", e.getMessage());
					throw new GdxRuntimeException("Error creating shader, cannot revert to default shader", e);
				}
				error = true;
				Gdx.app.error("ShaderTest", "Could not create shader, reverted to default shader.", e);
				return super.getShader(renderable);
			}
		}

		@Override
		protected Shader createShader (Renderable renderable) {
			if (config.vertexShader != null && config.fragmentShader != null && tempFolder != null
				&& Gdx.app.getType() == ApplicationType.Desktop) {
				String prefix = DefaultShader.createPrefix(renderable, config);
				Gdx.files.absolute(tempFolder).child(name + ".vertex.glsl").writeString(prefix + config.vertexShader, false);
				Gdx.files.absolute(tempFolder).child(name + ".fragment.glsl").writeString(prefix + config.fragmentShader, false);
			}
			BaseShader result = new MultiPassShader(renderable, config);
			if (tempFolder != null && Gdx.app.getType() == ApplicationType.Desktop)
				Gdx.files.absolute(tempFolder).child(name + ".log.txt").writeString(result.program.getLog(), false);
			return result;
		}
	}

	protected Environment environment;
	protected DirectionalLight dirLight;
	protected TestShaderProvider shaderProvider;
	protected FileHandle shaderRoot;
	protected ModelBatch shaderBatch;
	protected CollapsableWindow shadersWindow, materialsWindow, environmentsWindow;
	protected ObjectMap<ModelInstance, AnimationController> animationControllers = new ObjectMap<ModelInstance, AnimationController>();
	protected String currentModel = null;
	protected String currentMaterial = null;
	protected boolean loadingMaterial = false;
	Cubemap cubemap;

	@Override
	public void create () {
		super.create();
		environment = new Environment();
		environment.set(new ColorAttribute(ColorAttribute.AmbientLight, 0.1f, 0.1f, 0.1f, 1.f));
		environment.add(dirLight = new DirectionalLight().set(0.8f, 0.8f, 0.8f, -0.5f, -1.0f, -0.8f));

		shaderProvider = new TestShaderProvider();
		shaderBatch = new ModelBatch(shaderProvider);

		cam.position.set(1, 1, 1);
		cam.lookAt(0, 0, 0);
		cam.update();
		showAxes = true;

		onModelClicked("g3d/shapes/teapot.g3dj");

		shaderRoot = (hotLoadFolder != null && Gdx.app.getType() == ApplicationType.Desktop) ? Gdx.files.absolute(hotLoadFolder)
			: Gdx.files.internal("data/g3d/shaders");
	}

	@Override
	public void dispose () {
		shaderBatch.dispose();
		shaderBatch = null;
		shaderProvider = null;
		if (cubemap != null) cubemap.dispose();
		cubemap = null;
		super.dispose();
	}

	public void setEnvironment (String name) {
		if (name == null) return;
		if (cubemap != null) {
			cubemap.dispose();
			cubemap = null;
		}
		if (name.equals("<none>")) {
			if (environment.has(CubemapAttribute.EnvironmentMap)) {
				environment.remove(CubemapAttribute.EnvironmentMap);
				shaderProvider.clear();
			}
		} else {
			FileHandle root = Gdx.files.internal("data/g3d/environment");
			FacedCubemapData faces = new FacedCubemapData(root.child(name + "_PX.png"), root.child(name + "_NX.png"),
				root.child(name + "_PY.png"), root.child(name + "_NY.png"), root.child(name + "_PZ.png"),
				root.child(name + "_NZ.png"), false); // FIXME mipmapping on desktop
			cubemap = new Cubemap(faces);
			faces.load(CubemapSide.NegativeX, root.child(name + "_NX.png"));
			cubemap.load(faces);
			if (!environment.has(CubemapAttribute.EnvironmentMap)) shaderProvider.clear();
			environment.set(new CubemapAttribute(CubemapAttribute.EnvironmentMap, cubemap));
		}
	}

	public void setMaterial (String name) {
		if (name == null) return;
		if (currentlyLoading != null) {
			Gdx.app.error("ModelTest", "Wait for the current model/material to be loaded.");
			return;
		}

		currentlyLoading = "data/g3d/materials/" + name + ".g3dj";
		loadingMaterial = true;
		if (!name.equals(currentMaterial)) assets.load(currentlyLoading, Model.class);
		loading = true;
	}

	public void setShader (String name) {
		shaderProvider.error = false;
		if (name.equals("<default>")) {
			shaderProvider.config.vertexShader = null;
			shaderProvider.config.fragmentShader = null;
			shaderProvider.name = "default";
		} else {
			ShaderLoader loader = new ShaderLoader(shaderRoot);
			shaderProvider.config.vertexShader = loader.load(name + ".glsl:VS");
			shaderProvider.config.fragmentShader = loader.load(name + ".glsl:FS");
			shaderProvider.name = name;
		}
		shaderProvider.clear();
	}

	private final Vector3 tmpV = new Vector3();
	private final Quaternion tmpQ = new Quaternion();
	private final BoundingBox bounds = new BoundingBox();

	@Override
	protected void render (ModelBatch batch, Array<ModelInstance> instances) {
	}

	final Vector3 dirLightRotAxis = new Vector3(-1, -1, -1).nor();

	@Override
	public void render (Array<ModelInstance> instances) {
		dirLight.direction.rotate(dirLightRotAxis, Gdx.graphics.getDeltaTime() * 45f);

		super.render(null);
		for (ObjectMap.Entry<ModelInstance, AnimationController> e : animationControllers.entries())
			e.value.update(Gdx.graphics.getDeltaTime());
		shaderBatch.begin(cam);
		shaderBatch.render(instances, environment);
		shaderBatch.end();
	}

	@Override
	protected void getStatus (StringBuilder stringBuilder) {
		super.getStatus(stringBuilder);

		if (shaderProvider.error)
			stringBuilder.append(" ERROR CREATING SHADER, REVERTED TO DEFAULT");
		else {
			for (final ModelInstance instance : instances) {
				if (instance.animations.size > 0) {
					stringBuilder.append(" press space or menu to switch animation");
					break;
				}
			}
		}
	}

	protected String currentlyLoading;

	@Override
	protected void onModelClicked (final String name) {
		if (name == null) return;
		if (currentlyLoading != null) {
			Gdx.app.error("ModelTest", "Wait for the current model/material to be loaded.");
			return;
		}

		currentlyLoading = "data/" + name;
		loadingMaterial = false;
		if (!name.equals(currentModel)) assets.load(currentlyLoading, Model.class);
		loading = true;
	}

	@Override
	protected void onLoaded () {
		if (currentlyLoading == null || currentlyLoading.length() == 0) return;

		if (loadingMaterial) {
			loadingMaterial = false;
			if (currentMaterial != null && !currentMaterial.equals(currentlyLoading)) assets.unload(currentMaterial);
			currentMaterial = currentlyLoading;
			currentlyLoading = null;
			ModelInstance instance = instances.get(0);
			if (instance != null) {
				instance.materials.get(0).clear();
				instance.materials.get(0).set(assets.get(currentMaterial, Model.class).materials.get(0));
			}
		} else {
			if (currentModel != null && !currentModel.equals(currentlyLoading)) assets.unload(currentModel);
			currentModel = currentlyLoading;
			currentlyLoading = null;

			instances.clear();
			animationControllers.clear();
			final ModelInstance instance = new ModelInstance(assets.get(currentModel, Model.class), transform);
			instances.add(instance);
			if (instance.animations.size > 0) animationControllers.put(instance, new AnimationController(instance));

			instance.calculateBoundingBox(bounds);
			cam.position.set(1, 1, 1).nor().scl(bounds.getDimensions(tmpV).len() * 0.75f).add(bounds.getCenter(tmpV));
			cam.up.set(0, 1, 0);
			cam.lookAt(inputController.target.set(bounds.getCenter(tmpV)));
			cam.far = Math.max(100f, bounds.getDimensions(tmpV).len() * 2.0f);
			cam.update();
			moveRadius = bounds.getDimensions(tmpV).len() * 0.25f;
		}
	}

	@Override
	protected void createHUD () {
		super.createHUD();

		final List<String> shadersList = new List(skin);
		shadersList.setItems(shaders);
		shadersList.addListener(new ClickListener() {
			@Override
			public void clicked (InputEvent event, float x, float y) {
				if (!shadersWindow.isCollapsed() && getTapCount() == 2) {
					setShader(shadersList.getSelected());
					shadersWindow.collapse();
				}
			}
		});
		shadersWindow = addListWindow("Shaders", shadersList, -1, -1);

		final List<String> materialsList = new List(skin);
		materialsList.setItems(materials);
		materialsList.addListener(new ClickListener() {
			@Override
			public void clicked (InputEvent event, float x, float y) {
				if (!materialsWindow.isCollapsed() && getTapCount() == 2) {
					setMaterial(materialsList.getSelected());
					materialsWindow.collapse();
				}
			}
		});
		materialsWindow = addListWindow("Materials", materialsList, modelsWindow.getWidth(), -1);

		final List<String> environmentsList = new List(skin);
		environmentsList.setItems(environments);
		environmentsList.addListener(new ClickListener() {
			@Override
			public void clicked (InputEvent event, float x, float y) {
				if (!environmentsWindow.isCollapsed() && getTapCount() == 2) {
					setEnvironment(environmentsList.getSelected());
					environmentsWindow.collapse();
				}
			}
		});
		environmentsWindow = addListWindow("Environments", environmentsList, materialsWindow.getRight(), -1);
	}

	protected void switchAnimation () {
		for (ObjectMap.Entry<ModelInstance, AnimationController> e : animationControllers.entries()) {
			int animIndex = 0;
			if (e.value.current != null) {
				for (int i = 0; i < e.key.animations.size; i++) {
					final Animation animation = e.key.animations.get(i);
					if (e.value.current.animation == animation) {
						animIndex = i;
						break;
					}
				}
			}
			animIndex = (animIndex + 1) % e.key.animations.size;
			e.value.animate(e.key.animations.get(animIndex).id, -1, 1f, null, 0.2f);
		}
	}

	@Override
	public boolean keyUp (int keycode) {
		if (keycode == Keys.SPACE || keycode == Keys.MENU) switchAnimation();
		return super.keyUp(keycode);
	}
}
