using AllocsFixes.JSON;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using UnityEngine;

namespace AllocsFixes.MapRendering
{
	public class MapRendering
	{
		private static MapRendering instance;

		public static MapRendering Instance {
			get {
				if (instance == null) {
					instance = new MapRendering ();
				}
				return instance;
			}
		}

		private static object lockObject = new object ();
		private MapRenderBlockBuffer[] zoomLevelBuffers;
		private Dictionary<Vector2i, Color[]> dirtyChunks = new Dictionary<Vector2i, Color[]> ();
		private System.Timers.Timer chunkSaveTimer = new System.Timers.Timer (500);
		private bool renderingFullMap = false;
		public static bool renderingEnabled = true;
		private MicroStopwatch msw = new MicroStopwatch ();
		private AllocsFixes.FileCache.MapTileCache cache = new AllocsFixes.FileCache.MapTileCache ();

		public static AllocsFixes.FileCache.MapTileCache GetTileCache() {
			return Instance.cache;
		}

		private MapRendering ()
		{
			Constants.MAP_DIRECTORY = StaticDirectories.GetSaveGameDir () + "/map";

			lock (lockObject) {
				if (!LoadMapInfo ())
					WriteMapInfo ();
			}

			cache.SetZoomCount (Constants.ZOOMLEVELS);

			zoomLevelBuffers = new MapRenderBlockBuffer[Constants.ZOOMLEVELS];
			for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
				zoomLevelBuffers [i] = new MapRenderBlockBuffer (i, cache);
			}

			chunkSaveTimer.AutoReset = false;
			chunkSaveTimer.Elapsed += new System.Timers.ElapsedEventHandler (TimedRendering);
		}

		public static void RenderSingleChunk (Chunk chunk)
		{
			if (renderingEnabled) {
				ThreadPool.QueueUserWorkItem ((o) =>
				{
					try {
						if (!Instance.renderingFullMap) {
							lock (lockObject) {
								Chunk c = (Chunk)o;
								Vector3i cPos = c.GetWorldPos ();
								Vector2i cPos2 = new Vector2i (cPos.x / Constants.MAP_CHUNK_SIZE, cPos.z / Constants.MAP_CHUNK_SIZE);

								ushort[] mapColors = c.GetMapColors ();
								if (mapColors != null) {
									Color[] realColors = new Color[Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE];
									for (int i_colors = 0; i_colors < mapColors.Length; i_colors++) {
										realColors [i_colors] = shortColorToColor (mapColors [i_colors]);
									}
									Instance.dirtyChunks [cPos2] = realColors;
									//Log.Out ("Add Dirty: " + cPos2);
									Instance.chunkSaveTimer.Stop ();
									Instance.chunkSaveTimer.Start ();
								}
							}
						}
					} catch (Exception e) {
						Log.Out ("Exception in MapRendering.RenderSingleChunk(): " + e);
					}
				}, chunk);
			}
		}

