//#define DISABLE_CACHE

using Cryville.Common;
using System;
using System.Collections.Generic;
using UnityEngine;

namespace Cryville.Crtr.Event {
	public class ContainerState {
		public EventBus Bus;
		public EventContainer Container;
		public ContainerState Parent = null;
		public ushort Depth;

		public Dictionary<EventContainer, ContainerState> Children
			= new Dictionary<EventContainer, ContainerState>();
		readonly HashSet<EventContainer> WorkingChildren
			= new HashSet<EventContainer>();
		readonly HashSet<EventContainer> InvalidatedChildren
			= new HashSet<EventContainer>();
		public Dictionary<Type, List<ContainerState>> TypedChildren
			= new Dictionary<Type, List<ContainerState>>();

		public ContainerState GetChild(int index, Type handlerType) {
			return TypedChildren[handlerType][index];
		}
		public ContainerState GetChild(EventContainer ev) {
			return Children[ev];
		}

		void NotifyWorkingChanged(EventContainer key) {
			InvalidatedChildren.Add(key);
		}
		void ValidateChildren() {
			foreach (var cev in InvalidatedChildren)
				if (Children[cev].Working && !WorkingChildren.Contains(cev)) WorkingChildren.Add(cev);
				else if (!Children[cev].Working && WorkingChildren.Contains(cev)) WorkingChildren.Remove(cev);
			InvalidatedChildren.Clear();
		}

		private bool m_Working;
		public bool Working {
			get { return m_Working; }
			set {
				m_Working = value;
				if (Parent != null) Parent.NotifyWorkingChanged(Container);
				Bus.NotifyWorkingChanged(this);
			}
		}
		public byte CloneType;
		private ContainerState rootPrototype = null;
		private ContainerState prototype = null;
		public SkinContainer skinContainer;
		public Judge judge;

		public ContainerHandler Handler {
			get;
			private set;
		}

		public float Time {
			get {
				return Bus.Time;
			}
		}

		readonly RMVPool RMVPool = new RMVPool();
		protected Dictionary<StampedEvent, RealtimeMotionValue> PlayingMotions = new Dictionary<StampedEvent, RealtimeMotionValue>();
		protected Dictionary<Identifier, RealtimeMotionValue> Values;
		protected Dictionary<Identifier, Vector> CachedValues;
		protected Dictionary<Identifier, bool> CachedValueStates;

		/// <summary>
		/// Gets a motion value.
		/// </summary>
		/// <param name="name">The motion name.</param>
		/// <param name="clone">Returns a cloned motion value instead.</param>
		/// <returns>A motion value.</returns>
		RealtimeMotionValue GetMotionValue(Identifier name, bool clone = false) {
			RealtimeMotionValue value;
			if (!Values.TryGetValue(name, out value)) {
				value = new RealtimeMotionValue().Init(Parent == null
					? ChartPlayer.motionRegistry[name].GlobalInitValue
					: ChartPlayer.motionRegistry[name].InitValue
				);
				Values.Add(name, value);
			}
			if (clone) return value.Clone();
			return value;
		}

		void InvalidateMotion(Identifier name) {
			CachedValueStates[name] = false;
			foreach (var c in Children)
				c.Value.InvalidateMotion(name);
		}

		public ContainerState(Chart c, EventContainer _ev, ContainerState parent = null) {
			Container = _ev;

			if (parent != null) {
				AddChild(_ev, this, parent);
				Parent = parent;
			}

			Values = new Dictionary<Identifier, RealtimeMotionValue>(ChartPlayer.motionRegistry.Count);
			CachedValues = new Dictionary<Identifier, Vector>(ChartPlayer.motionRegistry.Count);
			CachedValueStates = new Dictionary<Identifier, bool>(ChartPlayer.motionRegistry.Count);
			foreach (var m in ChartPlayer.motionRegistry)
				Values.Add(m.Key, new RealtimeMotionValue().Init(Parent == null ? m.Value.GlobalInitValue : m.Value.InitValue));
		}

		static void AddChild(EventContainer c, ContainerState s, ContainerState target) {
			target.Children.Add(c, s);
			Type t = c.GetType();
			if (!target.TypedChildren.ContainsKey(t)) target.TypedChildren.Add(t, new List<ContainerState>());
			target.TypedChildren[t].Add(s);
		}

