package com.bitfire.uracer.game.logic.post.effects;

import java.nio.ByteBuffer;

import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.Pixmap;
import com.badlogic.gdx.graphics.Pixmap.Format;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureFilter;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.glutils.FrameBuffer;
import com.badlogic.gdx.graphics.glutils.ShaderProgram;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Matrix3;
import com.badlogic.gdx.math.Matrix4;
import com.bitfire.postprocessing.PostProcessorEffect;
import com.bitfire.postprocessing.filters.Blur;
import com.bitfire.postprocessing.filters.Blur.BlurType;
import com.bitfire.postprocessing.utils.FullscreenQuad;
import com.bitfire.postprocessing.utils.PingPongBuffer;
import com.bitfire.uracer.configuration.Config;
import com.bitfire.uracer.game.GameEvents;
import com.bitfire.uracer.game.events.GameRendererEvent;
import com.bitfire.uracer.game.events.GameRendererEvent.Order;
import com.bitfire.uracer.game.events.GameRendererEvent.Type;
import com.bitfire.uracer.utils.ScaleUtils;
import com.bitfire.utils.ShaderLoader;

public final class Ssao extends PostProcessorEffect {

	public enum Quality {
		Ultra(1), High(0.75f), Normal(0.5f);
		public final float scale;

		Quality (float scale) {
			this.scale = scale;
		}
	}

	private final PingPongBuffer occlusionMap;
	private Texture normalDepthMap;
	private final ShaderProgram shMix, shSsao;
	private final FullscreenQuad quad = new FullscreenQuad();
	private Texture randomField;
	private Blur blur;

	private Matrix3 mtxRot = new Matrix3();
	private Matrix3 invRot = new Matrix3();
	private Matrix4 invPrj = new Matrix4();

	private GameRendererEvent.Listener gameRendererEvent = new GameRendererEvent.Listener() {
		@Override
		public void handle (Object source, Type type, Order order) {
			debug(GameEvents.gameRenderer.batch);
		}
	};

	public Ssao (int fboWidth, int fboHeight, Quality quality) {
		Gdx.app.log("SsaoProcessor", "Quality profile = " + quality.toString());
		float oscale = quality.scale;

		// maps
		occlusionMap = new PingPongBuffer((int)((float)fboWidth * oscale), (int)((float)fboHeight * oscale), Format.RGBA8888, false);

		// shaders
		shMix = ShaderLoader.fromFile("screenspace", "ssao/mix");
		shSsao = ShaderLoader.fromFile("ssao/ssao", "ssao/ssao");

		// blur
		blur = new Blur(occlusionMap.width, occlusionMap.height);
		blur.setType(BlurType.Gaussian5x5b);
		blur.setPasses(2);

		createRandomField(16, 16, Format.RGBA8888);
		// enableDebug();
	}

	/** Computes random field for the ssao shader */
	private void createRandomField (int width, int height, Format format) {
		randomField = new Texture(width, height, format);
		randomField.setFilter(TextureFilter.Nearest, TextureFilter.Nearest);
		randomField.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);

		Pixmap pixels = new Pixmap(width, height, format);
		ByteBuffer bytes = pixels.getPixels();
		int wrote = 0;

		while (wrote < width * height * 4) {
			float x = (MathUtils.random() - 0.5f) * 2.0f;
			float y = (MathUtils.random() - 0.5f) * 2.0f;
			float z = (MathUtils.random() - 0.5f) * 2.0f;
			float l = (float)Math.sqrt(x * x + y * y + z * z);
			if (l <= 1.0f && l > 0.1f) {
				x = (x + 1.0f) * 0.5f;
				y = (y + 1.0f) * 0.5f;
				z = (z + 1.0f) * 0.5f;
				bytes.put((byte)(x * 255f));
				bytes.put((byte)(y * 255f));
				bytes.put((byte)(z * 255f));
				bytes.put((byte)255);
				wrote += 4;
			}
		}

