﻿/*
 * File:			GAFBakedFilteredObjectImpl_Pro.cs
 * Version:			1.0
 * Last changed:	2014/12/12 16:54
 * Author:			Alexey_Nikitin
 * Copyright:		© GAF Media
 * Project:			UnityVS.UnityProject.CSharp
 */

using UnityEngine;

using GAF.Data;
using System.Linq;

namespace GAF.Objects
{
	public class GAFBakedFilteredObjectImpl_Pro : GAFBakedObjectImpl
	{
		#region Members

		private static readonly float GaussianKernelSize = 9;

		private static readonly Vector2[] filterUV = new Vector2[4]
		{
			  new Vector2(0,0)
			, new Vector2(0,1)
			, new Vector2(1,1)
			, new Vector2(1,0)
		};

		private Vector2[] m_InitialUV = null;

		private Material m_SharedMaterial = null;
		private Material m_FilterMaterial = null;
		private Material m_FilteredMaterial = null;

		private RenderTexture	m_FilteredTexture	= null;
		private Mesh			m_DropShadowMesh	= null;

		private bool m_UpdateFilterData = false;

		#endregion // Members

		#region Interface

		public GAFBakedFilteredObjectImpl_Pro(
			  IGAFObjectSerializedProperties	_Data
			, GAFRenderProcessor				_Processor
			, GAFBakedObjectController			_Controller)
			: base(_Data, _Processor, _Controller)
		{
			m_InitialUV = uvs;

			m_SharedMaterial = material;
			m_FilteredMaterial = new Material(Shader.Find("GAF/GAFFilteredObject(Pro)"));

			serializedProperties.manager.onWillRenderObject += onWillRenderObject;
			serializedProperties.manager.onLateUpdate		+= onLateUpdate;	
		}

		public override void updateToState(GAFObjectStateData _State, bool _Refresh)
		{
			updateFilters(_State, _Refresh);

			if (material != m_SharedMaterial)
				updateMaterialColor(_State, _Refresh);
			else
				updateMeshColor(_State, _Refresh);

			updateTransform(_State, _Refresh);
		}

		public void updateFilters(GAFObjectStateData _State, bool _Refresh)
		{
			if (_State.filterData != currentState.filterData ||
				_Refresh)
			{
				deleteObject(m_FilterMaterial);
				deleteObject(m_FilteredTexture);
				deleteObject(m_DropShadowMesh);

				if (_State.filterData == null)
				{
					material = m_SharedMaterial;
					uvs = m_InitialUV;
					m_UpdateFilterData = false;
				}
				else
				{
					setupMaterials(_State.filterData.type);
					setupUV(_State.filterData.type);
					m_UpdateFilterData = true;
				}

				currentState.filterData = _State.filterData;
			}
		}

		public override void cleanUp()
		{
			base.cleanUp();

			serializedProperties.manager.onWillRenderObject -= onWillRenderObject;
			serializedProperties.manager.onLateUpdate		-= onLateUpdate;

			m_FilterMaterial	= null;
			m_FilteredMaterial	= null;
			m_FilteredTexture	= null;
		}

		#endregion // Interface

		#region Properties

		public Material sharedMaterial
		{
			get
			{
				return m_SharedMaterial;
			}
		}

		public Material filterMaterial
		{
			get
			{
				return m_FilterMaterial;
			}
		}

		public Material filteredMaterial
		{
			get
			{
				return m_FilteredMaterial;
			}
		}

		#endregion // Properties

		#region Implementation

		private void onWillRenderObject()
		{
			if (m_UpdateFilterData)
			{
				switch (currentState.filterData.type)
				{
					case GAFFilterType.GFT_Blur: updateBlur(); break;
					case GAFFilterType.GFT_Glow: updateGlow(); break;
					case GAFFilterType.GFT_DropShadow: updateDropShadow(); break;
					case GAFFilterType.GFT_ColorMatrix: updateColorMtx(); break;
				}

				m_UpdateFilterData = false;
			}
		}

		private void onLateUpdate()
		{
			if (currentState != null &&
				currentState.filterData != null)
			{
				switch (currentState.filterData.type)
				{
					case GAFFilterType.GFT_DropShadow: renderShadow(); break;
				}
			}
		}