		public ContainerState Clone(byte ct) {
			var r = (ContainerState)MemberwiseClone();

			var mvs = new Dictionary<Identifier, RealtimeMotionValue>(ChartPlayer.motionRegistry.Count);
			foreach (var mv in Values) {
				mvs.Add(mv.Key, mv.Value.Clone());
			}
			r.Values = mvs;

			var cvs = new Dictionary<Identifier, Vector>(ChartPlayer.motionRegistry.Count);
			foreach (var cv in CachedValues) {
				cvs.Add(cv.Key, cv.Value.Clone());
			}
			r.CachedValues = cvs;

			var cvss = new Dictionary<Identifier, bool>(ChartPlayer.motionRegistry.Count);
			foreach (var cv in CachedValueStates) {
				cvss.Add(cv.Key, cv.Value);
			}
			r.CachedValueStates = cvss;

			r.Children = new Dictionary<EventContainer, ContainerState>();
			foreach (var child in Children) {
				var cc = child.Value.Clone(ct);
				cc.Parent = r;
				AddChild(child.Key, cc, r);
			}

			var pms = new Dictionary<StampedEvent, RealtimeMotionValue>(PlayingMotions.Count);
			foreach (var m in PlayingMotions)
				pms.Add(m.Key, m.Value);
			r.PlayingMotions = pms;

			if (ct == 1) Handler.bs = r;
			else if (ct == 2) Handler.ts = r;
			else if (ct == 3) Handler.ns = r;
			else if (ct >= 16) Handler.ps = r;
			else throw new InvalidOperationException("Invalid clone type");
			r.prototype = this;
			if (prototype == null) r.rootPrototype = this;
			else r.rootPrototype = rootPrototype;
			r.CloneType = ct;
			return r;
		}

		public void CopyTo(byte ct, ContainerState dest) {
			dest.Working = Working;

			foreach (var mv in Values) {
				RealtimeMotionValue dv;
				if (dest.Values.TryGetValue(mv.Key, out dv)) mv.Value.CopyTo(dv);
				else dest.Values.Add(mv.Key, mv.Value.Clone());
			}

			foreach (var cv in CachedValues) {
				Vector dv;
				if(dest.CachedValues.TryGetValue(cv.Key, out dv)) cv.Value.CopyTo(dv);
				else dest.CachedValues.Add(cv.Key, cv.Value.Clone());
			}
			foreach (var cvs in CachedValueStates) dest.CachedValueStates[cvs.Key] = cvs.Value;

			if (ct != 1) foreach (var cev in WorkingChildren)
				Children[cev].CopyTo(ct, dest.Children[cev]);
			else foreach (var child in Children)
				child.Value.CopyTo(ct, dest.Children[child.Key]);
			ValidateChildren();

			RMVPool.ReturnAll();
			dest.PlayingMotions.Clear();
			foreach (var m in PlayingMotions) dest.PlayingMotions.Add(m.Key, m.Value);
		}

		public bool Disposed {
			get;
			private set;
		}
		public void Dispose() {
			if (Disposed) return;
			Disposed = true;
			if (Handler != null) Handler.Dispose();
			foreach (var s in Children)
				s.Value.Dispose();
			RMVPool.ReturnAll();
		}

		public void AttachHandler(ContainerHandler h) {
			if (Handler != null)
				throw new InvalidOperationException();
			Handler = h;
			h.cs = this;
		}

		public void AttachSystems(PdtSkin skin, Judge judge) {
			skinContainer = new SkinContainer(skin);
			this.judge = judge;
		}

		public Vector GetRawValue(Identifier key) {
			Vector tr;
			if (!CachedValues.TryGetValue(key, out tr)) {
				tr = (Vector)ReflectionHelper.InvokeEmptyConstructor(ChartPlayer.motionRegistry[key].Type);
				CachedValues.Add(key, tr);
				CachedValueStates[key] = false;
			}
			Vector r = tr;
#if !DISABLE_CACHE
			if (CachedValueStates[key]) return r;
#endif
			float reltime = 0;
			if (rootPrototype != null) reltime = Time - rootPrototype.Time;
			GetMotionValue(key).GetValue(reltime, ref r);
			if (Parent != null) r.ApplyFrom(Parent.GetRawValue(key));
#if !DISABLE_CACHE
			CachedValueStates[key] = true;
#endif
			return r;
		}

		public T GetRawValue<T>(Identifier key) where T : Vector {
			return (T)GetRawValue(key);
		}

		static readonly Identifier n_pt = new Identifier("pt");
		public Vector2 ScreenPoint {
			get {
				var mv = GetRawValue<VecPt>(n_pt);
				return mv.ToVector2(ChartPlayer.hitRect);
			}
		}

		static readonly Identifier n_dir = new Identifier("dir");
		public Vector3 Direction {
			get {
				Vec3 r = GetRawValue<Vec3>(n_dir);
				return r.ToVector3();
			}
		}

		static readonly Identifier n_normal = new Identifier("normal");
		public Vector3 Normal {
			get {
				Vec3 r = GetRawValue<Vec3>(n_normal);
				return r.ToVector3();
			}
		}

		public Quaternion QuatDir {
			get {
				return Quaternion.LookRotation(Quaternion.Euler(Direction) * Vector3.forward, Normal);
			}
		}

		static readonly Identifier n_sv = new Identifier("sv");
		static readonly Identifier n_svm = new Identifier("svm");
		public float ScrollVelocity {
			get {
				return GetRawValue<VecPtComp>(n_sv).ToFloat(ChartPlayer.hitRect)
					* GetRawValue<Vec1m>(n_svm).Value;
			}
		}

		static readonly Identifier n_dist = new Identifier("dist");
		public float Distance {
			get {
				var mv = GetRawValue<VecPtComp>(n_dist);
				return mv.ToFloat(ChartPlayer.hitRect);
			}
		}

