using Cryville.EEW.Core;
using Cryville.EEW.CWAOpenData.Map;
using Cryville.EEW.GlobalQuake.Map;
using Cryville.EEW.JMAAtom.Map;
using Cryville.EEW.Map;
using Cryville.EEW.NOAA.Map;
using Cryville.EEW.Report;
using Cryville.EEW.Wolfx.Map;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using UnityEngine;
using UnityMapElement = Cryville.EEW.Unity.Map.Element.MapElement;

namespace Cryville.EEW.Unity.Map {
	sealed class MapElementManager : MonoBehaviour {
		ReportViewModel _selected;
		ReportViewModel _current;
		readonly List<UnityMapElement> _displayingElements = new();
		readonly List<ReportViewModel> _displayingReports = new();
		readonly List<int> _displayingOrder = new();

		public int Count => _displayingReports.Count;

		[SerializeField] MapElementManager m_subManager;

		public RectangleF? AABB {
			get {
				if (_selected != null) return AABBOf(_selected);
				if (_current != null) return AABBOf(_current);
				return null;
			}
		}
		RectangleF? AABBOf(ReportViewModel e) {
			int index = _displayingReports.IndexOf(e);
			if (index == -1) return null;
			return _displayingElements[index].AABB;
		}

		public void SetSelected(ReportViewModel e, bool forced = false) {
			if (e == null) {
				Remove(_selected);
				_selected = null;
				return;
			}
			if (_displayingReports.Contains(e)) {
				if (forced) return;
				Remove(_selected);
				_selected = null;
				return;
			}
			if (e.IsExcludedFromHistory)
				return;
			if (!Add(e))
				return;
			if (_selected is not null)
				Remove(_selected);
			_selected = e;
		}
		public bool SetCurrent(ReportViewModel e) {
			if (e == null) {
				_current = null;
				return true;
			}
			if (!_displayingReports.Contains(e)) {
				_current = null;
				return false;
			}
			if (_current == e)
				return false;
			_current = e;
			return true;
		}

		public bool Add(ReportViewModel e) {
			var element = Build(e.Model, out _, out int order);
			if (element == null) return false;
			var pos = element.transform.localPosition;
			pos.z = OrderToZ(_displayingOrder.Sum());
			element.transform.localPosition = pos;
			_displayingElements.Add(element);
			_displayingReports.Add(e);
			_displayingOrder.Add(order);
			element.transform.SetParent(transform, false);
			if (m_subManager != null) m_subManager.Add(e);
			return true;
		}
		public void Remove(ReportViewModel e) {
			if (_current == e) _current = null;
			int index = _displayingReports.IndexOf(e);
			if (index == -1) return;
			_displayingElements.RemoveAt(index);
			_displayingReports.RemoveAt(index);
			_displayingOrder.RemoveAt(index);
			var element = transform.GetChild(index);
			element.SetParent(null, false);
			Destroy(element.gameObject);

			int order = _displayingOrder.Take(index).Sum();
			for (int i = index; i < transform.childCount; i++) {
				var child = transform.GetChild(i);
				var pos = child.localPosition;
				pos.z = OrderToZ(order);
				child.localPosition = pos;
				order += _displayingOrder[i];
			}

			if (m_subManager != null) m_subManager.Remove(e);
		}

		float m_scale = 1;
		public float Scale {
			get => m_scale;
			set {
				m_scale = value;
				foreach (Transform element in transform) {
					element.GetComponent<UnityMapElement>().Scale = value;
				}
				if (m_subManager != null) m_subManager.Scale = value;
			}
		}

		readonly ContextedGeneratorManager<IMapGeneratorContext, MapElement> _gen = new(new IContextedGenerator<IMapGeneratorContext, MapElement>[] {
			new CENCEarthquakeMapGenerator(),
			new CENCEEWMapGenerator(),
			new CWAEarthquakeMapGenerator(),
			new CWAEEWMapGenerator(),
			new CWATsunamiMapGenerator(),
			new FujianEEWMapGenerator(),
			new GlobalQuakeMapViewGenerator(),
			new JMAAtomMapGenerator(),
			new JMAEEWMapGenerator(),
			new NOAAMapGenerator(),
			new SichuanEEWMapGenerator(),
		});
		public UnityMapElement Build(object e, out CultureInfo culture, out int order) {
			culture = CultureInfo.InvariantCulture;
			order = 0;
			var ret = Convert(_gen.TryGenerate(e, SharedSettings.Instance, ref culture), ref order);
			if (ret == null)
				return null;
			ret.Scale = Scale;
			return ret;
		}

		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		public static float OrderToZ(int order) => order / -1000f;

