using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System;
using System.IO;
using System.Reflection;
using SystemType = System.Type;

namespace UnityEditor
{
	public class GISSplatData : ScriptableObject
	{
		// Rows and Columns of texture
		public int rowCount = 0;
		public int columnCount = 0;

		// The given lat long of the combined texture from sources
		public double eastLongitude = 180;
		public double westLongitude = -180;
		public double northLatitude = 90;
		public double southLatitude = -90;

		[SerializeField]
		protected Texture2D[] m_Textures = null;

		public const int kMaxRowCount = 100;
		public const int kMaxColumnCount = 100;

		// Custom message ID for splat
		internal const uint kCreatedSplatMessageId = 0x00020001;

		// Flag to determine is terrain splat was created
		private bool TerrainSplatCreatedElsewhere = false;

		[MenuItem("GIS/Create Splat Data")]
		public static void CreateGISSplatAsset ()
		{
			// Find the path to current selection
			string path = "Assets";
			foreach (UnityEngine.Object obj in Selection.GetFiltered (typeof (UnityEngine.Object), SelectionMode.Assets))
			{
				path = AssetDatabase.GetAssetPath (obj);
				if (File.Exists (path))
				{
					path = Path.GetDirectoryName (path);
				}
				break;
			}

			GISSplatData gisSplatAsset = ScriptableObject.CreateInstance<GISSplatData> ();
			AssetDatabase.CreateAsset (gisSplatAsset, AssetDatabase.GenerateUniqueAssetPath (path + "/GISSplat.asset"));
			Selection.activeObject = gisSplatAsset;
		}

		public void OnEnable()
		{
			// Registering message ID and Listener to MessageBus
			BindingFlags flags = BindingFlags.Instance | BindingFlags.NonPublic;
			SystemType type = typeof (GISSplatData);
			MethodInfo method = type.GetMethod ("OnCreatedSplatMessage", flags);

			MessageBus.Instance.RegisterMessageId (kCreatedSplatMessageId);
			MessageBus.Instance.RegisterListener (kCreatedSplatMessageId, new MessageBusListener (this, method));
		}

		private void OnDisable ()
		{
			MessageBus.Instance.DeregisterListener (this);
		}

		private void OnCreatedSplatMessage()
		{
			TerrainSplatCreatedElsewhere = true;
		}

		public void Reset ()
		{
			m_Textures = null;	
		}

		private void ValidateRowCountSize ()
		{
			if (rowCount > kMaxRowCount || rowCount < 0)
			{
				Debug.LogWarning (String.Format ("Row count set beyond limit. Reseting back to {0}", kMaxRowCount));
				rowCount = kMaxRowCount;
			}

			if (columnCount > kMaxColumnCount || columnCount < 0)
			{
				Debug.LogWarning (String.Format ("Column count set beyond limit. Reseting back to {0}", kMaxColumnCount));
				columnCount = kMaxColumnCount;	
			}
		}

		// Initialize or resize texture array when rowCount and columnCount doesn't fit
		public void InitTextureArray ()
		{
			
			ValidateRowCountSize ();
			int size = rowCount * columnCount;
			if (size <= 0)
				return;

			if (m_Textures == null)
			{
				m_Textures = new Texture2D[size];
			}
			else if (m_Textures.Length != size)
			{
				// Destroy all stored m_Textures
				for (int i = 0; i < m_Textures.Length; ++i)
				{
					if (m_Textures[i])
						if (!EditorUtility.IsPersistent (m_Textures[i]))
							DestroyImmediate (m_Textures[i]);
				}
				System.Array.Resize (ref m_Textures, size);
			}

			EditorUtility.SetDirty (this);
		}

		public void SetGISImage (int row, int column, Texture2D texture)
		{
			int index = row * columnCount + column;
			if (index < m_Textures.Length)
			{
				m_Textures[index] = texture;
			}
		}

		public Texture2D GetGISImage (int row, int column)
		{
			int index = row * columnCount + column;
			if (index >= 0 && index < m_Textures.Length)
			{
				return m_Textures[index];
			}
			return null;
		}

		public Texture2D[] Textures
		{
			get { return m_Textures; }
		}		