		bytes.flip();
		randomField.draw(pixels, 0, 0);
		pixels.dispose();
	}

	@Override
	public void dispose () {
		disableDebug();
		randomField.dispose();
		blur.dispose();
		shSsao.dispose();
		shMix.dispose();
		occlusionMap.dispose();
	}

	public void enableDebug () {
		GameEvents.gameRenderer.addListener(gameRendererEvent, GameRendererEvent.Type.BatchDebug, GameRendererEvent.Order.DEFAULT);
	}

	public void disableDebug () {
		GameEvents.gameRenderer.removeListener(gameRendererEvent, GameRendererEvent.Type.BatchDebug,
			GameRendererEvent.Order.DEFAULT);
	}

	public void setOcclusionThresholds (float no_occlusion, float full_occlusion) {
		shSsao.begin();
		shSsao.setUniformf("full_occlusion_treshold", full_occlusion);
		shSsao.setUniformf("no_occlusion_treshold", no_occlusion);
		shSsao.end();
	}

	public void setNormalDepthMap (Texture normalDepthMap) {
		this.normalDepthMap = normalDepthMap;
	}

	public void setRadius (float epsilon, float radius) {
		shSsao.begin();
		shSsao.setUniformf("radius", radius);
		shSsao.setUniformf("epsilon", epsilon);
		shSsao.end();
	}

	public void setPower (float power, float occlusion_power) {
		shSsao.begin();
		shSsao.setUniformf("occlusion_power", occlusion_power);
		shSsao.setUniformf("power", power);
		shSsao.end();
	}

	public void setPatternSize (int pattern_size) {
		shSsao.begin();
		shSsao.setUniformf("pattern_size", pattern_size);
		shSsao.end();
	}

	public void setSampleCount (int sample_count) {
		shSsao.begin();
		shSsao.setUniformi("sample_count", sample_count);
		shSsao.end();
	}

	private void dbgTextureW (SpriteBatch batch, float width, Texture tex, int index) {
		if (tex == null) return;

		float h = width / ScaleUtils.RefAspect;
		float x = Config.Graphics.ReferenceScreenWidth - width - 10;
		float y = index * 10;
		batch.draw(tex, x, y, width, h);
	}

	private void debug (SpriteBatch batch) {
		Gdx.gl.glDisable(GL20.GL_BLEND);
		Gdx.gl.glDisable(GL20.GL_CULL_FACE);
		Gdx.gl.glDisable(GL20.GL_DEPTH_TEST);
		batch.disableBlending();

		dbgTextureW(batch, 180, normalDepthMap, 12);
		dbgTextureW(batch, 360, occlusionMap.getResultTexture(), 24);
	}

	@Override
	public void render (final FrameBuffer src, final FrameBuffer dest) {
		Texture tsrc = src.getColorBufferTexture();

		Gdx.gl.glDisable(GL20.GL_CULL_FACE);
		Gdx.gl.glDisable(GL20.GL_DEPTH_TEST);

		Camera cam = GameEvents.gameRenderer.camPersp;

		mtxRot.set(cam.view);
		invPrj.set(cam.projection).inv();
		// invRot.set(mtxRot).inv();

		occlusionMap.begin();
		occlusionMap.capture();
		{
			shSsao.begin();
			{
				Gdx.gl.glClearColor(0, 0, 0, 1);
				Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

				// samplers
				normalDepthMap.bind(0);
				randomField.bind(1);

				shSsao.setUniformi("normaldepth", 0);
				shSsao.setUniformi("random_field", 1);

				shSsao.setUniformMatrix("proj", cam.projection);
				shSsao.setUniformMatrix("inv_proj", invPrj);
				shSsao.setUniformMatrix("inv_rot", invRot);

				shSsao.setUniformf("viewport", occlusionMap.width, occlusionMap.height);
				shSsao.setUniformf("near", cam.near);
				shSsao.setUniformf("far", cam.far);

				quad.render(shSsao);
			}
			shSsao.end();

			// blur pass
			blur.render(occlusionMap);
		}
		occlusionMap.end();

		restoreViewport(dest);

		if (dest != null) dest.begin();
		shMix.begin();
		{
			tsrc.bind(0);
			occlusionMap.getResultTexture().bind(1);

			shMix.setUniformi("scene", 0);
			shMix.setUniformi("occlusion_map", 1);

			quad.render(shMix);
		}
		shMix.end();
		if (dest != null) dest.end();
	}

	@Override
	public void rebind () {
	}
}