		public void RenderFullMap ()
		{
			MicroStopwatch microStopwatch = new MicroStopwatch ();

			string regionSaveDir = StaticDirectories.GetSaveGameRegionDir ();
			RegionFileManager rfm = new RegionFileManager (regionSaveDir, regionSaveDir, 0, false);
			Texture2D fullMapTexture = null;

			Vector2i minChunk = default(Vector2i), maxChunk = default(Vector2i);
			Vector2i minPos = default(Vector2i), maxPos = default(Vector2i);
			int widthChunks, heightChunks, widthPix, heightPix;
			getWorldExtent (rfm, out minChunk, out maxChunk, out minPos, out maxPos, out widthChunks, out heightChunks, out widthPix, out heightPix);

			Log.Out (String.Format ("RenderMap: min: {0}, max: {1}, minPos: {2}, maxPos: {3}, w/h: {4}/{5}, wP/hP: {6}/{7}",
			                        minChunk.ToString (), maxChunk.ToString (),
			                        minPos.ToString (), maxPos.ToString (),
			                        widthChunks, heightChunks,
			                        widthPix, heightPix)
			);

			lock (lockObject) {
				for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
					zoomLevelBuffers [i].ResetBlock ();
				}

				if (Directory.Exists (Constants.MAP_DIRECTORY)) {
					Directory.Delete (Constants.MAP_DIRECTORY, true);
				}
				WriteMapInfo ();

				renderingFullMap = true;

				if (widthPix <= 8000 && heightPix <= 8000)
					fullMapTexture = new Texture2D (widthPix, heightPix);

				Vector2i curFullMapPos = default(Vector2i);
				Vector2i curChunkPos = default(Vector2i);
				for (curFullMapPos.x = 0; curFullMapPos.x < widthPix; curFullMapPos.x += Constants.MAP_CHUNK_SIZE) {
					for (curFullMapPos.y = 0; curFullMapPos.y < heightPix; curFullMapPos.y += Constants.MAP_CHUNK_SIZE) {
						curChunkPos.x = (curFullMapPos.x / Constants.MAP_CHUNK_SIZE) + minChunk.x;
						curChunkPos.y = (curFullMapPos.y / Constants.MAP_CHUNK_SIZE) + minChunk.y;

						try {
							long chunkKey = WorldChunkCache.MakeChunkKey (curChunkPos.x, curChunkPos.y);
							if (rfm.ContainsChunkSync (chunkKey)) {
								Chunk c = rfm.GetChunkSync (chunkKey);
								ushort[] mapColors = c.GetMapColors ();
								if (mapColors != null) {
									Color[] realColors = new Color[Constants.MAP_CHUNK_SIZE * Constants.MAP_CHUNK_SIZE];
									for (int i_colors = 0; i_colors < mapColors.Length; i_colors++) {
										realColors [i_colors] = shortColorToColor (mapColors [i_colors]);
									}
									dirtyChunks [curChunkPos] = realColors;
									if (fullMapTexture != null)
										fullMapTexture.SetPixels (curFullMapPos.x, curFullMapPos.y, Constants.MAP_CHUNK_SIZE, Constants.MAP_CHUNK_SIZE, realColors);
								}
							}
						} catch (Exception e) {
							Log.Out ("Exception: " + e);
						}
					}

					while (dirtyChunks.Count > 0) {
						RenderDirtyChunks ();
					}

					Log.Out (String.Format ("RenderMap: {0}/{1} ({2}%)", curFullMapPos.x, widthPix, (int)((float)curFullMapPos.x / widthPix * 100)));
				}
			}

			if (fullMapTexture != null) {
				byte[] array = fullMapTexture.EncodeToPNG ();
				File.WriteAllBytes (Constants.MAP_DIRECTORY + "/map.png", array);
				Texture2D.Destroy (fullMapTexture);
				fullMapTexture = null;
			}

			renderingFullMap = false;