		private void setupMaterials(GAFFilterType _Type)
		{
			switch (_Type)
			{
				case GAFFilterType.GFT_Blur:
					m_FilterMaterial = new Material(Shader.Find("GAF/GAFBlur(Pro)"));
					m_FilteredMaterial.DisableKeyword("GAF_COLOR_MTX_FILTER_ON");
					m_FilteredMaterial.EnableKeyword("GAF_COLOR_MTX_FILTER_OFF");
					material = m_FilteredMaterial;
					break;

				case GAFFilterType.GFT_Glow:
					m_FilterMaterial = new Material(Shader.Find("GAF/GAFGlow(Pro)"));
					m_FilteredMaterial.DisableKeyword("GAF_COLOR_MTX_FILTER_ON");
					m_FilteredMaterial.EnableKeyword("GAF_COLOR_MTX_FILTER_OFF");
					material = m_FilteredMaterial;
					break;

				case GAFFilterType.GFT_DropShadow:
					m_FilterMaterial = new Material(Shader.Find("GAF/GAFGlow(Pro)"));
					m_FilteredMaterial.DisableKeyword("GAF_COLOR_MTX_FILTER_ON");
					m_FilteredMaterial.EnableKeyword("GAF_COLOR_MTX_FILTER_OFF");
					material = m_SharedMaterial;
					break;

				case GAFFilterType.GFT_ColorMatrix:
					m_FilterMaterial = null;
					m_FilteredMaterial.mainTexture = texture;
					m_FilteredMaterial.EnableKeyword("GAF_COLOR_MTX_FILTER_ON");
					m_FilteredMaterial.DisableKeyword("GAF_COLOR_MTX_FILTER_OFF");
					material = m_FilteredMaterial;
					break;
			}

			if (m_FilterMaterial != null)
			{
				m_FilterMaterial.mainTexture = texture;
				m_FilterMaterial.renderQueue = 3000;
			}
		}

		private void setupUV(GAFFilterType _Type)
		{
			switch (_Type)
			{
 				case GAFFilterType.GFT_Blur:
					uvs = filterUV;
					break;

				case GAFFilterType.GFT_Glow:
					uvs = filterUV;
					break;

				case GAFFilterType.GFT_DropShadow:
					uvs = m_InitialUV;
					break;

				case GAFFilterType.GFT_ColorMatrix:
					uvs = m_InitialUV;
					break;
			}
		}

		private void updateBlur()
		{
			if (m_FilterMaterial	!= null &&
				m_FilteredMaterial	!= null)
			{
				var clip = serializedProperties.clip;

				var elementSize = new Vector2(atlasElementData.width * clip.settings.csf, atlasElementData.height * clip.settings.csf);
				var blurSize = new Vector2(currentState.filterData.blurX / 4f, currentState.filterData.blurY / 4f);
				var rtSize = new Vector2(elementSize.x + GaussianKernelSize * blurSize.x, elementSize.y + GaussianKernelSize * blurSize.y);

				var buffer1 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				renderElementToRenderTexture(buffer1, elementSize, true);

				var buffer2 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				beginWithClear(buffer2);
				m_FilterMaterial.SetFloat("_BlurX", blurSize.x);
				m_FilterMaterial.SetFloat("_BlurY", 0);
				Graphics.Blit(buffer1, buffer2, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer1);
				deleteObject(buffer1);

				m_FilteredTexture = new RenderTexture((int)rtSize.x, (int)rtSize.y, 32);
				beginWithClear(m_FilteredTexture);
				m_FilterMaterial.SetFloat("_BlurX", 0);
				m_FilterMaterial.SetFloat("_BlurY", blurSize.y);
				Graphics.Blit(buffer2, m_FilteredTexture, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer2);
				deleteObject(buffer2);

				var meshCenter = new Vector3(
					  currentVertices.Select(vector => vector.x).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.y).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.z).Sum() / currentVertices.Length
					);

