using Cryville.EEW.Core.Map;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Cryville.EEW.Unity.Map {
	sealed class TileZOrderComparer : IComparer<MapTileIndex>, IComparer<MapTile> {
		public static readonly TileZOrderComparer Instance = new();
		public int Compare(MapTileIndex a, MapTileIndex b) {
			var c = a.Z.CompareTo(b.Z);
			if (c != 0) return c;
			c = a.Y.CompareTo(b.Y);
			if (c != 0) return c;
			return a.X.CompareTo(b.X);
		}
		public int Compare(MapTile a, MapTile b) {
			if (a == null) return b == null ? 0 : -1;
			if (b == null) return 1;
			return Compare(a.Index, b.Index);
		}
	}

	sealed class MapTileCacheManager : IDisposable {
		public int ExtraCachedZoomLevel { get; set; } = 20;

		GameObject m_prefabTile;
		public GameObject PrefabTile {
			get => m_prefabTile;
			set {
				m_prefabTile = value;
				if (_dummyTask) GameObject.Destroy(_dummyTask.gameObject);
				_dummyTask = GameObject.Instantiate(m_prefabTile, Parent, false).GetComponent<MapTile>();
			}
		}

		public Transform Parent { get; set; }

		public string CacheDir { get; set; }

		public event Action Updated;
		void OnUpdated(MapTile tile) {
			if (tile.IsEmpty) {
				lock (ActiveTiles) {
					if (_cache.Remove(tile.Index)) {
						ActiveTiles.RemoveAt(ActiveTiles.BinarySearch(tile, TileZOrderComparer.Instance));
					}
				}
			}
			Updated?.Invoke();
		}

		public void Dispose() {
			MonoBehaviour.Destroy(_dummyTask);

			lock (ActiveTiles) {
				foreach (var task in ActiveTiles)
					GameObject.Destroy(task.gameObject);
				ActiveTiles.Clear();
				_cache.Clear();
			}
		}

		readonly Dictionary<MapTileIndex, MapTile> _cache = new();
		public List<MapTile> ActiveTiles { get; } = new();

		MapTileIndex _a, _b;

		public void MoveTo(MapTileIndex a, MapTileIndex b) {
			if (a.Z != b.Z) throw new ArgumentException("Mismatched Z index.");
			if (a.X >= b.X || a.Y >= b.Y) throw new ArgumentException("Incorrect relative X/Y index.");

			lock (ActiveTiles) {
				UnloadTiles(a, b);
				_a = a; _b = b;
				LoadTiles(a, b);
			}
		}
		void LoadTiles(MapTileIndex a, MapTileIndex b) {
			for (int z = Math.Max(0, a.Z - ExtraCachedZoomLevel); z <= Math.Max(0, a.Z); z++) {
				var ia = a.ZoomToLevel(z, Math.Floor);
				var ib = b.ZoomToLevel(z, Math.Ceiling);
				for (int x = ia.X; x < ib.X; x++) {
					for (int y = ia.Y; y < ib.Y; y++) {
						var index = new MapTileIndex(x, y, z);
						if (_cache.ContainsKey(index)) continue;
						var task = GameObject.Instantiate(PrefabTile, Parent, false).GetComponent<MapTile>();
						task.Load(index, CacheDir, OnUpdated);
						_cache.Add(index, task);
						var i = ~ActiveTiles.BinarySearch(task, TileZOrderComparer.Instance);
						ActiveTiles.Insert(i, task);
					}
				}
			}
		}
		void UnloadTiles(MapTileIndex a, MapTileIndex b) {
			if (a.Z != _a.Z) {
				for (int z = _a.Z - ExtraCachedZoomLevel; z < a.Z - ExtraCachedZoomLevel; z++) UnloadTilesAtZoomLevel(z);
				for (int z = a.Z + 1; z <= _a.Z; z++) UnloadTilesAtZoomLevel(z);
			}
			if (a.X > _a.X) {
				for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
					var ia0 = _a.ZoomToLevel(z, Math.Floor);
					var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
					var ia1 = a.ZoomToLevel(z, Math.Floor);
					if (ia0.X == ia1.X) break;
					UnloadTilesInRegion(ia0.X, ia0.Y, ia1.X, ib0.Y, z);
				}
			}
			if (b.X < _b.X) {
				for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
					var ia0 = _a.ZoomToLevel(z, Math.Floor);
					var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
					var ib1 = b.ZoomToLevel(z, Math.Ceiling);
					if (ib0.X == ib1.X) break;
					UnloadTilesInRegion(ib1.X, ia0.Y, ib0.X, ib0.Y, z);
				}
			}
			if (a.Y > _a.Y) {
				for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
					var ia0 = _a.ZoomToLevel(z, Math.Floor);
					var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
					var ia1 = a.ZoomToLevel(z, Math.Floor);
					if (ia0.Y == ia1.Y) break;
					UnloadTilesInRegion(ia0.X, ia0.Y, ib0.X, ia1.Y, z);
				}
			}
			if (b.Y < _b.Y) {
				for (int z = Math.Max(0, a.Z); z >= Math.Max(0, a.Z - ExtraCachedZoomLevel); --z) {
					var ia0 = _a.ZoomToLevel(z, Math.Floor);
					var ib0 = _b.ZoomToLevel(z, Math.Ceiling);
					var ib1 = b.ZoomToLevel(z, Math.Ceiling);
					if (ib0.Y == ib1.Y) break;
					UnloadTilesInRegion(ia0.X, ib1.Y, ib0.X, ib0.Y, z);
				}
			}
		}
		void UnloadTilesInRegion(int x1, int y1, int x2, int y2, int z) {
			for (int x = x1; x < x2; x++) {
				for (int y = y1; y < y2; y++) {
					var index = new MapTileIndex(x, y, z);
					if (!_cache.TryGetValue(index, out var task)) continue;
					GameObject.Destroy(task.gameObject);
				}
			}
		}

		MapTile _dummyTask;
		void UnloadTilesAtZoomLevel(int z) {
			if (z < 0) return;

			_dummyTask.Index = new(int.MinValue, int.MinValue, z);
			var i0 = ActiveTiles.BinarySearch(_dummyTask, TileZOrderComparer.Instance);
			if (i0 < 0) i0 = ~i0;

			_dummyTask.Index = new(int.MinValue, int.MinValue, z + 1);
			var i1 = ActiveTiles.BinarySearch(_dummyTask, TileZOrderComparer.Instance);
			if (i1 < 0) i1 = ~i1;

			for (var i = i1 - 1; i >= i0; --i) {
				var index = ActiveTiles[i].Index;
				if (!_cache.TryGetValue(index, out var task)) continue;
				GameObject.Destroy(task.gameObject);
			}
		}
	}
}