		static readonly Identifier n_corner = new Identifier("corner");
		public bool Corner {
			get {
				return GetRawValue<VecI1>(n_corner).Value % 2 >= 1;
			}
		}

		static readonly Identifier n_ctrl0 = new Identifier("ctrl0");
		static readonly Identifier n_ctrl1 = new Identifier("ctrl1");
		public Vector3 GetControlPoint(bool alt1, float deltaz) {
			var mv = GetRawValue<VecCtrl>(alt1 ? n_ctrl1 : n_ctrl0);
			if (alt1 && mv.IsZero()) {
				mv = GetRawValue<VecCtrl>(n_ctrl0);
			}
			return mv.ToVector3(ChartPlayer.hitRect, deltaz);
		}

		static readonly Identifier n_track = new Identifier("track");
		public float Track {
			get {
				return GetRawValue<Vec1>(n_track).Value;
			}
		}

		bool breakflag = false;

		public void Break() {
			Handler.EndUpdate(this);
			breakflag = true;
			Working = false;
		}

		public void Handle(StampedEvent ev, Action<StampedEvent> callback = null) {
			if (breakflag) return;
			if (ev != null) {
				if (ev.Unstamped is Chart.Motion) {
					var tev = (Chart.Motion)ev.Unstamped;
					var mv = RMVPool.Rent(tev.Name);
					GetMotionValue(tev.Name).CopyTo(mv);
					PlayingMotions.Add(ev, mv);
					Callback(ev, callback);
					if (!ev.Unstamped.IsLong)
						PlayingMotions.Remove(ev);
				}
				else if (ev.Unstamped is EventContainer) {
					var cev = (EventContainer)ev.Unstamped;
					var ccs = GetChild(cev);
					ccs.Working = true;
					ccs.StartUpdate();
					UpdateMotions();
					if (!ev.Unstamped.IsLong) {
						ccs.Working = false;
						ccs.BroadcastEndUpdate();
						if (CloneType == 1) ccs.Dispose();
					}
				}
				else if (ev.Unstamped is InstantEvent) {
					var tev = (InstantEvent)ev.Unstamped;
					if (tev.IsRelease) {
						var nev = tev.Original;
						if (nev is Chart.Motion) {
							Callback(ev, callback);
							PlayingMotions.Remove(ev.Origin);
						}
						else if (nev is EventContainer) {
							var cev = (EventContainer)ev.Origin.Unstamped;
							var ccs = GetChild(cev);
							UpdateMotions();
							ccs.Working = false;
							ccs.BroadcastEndUpdate();
							if (CloneType == 1) ccs.Dispose();
						}
					}
				}
				Callback(ev.Unstamped == null || ev.Unstamped.Priority >= 0 ? ev : null, callback);
			}
			else Callback(null, callback);
		}

		void Callback(StampedEvent ev, Action<StampedEvent> callback) {
			UpdateMotions();
			if (callback != null)
				callback(ev);
			if (ev == null || ev.Unstamped != null) Handler.Update(this, ev);
			else Handler.ExUpdate(this, ev);
			foreach (var m in PlayingMotions)
				Handler.MotionUpdate(CloneType, (Chart.Motion)m.Key.Unstamped);
		}

		private void UpdateMotions() {
			foreach (var m in PlayingMotions) {
				var tev = (Chart.Motion)m.Key.Unstamped;
				if (tev.RelativeNode != null && CloneType == 2) continue;
				var value = GetMotionValue(tev.Name/*, true*/);
				InvalidateMotion(tev.Name);
				if (m.Key.Duration == 0) {
					if (tev.RelativeNode != null) {
						value.SetRelativeNode(tev.RelativeNode);
					}
					else {
						value.AbsoluteValue.ReplaceFrom(tev.AbsoluteValue);
					}
				}
				else {
					var scaledTime = (Time - m.Key.Time - ChartPlayer.actualRenderStep * tev.sumfix) / m.Key.Duration;
					var lerpedTime = MotionLerper.GetEaseTime(scaledTime, tev.transition, tev.rate);
					if (tev.RelativeNode != null) {
						var target = value.QueryRelativeNode(tev.RelativeNode.Id);
						tev.RelativeNode.LerpWith(m.Value.GetRelativeNode(tev.RelativeNode.Id), lerpedTime, ref target);
					}
					else {
						tev.AbsoluteValue.LerpWith(m.Value.AbsoluteValue, lerpedTime, ref value.AbsoluteValue);
					}
				}
				Values[tev.Name] = value;
			}
		}

		public void BroadcastInit() {
			Handler.PreInit();
			Handler.Init();
			foreach (var s in Children) {
				s.Value.BroadcastInit();
			}
			Handler.PostInit();
		}

		public void StartUpdate() {
			Handler.StartUpdate(this);
		}

		public void BroadcastEndUpdate() {
			Handler.EndUpdate(this);
			foreach (var ls in Children.Values) {
				if (ls.Working) ls.BroadcastEndUpdate();
			}
		}

		public void Anchor() {
			Handler.Anchor();
			foreach (var ls in Children.Values) {
				if (ls.Handler.Alive) ls.Anchor();
			}
		}
	}
}