		public int ImageWidth
		{
			get 
			{ 
				if (m_Textures == null || m_Textures.Length == 0)
				{
					return 0;
				}
				// Find the first non null texture and it will represent the size of all the textures
				foreach (Texture2D texture in m_Textures)
				{
					if (texture != null)
					{
						return texture.width;
					}
				}
				return 0; 
			}
		}

		public int ImageHeight
		{
			get 
			{ 
				if (m_Textures == null || m_Textures.Length == 0 )
				{
					return 0;
				}
				// Find the first non null texture and it will represent the size of all the textures
				foreach (Texture2D texture in m_Textures)
				{
					if (texture != null)
					{
						return texture.height;
					}
				}
				return 0;  
			}
		}

		// Helper function to calculate texture offset by giving the subset Geo coordintes
		public Rect CalculateTextureOffset (float eastLongitudeRequired, float westLongitudeRequired, float northLatitudeRequired, float southLatitudeRequired)
		{
			Rect offset  = new Rect ();
			
			{
				int totalImageWidth = columnCount * ImageWidth;
				int totalImageHeight = rowCount * ImageHeight;
				float longitudeLength = (float) ( eastLongitude - westLongitude );
				float latitudeLength = (float) ( northLatitude - southLatitude );

				offset.x = (float) ( ( westLongitudeRequired - westLongitude ) / longitudeLength);
				offset.x *= totalImageWidth;

				offset.width = (float) ( ( eastLongitudeRequired - westLongitudeRequired ) / longitudeLength); 
				offset.width *= totalImageWidth;

				offset.y = (float) ( ( northLatitude - northLatitudeRequired )/ latitudeLength);
				offset.y *= totalImageHeight;
				
				offset.height = (float) ( ( northLatitudeRequired - southLatitudeRequired ) / latitudeLength);
				offset.height *= totalImageHeight;

			}

			return offset;
		}		

		public void ApplyData (GISTerrain gisTerrain, Rect splatTextureOffset, int splatTextureResolution)
		{			
			// Divide m_Textures according to number of terrains
			float ratioY = splatTextureOffset.height / (float) gisTerrain.rowCount;
			float ratioX = splatTextureOffset.width / (float) gisTerrain.columnCount;

			EditorUtility.DisplayProgressBar ("GIS Terrain Utility", "Applying splat", 0);

			// Check if any other splat layer preced this created the terrain's splat
			if (!TerrainSplatCreatedElsewhere)
			{
				// Create all the splate prototype and texture for each terrain data
				CreateTerrainSplat (gisTerrain);

				// Signal a message via MessageBus to other splat layers that this layer is creating a splat prototype
				MessageBus.Instance.Signal (kCreatedSplatMessageId, null);
			}
			// Reset this flag locally to ensure next terrain generation will work on a fresh splat texture.
			TerrainSplatCreatedElsewhere = false;

			try
			{
				for (int i = 0; i < gisTerrain.rowCount; ++i)
				{
					for (int j = 0; j < gisTerrain.columnCount; ++j)
					{
						EditorUtility.DisplayProgressBar ("GIS Terrain Utility", "Applying splat", (i * gisTerrain.columnCount + j) / (float) (gisTerrain.rowCount * gisTerrain.columnCount));
						ApplySplatToTerrain (splatTextureOffset.x + j * ratioX, splatTextureOffset.y + i * ratioY, ratioX, ratioY, gisTerrain.GetTerrain (i, j), splatTextureResolution);
					}
				}
			}
			catch (Exception ex)
			{
				Debug.LogWarning (String.Format ("Exception encountered {0}",ex));
			}

			EditorUtility.ClearProgressBar ();
		}