				m_FilteredMaterial.mainTexture = m_FilteredTexture;
				m_FilteredMaterial.SetVector("_Scale", new Vector4(rtSize.x / elementSize.x, rtSize.y / elementSize.y));
				m_FilteredMaterial.SetVector("_Pivot", meshCenter);
			}
		}

		private void updateGlow()
		{
			if (m_FilterMaterial	!= null &&
				m_FilteredMaterial	!= null)
			{
				var clip = serializedProperties.clip;

				var elementSize = new Vector2(atlasElementData.width * clip.settings.csf, atlasElementData.height * clip.settings.csf);
				var blurSize = new Vector2(currentState.filterData.blurX / 16f, currentState.filterData.blurY / 16f);
				var rtSize = new Vector2(elementSize.x + GaussianKernelSize * blurSize.x, elementSize.y + GaussianKernelSize * blurSize.y);

				var buffer1 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				renderElementToRenderTexture(buffer1, elementSize, true);

				var glowColorBytes = System.BitConverter.GetBytes(currentState.filterData.color).Reverse().ToArray();
				var glowColor = new Color(
					  (float)glowColorBytes[1] / 255f
					, (float)glowColorBytes[2] / 255f
					, (float)glowColorBytes[3] / 255f
					, (float)glowColorBytes[0] / 255f);

				var buffer2 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				beginWithClear(buffer2);
				m_FilterMaterial.SetFloat("_BlurX", blurSize.x);
				m_FilterMaterial.SetFloat("_BlurY", 0);
				m_FilterMaterial.SetColor("_GlowColor", glowColor);
				Graphics.Blit(buffer1, buffer2, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer1);
				deleteObject(buffer1);

				m_FilteredTexture = new RenderTexture((int)rtSize.x, (int)rtSize.y, 32);
				beginWithClear(m_FilteredTexture);
				m_FilterMaterial.SetFloat("_BlurX", 0);
				m_FilterMaterial.SetFloat("_BlurY", blurSize.y);
				m_FilterMaterial.SetColor("_GlowColor", glowColor);
				Graphics.Blit(buffer2, m_FilteredTexture, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer2);
				deleteObject(buffer2);

				renderElementToRenderTexture(m_FilteredTexture, elementSize, false);

				var meshCenter = new Vector3(
					  currentVertices.Select(vector => vector.x).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.y).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.z).Sum() / currentVertices.Length
					);

				m_FilteredMaterial.mainTexture = m_FilteredTexture;
				m_FilteredMaterial.SetVector("_Scale", new Vector4(rtSize.x / elementSize.x, rtSize.y / elementSize.y));
				m_FilteredMaterial.SetVector("_Pivot", meshCenter);
			}
		}

		private void updateDropShadow()
		{
			if (m_FilterMaterial	!= null &&
				m_FilteredMaterial	!= null)
			{
				var clip = serializedProperties.clip;

				var elementSize = new Vector2(atlasElementData.width * clip.settings.csf, atlasElementData.height * clip.settings.csf);
				var blurSize = new Vector2(currentState.filterData.blurX / 16f, currentState.filterData.blurY / 16f);
				var rtSize = new Vector2(elementSize.x + GaussianKernelSize * blurSize.x, elementSize.y + GaussianKernelSize * blurSize.y);

				var buffer1 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				renderElementToRenderTexture(buffer1, elementSize, true);

				var glowColorBytes = System.BitConverter.GetBytes(currentState.filterData.color).Reverse().ToArray();
				var glowColor = new Color(
					  (float)glowColorBytes[1] / 255f
					, (float)glowColorBytes[2] / 255f
					, (float)glowColorBytes[3] / 255f
					, (float)glowColorBytes[0] / 255f * Mathf.Clamp(currentState.filterData.strength, 0f, 1f));

				var buffer2 = RenderTexture.GetTemporary((int)rtSize.x, (int)rtSize.y);
				beginWithClear(buffer2);
				m_FilterMaterial.SetFloat("_BlurX", blurSize.x);
				m_FilterMaterial.SetFloat("_BlurY", 0);
				m_FilterMaterial.SetColor("_GlowColor", glowColor);
				Graphics.Blit(buffer1, buffer2, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer1);
				deleteObject(buffer1);

				m_FilteredTexture = new RenderTexture((int)rtSize.x, (int)rtSize.y, 32);
				beginWithClear(m_FilteredTexture);
				m_FilterMaterial.SetFloat("_BlurX", 0);
				m_FilterMaterial.SetFloat("_BlurY", blurSize.y);
				m_FilterMaterial.SetColor("_GlowColor", glowColor);
				Graphics.Blit(buffer2, m_FilteredTexture, m_FilterMaterial);
				RenderTexture.ReleaseTemporary(buffer2);
				deleteObject(buffer2);

				var meshCenter = new Vector3(
					  currentVertices.Select(vector => vector.x).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.y).Sum() / currentVertices.Length
					, currentVertices.Select(vector => vector.z).Sum() / currentVertices.Length
					);

				m_FilteredMaterial.mainTexture = m_FilteredTexture;
				m_FilteredMaterial.SetVector("_Scale", new Vector4(rtSize.x / elementSize.x, rtSize.y / elementSize.y));
				m_FilteredMaterial.SetVector("_Pivot", meshCenter);
			}
		}

		private void updateColorMtx()
		{
			if (m_FilteredMaterial != null)
			{
				m_FilteredMaterial.SetMatrix("_ColorMtx", currentState.filterData.colorMtx);
				m_FilteredMaterial.SetVector("_Offset", currentState.filterData.colorOffset);
			}
		}

		private void renderShadow()
		{
			if (m_FilteredMaterial != null &&
				serializedProperties.visible)
			{
				if (m_DropShadowMesh == null)
				{
					m_DropShadowMesh = copyMesh();
					m_DropShadowMesh.uv = filterUV;
				}

				var anglerad = (Mathf.PI / 180f) * currentState.filterData.angle;
				var offset = new Vector3(Mathf.Cos(anglerad) * currentState.filterData.distance, -Mathf.Sin(anglerad) * currentState.filterData.distance);
				var position = serializedProperties.clip.transform.position + offset + new Vector3(0, 0, 0.001f);

				Graphics.DrawMesh(
					  m_DropShadowMesh
					, position
					, serializedProperties.clip.transform.rotation
					, m_FilteredMaterial
					, serializedProperties.clip.settings.spriteLayerID
					, null);
			}
		}

		private void beginWithClear(RenderTexture _RT)
		{
			_RT.DiscardContents();

			var current = RenderTexture.active;
			RenderTexture.active = _RT;

			GL.Clear(true, true, new Color(0, 0, 0, 0));

			RenderTexture.active = current;

            _RT.MarkRestoreExpected();
        }

		private void renderElementToRenderTexture(RenderTexture _RenderTexture, Vector2 _ElementOriginalSize, bool _Clear)
		{
			if (_Clear)
				beginWithClear(_RenderTexture);

			var current = RenderTexture.active;
			RenderTexture.active = _RenderTexture;

			var elementVertexX = _ElementOriginalSize.x / (float)_RenderTexture.width;
			var elementVertexY = _ElementOriginalSize.y / (float)_RenderTexture.height;

			var mesh = copyMesh();

			mesh.vertices = new Vector3[4]
			{
				  new Vector3(-elementVertexX,  elementVertexY, 0)
				, new Vector3(-elementVertexX, -elementVertexY, 0)
				, new Vector3( elementVertexX, -elementVertexY, 0)
				, new Vector3( elementVertexX,  elementVertexY, 0)
			};

			mesh.uv = m_InitialUV;

			var mat = new Material(Shader.Find("GAF/GAFObjectsGroup"));
			mat.mainTexture = texture;
			mat.DisableKeyword("GAF_VERTICES_TRANSFORM_ON");
			mat.EnableKeyword("GAF_VERTICES_TRANSFORM_OFF");

			if (mat.SetPass(0))
			{
				Graphics.DrawMeshNow(mesh, Matrix4x4.identity);
			}

			deleteObject(mat);
			deleteObject(mesh);

			RenderTexture.active = current;
		}

		private void deleteObject(Object _Obj)
		{
			if (_Obj != null)
			{
				if (Application.isPlaying)
					Object.Destroy(_Obj);
				else
					Object.DestroyImmediate(_Obj);

				_Obj = null;
			}
		}

		private Mesh copyMesh()
		{
			var copy = new Mesh();

			copy.vertices	= currentVertices;
			copy.uv			= uvs;
			copy.triangles	= triangles;
			copy.normals	= normals;
			copy.colors32	= colors;
			copy.tangents	= colorsShift;

			return copy;
		}

		#endregion // Implementation
	}
}