		[SerializeField] Element.GroupElement m_prefabGroupElement;
		[SerializeField] Element.MaskedGroupElement m_prefabMaskedGroupElement;
		[SerializeField] Element.OngoingGroupElement m_prefabOngoingGroupElement;
		[SerializeField] Element.HypocenterElement m_prefabHypocenterElement;
		[SerializeField] Element.LabeledPointElement m_prefabLabeledPointElement;
		[SerializeField] Element.MultiLineElement m_prefabMultiLineElement;
		[SerializeField] Element.MultiPolygonElement m_prefabMultiPolygonElement;
		[SerializeField] Element.PointElement m_prefabPointElement;
		[SerializeField] Element.TsunamiHeightElement m_prefabTsunamiHeightElement;
		[SerializeField] Element.WaveCircleElement m_prefabWaveCircleElement;
		public UnityMapElement Convert(MapElement mapElement, ref int order) => mapElement switch {
			MaskedGroupElement maskedGroup => ConvertMaskedGroup(maskedGroup, ref order),
			OngoingGroupElement ongoingGroup => ConvertOngoingGroup(ongoingGroup, ref order),
			GroupElement group => ConvertGroup(group, m_prefabGroupElement, ref order),
			HypocenterElement hypocenter => ConvertHypocenter(hypocenter, ref order),
			LabeledPointElement labeledPoint => ConvertLabeledPoint(labeledPoint, ref order),
			MultiLineElement multiLine => ConvertMultiLine(multiLine, ref order),
			MultiPolygonElement multiPolygon => ConvertMultiPolygon(multiPolygon, ref order),
			PointElement point => ConvertPoint(point, ref order),
			TsunamiHeightElement tsunamiHeight => ConvertTsunamiHeight(tsunamiHeight, ref order),
			WaveCircleElement waveCircle => ConvertWaveCircle(waveCircle, ref order),
			_ => null,
		};
		T Convert<T>(MapElement element, T prefab) where T : UnityMapElement {
			var ret = Instantiate(prefab);
			ret.MaxScale = element.MaxScale;
			return ret;
		}
		T ConvertGroup<T>(GroupElement group, T prefab, ref int order) where T : Element.GroupElement {
			var ret = Convert(group, prefab);
			foreach (var e in group.Elements) ret.AddElement(Convert(e, ref order));
			return ret;
		}
		Element.MaskedGroupElement ConvertMaskedGroup(MaskedGroupElement maskedGroup, ref int order) {
			var ret = ConvertGroup(maskedGroup, m_prefabMaskedGroupElement, ref order);
			ret.SetMasks(maskedGroup.Masks);
			return ret;
		}
		Element.OngoingGroupElement ConvertOngoingGroup(OngoingGroupElement ongoingGroup, ref int order) {
			var ret = ConvertGroup(ongoingGroup, m_prefabOngoingGroupElement, ref order);
			ret.BlinkingPeriod = ongoingGroup.BlinkingPeriod;
			return ret;
		}
		Element.HypocenterElement ConvertHypocenter(HypocenterElement hypocenter, ref int order) {
			var ret = Convert(hypocenter, m_prefabHypocenterElement);
			ret.SetLocation(hypocenter.Location, ref order);
			ret.IsLowQuality = hypocenter.IsLowQuality;
			ret.Size = hypocenter.Size;
			return ret;
		}
		Element.LabeledPointElement ConvertLabeledPoint(LabeledPointElement labeledPoint, ref int order) {
			var ret = Convert(labeledPoint, m_prefabLabeledPointElement);
			ret.SetLocation(labeledPoint.Location, ref order);
			ret.Text = labeledPoint.Text;
			ret.Color = labeledPoint.FillColor.ToUnityColor();
			ret.TextColor = labeledPoint.TextColor.ToUnityColor();
			ret.IsArea = labeledPoint.IsAreaLabel;
			ret.Size = labeledPoint.Size;
			return ret;
		}
		Element.MultiLineElement ConvertMultiLine(MultiLineElement multiLine, ref int order) {
			var ret = Convert(multiLine, m_prefabMultiLineElement);
			ret.SetLines(multiLine.Lines, ref order);
			ret.Color = multiLine.BorderColor.ToUnityColor();
			ret.Width = multiLine.StrokeWidth;
			return ret;
		}
		Element.MultiPolygonElement ConvertMultiPolygon(MultiPolygonElement multiPolygon, ref int order) {
			var ret = Convert(multiPolygon, m_prefabMultiPolygonElement);
			ret.SetPolygons(multiPolygon.Polygons, ref order);
			ret.FillColor = multiPolygon.FillColor.ToUnityColor();
			ret.BorderColor = multiPolygon.BorderColor.ToUnityColor();
			ret.BorderWidth = multiPolygon.StrokeWidth;
			return ret;
		}
		Element.PointElement ConvertPoint(PointElement point, ref int order) {
			var ret = Convert(point, m_prefabPointElement);
			ret.SetLocation(point.Location, ref order);
			ret.Color = point.FillColor.ToUnityColor();
			ret.Size = point.Size;
			return ret;
		}
		Element.TsunamiHeightElement ConvertTsunamiHeight(TsunamiHeightElement tsunamiHeight, ref int order) {
			var ret = Convert(tsunamiHeight, m_prefabTsunamiHeightElement);
			ret.SetLocation(tsunamiHeight.Location, ref order);
			ret.Height = tsunamiHeight.Height;
			ret.Color = tsunamiHeight.FillColor.ToUnityColor();
			ret.IsRising = tsunamiHeight.IsRising;
			ret.IsOutOfRange = tsunamiHeight.IsOutOfRange;
			ret.IsMissing = tsunamiHeight.IsMissing;
			ret.Width = tsunamiHeight.Width;
			return ret;
		}
		Element.WaveCircleElement ConvertWaveCircle(WaveCircleElement waveCircle, ref int order) {
			var ret = Convert(waveCircle, m_prefabWaveCircleElement);
			ret.SetHypocenterLocation(waveCircle.HypocenterLocation, ref order);
			ret.OriginTime = waveCircle.OriginTime;
			ret.Depth = waveCircle.Depth;
			ret.ReportTime = waveCircle.ReportTime;
			return ret;
		}
	}
}