			Log.Out ("Generating map took: " + microStopwatch.ElapsedMilliseconds + " ms");
			Log.Out ("World extent: " + minPos + " - " + maxPos);
		}

		private void SaveAllBlockMaps (object source, System.Timers.ElapsedEventArgs e)
		{
			for (int i = 0; i < Constants.ZOOMLEVELS; i++) {
				zoomLevelBuffers [i].SaveBlock ();
			}
		}

		private void TimedRendering (object source, System.Timers.ElapsedEventArgs e)
		{
			lock (lockObject) {
				RenderDirtyChunks ();
				if (dirtyChunks.Count > 0)
					Instance.chunkSaveTimer.Start ();
			}
		}

		private void RenderDirtyChunks ()
		{
			msw.ResetAndRestart ();

			if (dirtyChunks.Count > 0) {
				List<Vector2i> keys = new List<Vector2i> (dirtyChunks.Keys);
				List<Vector2i> chunksDone = new List<Vector2i> ();

				Vector2i chunkPos = keys [0];
				chunksDone.Add (chunkPos);

				//Log.Out ("Start Dirty: " + chunkPos);

				Vector2i block = default(Vector2i), blockOffset = default(Vector2i);
				getBlockNumber (chunkPos, out block, out blockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MAP_CHUNK_SIZE);

				zoomLevelBuffers [Constants.ZOOMLEVELS - 1].LoadBlock (block);

				Vector2i v_block = default(Vector2i), v_blockOffset = default(Vector2i);
				foreach (Vector2i v in keys) {
					getBlockNumber (v, out v_block, out v_blockOffset, Constants.MAP_BLOCK_TO_CHUNK_DIV, Constants.MAP_CHUNK_SIZE);
					if (v_block.Equals (block)) {
						//Log.Out ("Dirty: " + v + " render: true");
						chunksDone.Add (v);
						zoomLevelBuffers [Constants.ZOOMLEVELS - 1].SetPart (v_blockOffset, Constants.MAP_CHUNK_SIZE, dirtyChunks [v]);
					} else {
						//Log.Out ("Dirty: " + v + " render: false");
					}
				}

				foreach (Vector2i v in chunksDone)
					dirtyChunks.Remove (v);

				RenderZoomLevel (Constants.ZOOMLEVELS - 1, block);

				SaveAllBlockMaps (null, null);
			}
		}

		private void RenderZoomLevel (int level, Vector2i innerBlock)
		{
			if (level > 0) {
				Vector2i block = default(Vector2i), blockOffset = default(Vector2i);
				getBlockNumber (innerBlock, out block, out blockOffset, 2, Constants.MAP_BLOCK_SIZE / 2);

				zoomLevelBuffers [level - 1].LoadBlock (block);
				zoomLevelBuffers [level - 1].SetPart (blockOffset, Constants.MAP_BLOCK_SIZE / 2, zoomLevelBuffers [level].GetHalfScaled ());

				RenderZoomLevel (level - 1, block);
			}
		}

		private void getBlockNumber (Vector2i innerPos, out Vector2i block, out Vector2i blockOffset, int scaleFactor, int offsetSize)
		{
			block = default(Vector2i);
			blockOffset = default(Vector2i);
			block.x = ((innerPos.x + 16777216) / scaleFactor) - (16777216 / scaleFactor);
			block.y = ((innerPos.y + 16777216) / scaleFactor) - (16777216 / scaleFactor);
			blockOffset.x = ((innerPos.x + 16777216) % scaleFactor) * offsetSize;
			blockOffset.y = ((innerPos.y + 16777216) % scaleFactor) * offsetSize;
		}

		private void WriteMapInfo ()
		{
			JSONObject mapInfo = new JSONObject ();
			mapInfo.Add ("blockSize", new JSONNumber (Constants.MAP_BLOCK_SIZE));
			mapInfo.Add ("maxZoom", new JSONNumber (Constants.ZOOMLEVELS - 1));

			Directory.CreateDirectory (Constants.MAP_DIRECTORY);
			File.WriteAllText (Constants.MAP_DIRECTORY + "/mapinfo.json", mapInfo.ToString (), Encoding.UTF8);
		}

		private bool LoadMapInfo ()
		{
			if (File.Exists (Constants.MAP_DIRECTORY + "/mapinfo.json")) {
				string json = File.ReadAllText (Constants.MAP_DIRECTORY + "/mapinfo.json", Encoding.UTF8);
				try {
					JSONNode node = Parser.Parse (json);
					if (node is JSONObject) {
						JSONObject jo = (JSONObject)node;
						if (jo.ContainsKey ("blockSize"))
							Constants.MAP_BLOCK_SIZE = ((JSONNumber)jo ["blockSize"]).GetInt ();
						if (jo.ContainsKey ("maxZoom"))
							Constants.ZOOMLEVELS = ((JSONNumber)jo ["maxZoom"]).GetInt () + 1;
						return true;
					}
				} catch (MalformedJSONException e) {
					Log.Out ("Exception in LoadMapInfo: " + e);
				} catch (InvalidCastException e) {
					Log.Out ("Exception in LoadMapInfo: " + e);
				}
			}
			return false;
		}

		private void getWorldExtent (RegionFileManager rfm, out Vector2i minChunk, out Vector2i maxChunk,
	                                    out Vector2i minPos, out Vector2i maxPos,
	                                    out int widthChunks, out int heightChunks,
	                                    out int widthPix, out int heightPix)
		{
			minChunk = default(Vector2i);
			maxChunk = default(Vector2i);
			minPos = default(Vector2i);
			maxPos = default(Vector2i);

			long[] keys = rfm.GetAllChunkKeys ();
			int minX = Int32.MaxValue;
			int minY = Int32.MaxValue;
			int maxX = Int32.MinValue;
			int maxY = Int32.MinValue;
			foreach (long key in keys) {
				int x = WorldChunkCache.extractX (key);
				int y = WorldChunkCache.extractZ (key);

				if (x < minX)
					minX = x;
				if (x > maxX)
					maxX = x;
				if (y < minY)
					minY = y;
				if (y > maxY)
					maxY = y;
			}

			minChunk.x = minX;
			minChunk.y = minY;

			maxChunk.x = maxX;
			maxChunk.y = maxY;

			minPos.x = minX * Constants.MAP_CHUNK_SIZE;
			minPos.y = minY * Constants.MAP_CHUNK_SIZE;

			maxPos.x = maxX * Constants.MAP_CHUNK_SIZE;
			maxPos.y = maxY * Constants.MAP_CHUNK_SIZE;

			widthChunks = maxX - minX + 1;
			heightChunks = maxY - minY + 1;

			widthPix = widthChunks * Constants.MAP_CHUNK_SIZE;
			heightPix = heightChunks * Constants.MAP_CHUNK_SIZE;
		}

		private static Color shortColorToColor (ushort col)
		{
			return new Color (((float)(col >> 10 & 31) / 31f), ((float)(col >> 5 & 31) / 31f), ((float)(col & 31) / 31f), 255);
		}

	}
}