using Cryville.Common;
using Cryville.Common.Buffers;
using Cryville.Common.Collections.Specialized;
using Cryville.Crtr.Ruleset;
using Cryville.Crtr.Skin;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using UnityEngine;

namespace Cryville.Crtr.Event {
	public abstract class ContainerHandler : ISkinnableGroup {
		#region Struct
		public ContainerHandler() { }

		/// <summary>
		/// Prehandling <see cref="ContainerState"/>, prehandling the events.
		/// </summary>
		public ContainerState ps;

		/// <summary>
		/// Backward <see cref="ContainerState"/>, disposing events at the backward clipping plane, firing anchor state and temporary state.
		/// </summary>
		public ContainerState bs;

		/// <summary>
		/// Current <see cref="ContainerState"/>, handling events occuring at the current time.
		/// </summary>
		public ContainerState cs;

		/// <summary>
		/// Anchor <see cref="ContainerState"/>, computing the start position of the temporary state.
		/// </summary>
		public ContainerState ns;

		/// <summary>
		/// Temporary <see cref="ContainerState"/>, rendering events between the clipping planes.
		/// </summary>
		public ContainerState ts;

		protected Transform RootTransform;

		readonly List<SkinComponent> _comps = new List<SkinComponent>();
		protected IEnumerable<SkinComponent> Components { get { return _comps; } }

		public Vector3 Position { get; protected set; }
		public Quaternion Rotation { get; protected set; }
		public bool Alive { get; private set; }
		bool PreGraphicalActive;
		public void SetPreGraphicalActive(bool value, ContainerState s) {
			if (PreGraphicalActive == value) return;
			PreGraphicalActive = value;
			if (PreGraphicalActive) StartPreGraphicalUpdate(s);
			else EndPreGraphicalUpdate(s);
		}
		bool GraphicalActive;
		public void SetGraphicalActive(bool value, ContainerState s) {
			if (GraphicalActive == value) return;
			GraphicalActive = value;
			if (GraphicalActive) StartGraphicalUpdate(s);
			else EndGraphicalUpdate(s);
		}

		public EventContainer Container {
			get { return cs.Container; }
		}

		static readonly int _var_current_time = IdentifierManager.Shared.Request("current_time");
		static readonly int _var_invisible_bounds = IdentifierManager.Shared.Request("invisible_bounds");
		public readonly IntKeyedDictionary<PropSrc> PropSrcs = new IntKeyedDictionary<PropSrc>();
		SkinContainer skinContainer;
		protected Judge judge;
		public void AttachSystems(PdtSkin skin, Judge judge) {
			skinContainer = new SkinContainer(this, skin.elements);
			this.judge = judge;
		}

		public readonly IntKeyedDictionary<List<Anchor>> Anchors = new IntKeyedDictionary<List<Anchor>>();
		public readonly IntKeyedDictionary<Anchor> DynamicAnchors = new IntKeyedDictionary<Anchor>();
		public readonly IntKeyedDictionary<double> DynamicAnchorSetTime = new IntKeyedDictionary<double>();
		Anchor a_cur;
		Anchor a_head;
		Anchor a_tail;
		static readonly int _a_cur = IdentifierManager.Shared.Request("cur");
		static readonly int _a_head = IdentifierManager.Shared.Request("head");
		static readonly int _a_tail = IdentifierManager.Shared.Request("tail");
		double atime_head;
		double atime_tail;
		public Anchor RegisterAnchor(int name, bool dyn = false, int propSrcCount = 0) {
			var strname = IdentifierManager.Shared.Retrieve(name);
			var go = new GameObject("." + strname).transform;
			go.SetParent(RootTransform, false);
			var result = new Anchor(name, go, propSrcCount);
			if (dyn) {
				if (DynamicAnchors.ContainsKey(name))
					throw new ArgumentException(string.Format("The anchor \"{0}\" already exists", strname));
				DynamicAnchors.Add(name, result);
				DynamicAnchorSetTime.Add(name, double.NaN);
			}
			List<Anchor> list;
			if (!Anchors.TryGetValue(name, out list))
				Anchors.Add(name, list = new List<Anchor>());
			list.Add(result);
			return result;
		}
		protected void OpenAnchor(Anchor anchor) {
			if (_openedAnchor != null) throw new InvalidOperationException("An anchor has been opened");
			anchor.Transform.gameObject.SetActive(true);
			_openedAnchor = anchor;
		}
		protected void CloseAnchor() {
			_openedAnchor = null;
		}
		#endregion