		void CreateTerrainSplat (GISTerrain gisTerrain)
		{
			for (int i = 0; i < gisTerrain.rowCount; ++i)
			{
				for (int j = 0; j < gisTerrain.columnCount; ++j)
				{
					TerrainData terrainData = gisTerrain.GetTerrain (i, j);
					SplatPrototype[] infos = terrainData.splatPrototypes;
					if (infos == null || infos.Length == 0)
					{
						infos = new SplatPrototype[1];
					}

					if (infos[0] == null)
					{
						infos[0] = new SplatPrototype ();
					}

					if (infos[0].texture == null)
					{
						string terrainAssetPath = AssetDatabase.GetAssetPath (terrainData);
						// Store the generated texture for the TerrainData
						string path = AssetDatabase.GenerateUniqueAssetPath (Path.GetDirectoryName (terrainAssetPath) + "/" + terrainData.name + ".asset");

						infos[0].texture = new Texture2D (terrainData.baseMapResolution, terrainData.baseMapResolution, TextureFormat.ARGB32, true);
						AssetDatabase.CreateAsset (infos[0].texture, path);

						infos[0].tileSize = new Vector2 (terrainData.size.x, terrainData.size.z);
						infos[0].tileOffset = new Vector2 (0, 0);

						// Assign in to TerrainData
						terrainData.splatPrototypes = infos;
					}

					// Clear the texture with clear color
					Texture2D splatTexture = infos[0].texture;
					Color[] clearColors = new Color[splatTexture.width * splatTexture.height];
					for (int c = 0; c < clearColors.Length; ++c)
					{
						clearColors[c] = Color.clear;
					}
					infos[0].texture.SetPixels (clearColors);
				}
			}
		}

		void ApplySplatToTerrain (float x, float y, float width, float height, TerrainData terrainData, int splatTextureResolution)
		{
			if (terrainData == null)
			{
				return;
			}

			// Find the index for the starting texture
			int startImageCol, startImageRow;

			// Get splat texture from terrain data
			SplatPrototype[] infos = terrainData.splatPrototypes;
			Texture2D splatTexture = infos[0].texture;

			float ratioY = height / (float)splatTexture.height;
			float ratioX = width / (float)splatTexture.width;			
			int totalImageWidth = columnCount * ImageWidth;
			int totalImageHeight = rowCount * ImageHeight;
			/* The m_Textures stored might not allow us to call GetPixel,
			   thus we need to duplicate the texture in memory so that we can read 
			   the pixel value
			*/
			// In memory texture cache for the above reason
			Dictionary<int, Texture2D> cachedImage = new Dictionary<int, Texture2D> ();

			for (int i = 0; i < splatTexture.height; ++i)
			{
				// Determine the texture index and Pixel Y we are interested in 
				int pixelY = (int) (y + (i * ratioY));
				// early bail
				if (pixelY < 0 || pixelY >= totalImageHeight)
					continue;
				
				startImageRow = Mathf.FloorToInt (pixelY / ImageHeight);
				
				// early bail
				if (startImageRow < 0 || startImageRow >= rowCount)
					continue;
				pixelY -= startImageRow * ImageHeight;

				for (int j = 0; j < splatTexture.width; ++j)
				{
					// Determine the texture index and Pixel Y we are interested in 
					int pixelX = (int) (x + (j * ratioX));
					
					if (pixelX < 0 || pixelX >= totalImageWidth)
						continue;
					
					startImageCol = Mathf.FloorToInt (pixelX / ImageWidth);
					
					if (startImageCol < 0 || startImageCol >= columnCount)
						continue;
					pixelX -= startImageCol * ImageWidth;
					
					// Determine our cache key to see if we have duplicated it before
					int index = (startImageRow * columnCount) + startImageCol;
					Texture2D cached = null;
					cachedImage.TryGetValue (index, out cached);

					if (cached == null)
					{
						// Store a cache copy by reading back the data.						
						Texture2D originalImage = GetGISImage (startImageRow, startImageCol);
						if (originalImage == null)
						{
							continue;
						}

						if (originalImage == null)
							continue;
						cached = new Texture2D (originalImage.width, originalImage.height);

						string assetPath = AssetDatabase.GetAssetPath (originalImage);
						cached.LoadImage (File.ReadAllBytes (assetPath));						
						cachedImage.Add (index, cached);
					}

					Color c = cached.GetPixel (pixelX, cached.height - pixelY);
					splatTexture.SetPixel (j, splatTexture.height - i, c);
				}
			}

			// Clear cache
			foreach (KeyValuePair<int, Texture2D> cacheItem in cachedImage)
			{
				if (cacheItem.Value != null)
				{
					Texture2D.DestroyImmediate (cacheItem.Value);
				}
			}
			cachedImage.Clear ();
			splatTexture.Apply ();

			// Reassign to trigger a paint onto the terrain
			terrainData.splatPrototypes = infos;
		}
	}
}