		#region Logic
		#region Init methods: Called on prehandle
		const string TagRootTransform = "RT";
		public virtual void PreInit() {
			RootTransform = new GameObject(TypeName + ":" + Container.GetHashCode().ToString(CultureInfo.InvariantCulture)).transform;
			RootTransform.tag = TagRootTransform;
			SkinContext = new SkinContext(RootTransform);
			if (cs.Parent != null)
				RootTransform.SetParent(cs.Parent.Handler.RootTransform, false);
			a_cur = RegisterAnchor(_a_cur);
			a_head = RegisterAnchor(_a_head, true);
			a_tail = RegisterAnchor(_a_tail, true);
		}
		public virtual void Init() {
			PdtEvaluator.Instance.ContextState = ps;
			PdtEvaluator.Instance.ContextEvent = Container;
			skinContainer.MatchStatic();
			MatchDynamic(ps, 0);
			PdtEvaluator.Instance.ContextEvent = null;
			PdtEvaluator.Instance.ContextState = null;
			foreach (Transform child in RootTransform) {
				if (child.CompareTag(TagRootTransform)) continue;
				_comps.AddRange(child.GetComponentsInChildren<SkinComponent>());
			}
			foreach (var i in _comps) i.Init();
		}
		public virtual void PostInit() {
			PropSrcs.Add(_var_current_time, new PropSrc.Float(() => (float)cs.rootPrototype.Time));
			PropSrcs.Add(_var_invisible_bounds, new PropSrc.Boolean(() => atime_head > atime_tail));
			RootTransform.gameObject.SetActive(false);
		}
		#endregion
		#region Start methods
		public virtual void StartPhysicalUpdate(ContainerState s) {
			if (s.CloneType < 16) Alive = true;
			else if (s.CloneType == 17) Init();
		}
		public virtual void StartLogicalUpdate(ContainerState s) { }
		protected virtual void StartPreGraphicalUpdate(ContainerState s) { }
		protected virtual void StartGraphicalUpdate(ContainerState s) {
			if (RootTransform) RootTransform.gameObject.SetActive(true);
		}
		#endregion
		public virtual void Update(ContainerState s, StampedEvent ev) {
			if (ev is StampedEvent.Anchor) {
				var tev = (StampedEvent.Anchor)ev;
				if (tev.Target == a_head) {
					if (s.CloneType == 2) SetGraphicalActive(true, s);
					else SetPreGraphicalActive(true, s);
				}
				if (RootTransform) {
					OpenAnchor(tev.Target);
#if UNITY_5_6_OR_NEWER
					tev.Target.Transform.SetPositionAndRotation(Position, Rotation);
#else
					tev.Target.Transform.position = Position;
					tev.Target.Transform.rotation = Rotation;
#endif
					MatchDynamic(s, 2);
					CloseAnchor();
				}
				if (tev.Target == a_tail) {
					if (s.CloneType == 2) SetGraphicalActive(false, s);
					else SetPreGraphicalActive(false, s);
				}
				ReturnAnchorEvent(tev);
			}
			else if (RootTransform && s.CloneType == 2) MatchDynamic(s, 2);
		}
		#region End methods
		protected virtual void EndGraphicalUpdate(ContainerState s) { }
		protected virtual void EndPreGraphicalUpdate(ContainerState s) { }
		public virtual void EndLogicalUpdate(ContainerState s) { }
		public virtual void EndPhysicalUpdate(ContainerState s) { }
		public virtual void Dispose() {
			if (RootTransform)
				GameObject.Destroy(RootTransform.gameObject);
			Alive = false;
		}
		public virtual void DisposeAll() { }
		#endregion
		#region Utils
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		protected static bool CanDoGraphicalUpdate(ContainerState s) { return s.CloneType >= 2 && s.CloneType < 16; }
		[MethodImpl(MethodImplOptions.AggressiveInlining)]
		void MatchDynamic(ContainerState s, int dl) {
			PdtEvaluator.Instance.ContextState = s;
			PdtEvaluator.Instance.ContextEvent = Container;
			skinContainer.MatchDynamic(dl);
			PdtEvaluator.Instance.ContextEvent = null;
			PdtEvaluator.Instance.ContextState = null;
		}
		#endregion
		#region Anchor
		public virtual void PreAnchor() {
			foreach (var p in PropSrcs) p.Value.Invalidate();
			foreach (var a in DynamicAnchors) DynamicAnchorSetTime[a.Key] = double.NaN;
			atime_head = cs.StampedContainer.Time;
			atime_tail = atime_head + cs.StampedContainer.Duration;
			MatchDynamic(cs, 1);
			foreach (var i in _comps) i.Tick(skinContainer, cs.Time);
			if (double.IsNaN(DynamicAnchorSetTime[_a_head])) DynamicAnchorSetTime[_a_head] = atime_head;
			if (double.IsNaN(DynamicAnchorSetTime[_a_tail])) DynamicAnchorSetTime[_a_tail] = atime_tail;
			PushAnchorEvent(ns, atime_head, a_head, -1, true);
			PushAnchorEvent(ns, atime_tail, a_tail, 1, true);
			foreach (var anchors in Anchors) foreach (var anchor in anchors.Value) anchor.Transform.gameObject.SetActive(false);
		}
		public virtual void Anchor() {
			if (cs.Active) PushAnchorEvent(ts, cs.Time, a_cur);
			foreach (var t in DynamicAnchorSetTime) {
				if (double.IsNaN(t.Value)) continue;
				int priority = 0;
				bool forced = true;
				if (t.Key == _a_head) { priority = -1; }
				else if (t.Key == _a_tail) { priority = 1; }
				else forced = false;
				PushAnchorEvent(ts, t.Value, DynamicAnchors[t.Key], priority, forced);
			}
		}
		static readonly SimpleObjectPool<StampedEvent.Anchor> anchorEvPool
			= new SimpleObjectPool<StampedEvent.Anchor>(1024);
		void PushAnchorEvent(ContainerState state, double time, Anchor anchor, int priority = 0, bool forced = false) {
			var tev = anchorEvPool.Rent();
			tev.Time = time;
			tev.Container = Container;
			tev.Target = anchor;
			tev.Forced = forced;
			tev.SetPriority(priority);
			state.Bus.PushTempEvent(tev);
		}
		public virtual void Discard(ContainerState s, StampedEvent ev) {
			if (ev is StampedEvent.Anchor) {
				ReturnAnchorEvent((StampedEvent.Anchor)ev);
			}
		}
		void ReturnAnchorEvent(StampedEvent.Anchor ev) {
			ev.Container = null;
			ev.Target = null;
			anchorEvPool.Return(ev);
		}
		#endregion
		#endregion

		#region ISkinnableGroup
		public abstract string TypeName { get; }
		public SkinContext SkinContext { get; private set; }
		public int AtAnchorDynamicLevel { get { return 2; } }
		Anchor _openedAnchor;
		public int OpenedAnchorName { get { return _openedAnchor == null ? 0 : _openedAnchor.Name; } }
		public bool TryGetAnchorsByName(int name, out IReadOnlyCollection<Anchor> result) {
			List<Anchor> anchors;
			var ret = Anchors.TryGetValue(name, out anchors);
			result = anchors;
			return ret;
		}
		void ISkinnableGroup.RegisterAnchor(int name) {
			RegisterAnchor(name, true);
		}
		public void PushAnchorEvent(double time, int name) {
			if (!DynamicAnchors.ContainsKey(name))
				throw new ArgumentException(string.Format("Specified anchor \"{0}\" not found", IdentifierManager.Shared.Retrieve(name)));
			if (name == _a_head) atime_head = time;
			else if (name == _a_tail) atime_tail = time;
			DynamicAnchorSetTime[name] = time;
		}
		#endregion
	}
}
