using Cryville.Common;
using Cryville.Common.Pdt;
using Cryville.Crtr.Config;
using Cryville.Input;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Threading;
using UnityEngine;
using UnityEngine.Profiling;
using Logger = Cryville.Common.Logging.Logger;
using RVector4 = UnityEngine.Vector4;

namespace Cryville.Crtr.Ruleset {
	public class InputProxy : IDisposable {
		readonly PdtEvaluator _etor;
		readonly PdtRuleset _ruleset;
		readonly Judge _judge;
		readonly InputVector _screenSize;
		public InputProxy(PdtRuleset ruleset, Judge judge, Vector2 screenSize) {
			for (int i = 0; i <= MAX_DEPTH; i++) {
				var vecsrc = new InputVectorSrc();
				_vecsrcs[i] = vecsrc;
				_vecops[i] = new InputVectorOp(vecsrc);
			}
			_etor = judge != null ? judge._etor : PdtEvaluator.Instance;
			_ruleset = ruleset;
			_judge = judge;
			_screenSize = new InputVector(screenSize.x, screenSize.y);
			foreach (var i in ruleset.inputs) {
				_use.Add(i.Key, 0);
				_rev.Add(i.Key, new List<Identifier>());
			}
			foreach (var i in ruleset.inputs) {
				if (i.Value.pass != null) {
					foreach (var p in i.Value.pass)
						_rev[p.Key].Add(i.Key);
				}
			}
		}
		#region Settings
		readonly Dictionary<Identifier, InputProxyEntry> _tproxies = new Dictionary<Identifier, InputProxyEntry>();
		readonly Dictionary<InputSource, InputProxyEntry> _sproxies = new Dictionary<InputSource, InputProxyEntry>();
		readonly Dictionary<Identifier, int> _use = new Dictionary<Identifier, int>();
		readonly Dictionary<Identifier, List<Identifier>> _rev = new Dictionary<Identifier, List<Identifier>>();
		public event EventHandler<ProxyChangedEventArgs> ProxyChanged;
		public void LoadFrom(Dictionary<string, RulesetConfig.InputEntry> config) {
			foreach (var cfg in config) {
				var handler = Game.InputManager.GetHandlerByTypeName(cfg.Value.handler);
				if (handler == null) {
					Logger.Log("main", 3, "Input", "Uninitialized or unknown handler in ruleset config: {0}", cfg.Value.handler);
					continue;
				}
				Set(new InputProxyEntry {
					Target = new Identifier(cfg.Key),
					Source = new InputSource {
						Handler = handler,
						Type = cfg.Value.type
					}
				});
			}
		}
		public void SaveTo(Dictionary<string, RulesetConfig.InputEntry> config) {
			config.Clear();
			foreach (var p in _tproxies) {
				config.Add((string)p.Key.Name, new RulesetConfig.InputEntry {
					handler = p.Value.Source.Value.Handler.GetType().AssemblyQualifiedName,
					type = p.Value.Source.Value.Type
				});
			}
		}
		public void Clear() {
			_tproxies.Clear();
			_sproxies.Clear();
			foreach (var i in _rev) _use[i.Key] = 0;
		}
		public void Set(InputProxyEntry proxy) {
			var target = proxy.Target;
			if (!_ruleset.inputs.ContainsKey(target)) throw new ArgumentException("Invalid input name");
			if (_tproxies.ContainsKey(target)) Remove(proxy);
			if (_use[target] > 0)
				throw new InvalidOperationException("Input already assigned");
			if (proxy.Source != null) {
				_tproxies.Add(target, proxy);
				_sproxies.Add(proxy.Source.Value, proxy);
				IncrementUseRecursive(target);
				IncrementReversedUseRecursive(target);
			}
		}
		void Remove(InputProxyEntry proxy) {
			var target = proxy.Target;
			_sproxies.Remove(_tproxies[target].Source.Value);
			_tproxies.Remove(target);
			DecrementUseRecursive(target);
			DecrementReversedUseRecursive(target);
		}
		public bool IsUsed(InputSource src) {
			return _sproxies.ContainsKey(src);
		}
		public bool IsCompleted() {
			foreach (var i in _use)
				if (!IsCompleted(i.Key)) return false;
			return true;
		}
		bool IsCompleted(Identifier name) {
			return name.Key == _var_pause || _use[name] != 0 || _tproxies.ContainsKey(name);
		}
		static readonly int _var_pause = IdentifierManager.Shared.Request("pause");
		void IncrementUseRecursive(Identifier name) {
			BroadcastProxyChanged(name);
			var passes = _ruleset.inputs[name].pass;
			if (passes != null) {
				foreach (var p in _ruleset.inputs[name].pass) {
					_use[p.Key]++;
					IncrementUseRecursive(p.Key);
				}
			}
		}
		void IncrementReversedUseRecursive(Identifier name) {
			foreach (var p in _rev[name]) {
				_use[p]++;
				BroadcastProxyChanged(p);
				IncrementReversedUseRecursive(p);
			}
		}
		void DecrementUseRecursive(Identifier name) {
			BroadcastProxyChanged(name);
			var passes = _ruleset.inputs[name].pass;
			if (passes != null) {
				foreach (var p in _ruleset.inputs[name].pass) {
					_use[p.Key]--;
					DecrementUseRecursive(p.Key);
				}
			}
		}
		void DecrementReversedUseRecursive(Identifier name) {
			foreach (var p in _rev[name]) {
				_use[p]--;
				BroadcastProxyChanged(p);
				DecrementReversedUseRecursive(p);
			}
		}
		void BroadcastProxyChanged(Identifier name) {
			var del = ProxyChanged;
			if (del != null) del(this, this[name]);
		}
		public ProxyChangedEventArgs this[Identifier name] {
			get {
				return new ProxyChangedEventArgs(name, _tproxies.ContainsKey(name) ? _tproxies[name].Source : null, _use[name] > 0, !IsCompleted(name));
			}
		}
		#endregion

		#region Handling
		public void Activate() {
			_vect.Clear(); _vecs.Clear();
			foreach (var src in _sproxies) {
				var isrc = src.Value.Source;
				if (isrc != null) {
					isrc.Value.Handler.OnInput += OnInput;
					isrc.Value.Handler.OnBatch += OnBatch;
				}
			}
			_targetActiveCount.Clear();
			foreach (var i in _ruleset.inputs) {
				if (i.Value.pass == null) _targetActiveCount.Add(i.Key, 0);
			}
		}
		public void Deactivate() {
			foreach (var src in _sproxies) {
				var isrc = src.Value.Source;
				if (isrc != null) {
					isrc.Value.Handler.OnInput -= OnInput;
					isrc.Value.Handler.OnBatch -= OnBatch;
				}
			}
		}

		~InputProxy() {
			Dispose(false);
			GC.SuppressFinalize(this);
		}
		public void Dispose() {
			Dispose(true);
		}
		protected void Dispose(bool disposing) {
			if (disposing) {
				Deactivate();
				foreach (var proxy in _tproxies) {
					proxy.Value.Source.Value.Handler.OnInput -= OnInput;
				}
			}
		}

		static readonly int _var_input_vec = IdentifierManager.Shared.Request("input_vec");
		const int MAX_DEPTH = 15;
		const int MAX_DIMENSION = 4;
		readonly InputVectorSrc[] _vecsrcs = new InputVectorSrc[MAX_DEPTH + 1];
		readonly InputVectorOp[] _vecops = new InputVectorOp[MAX_DEPTH + 1];
		unsafe class InputVectorSrc : PropSrc.FixedBuffer<RVector4> {
			public InputVectorSrc() : base(PdtInternalType.Vector, MAX_DIMENSION * sizeof(float) + sizeof(int), null) {
				fixed (byte* ptr = buf) {
					*(int*)(ptr + MAX_DIMENSION * sizeof(float)) = PdtInternalType.Number;
				}
			}
			public bool IsNull { get; set; }
			public RVector4 Get() {
				fixed (byte* _ptr = buf) {
					return *(RVector4*)_ptr;
				}
			}
			public void Set(RVector4 vec) {
				fixed (byte* _ptr = buf) {
					*(RVector4*)_ptr = vec;
				}
				Invalidate();
			}
		}
		class InputVectorOp : PropOp {
			readonly InputVectorSrc _src;
			public InputVectorOp(InputVectorSrc src) {
				_src = src;
			}
			protected override void Execute() {
				var op = GetOperand(0);
				if (op.Type == PdtInternalType.Null) {
					_src.IsNull = true;
				}
				else {
					var vec = new RVector4();
					int dim;
					if (op.Type == PdtInternalType.Number) dim = 1;
					else if (op.Type == PdtInternalType.Vector) {
						int arrtype, _;
						op.GetArraySuffix(out arrtype, out _);
						if (arrtype != PdtInternalType.Number)
							throw new InvalidCastException("Not a vector of numbers");
						dim = Math.Min(3, (op.Length - sizeof(int)) / sizeof(float));
					}
					else throw new InvalidCastException("Invalid vector");
					for (int i = 0; i < dim; i++) {
						vec[i] = op.AsNumber(i * sizeof(float));
					}
					_src.IsNull = false;
					_src.Set(vec);
				}
			}
		}
		readonly Dictionary<InputHandler, double> _timeOrigins = new Dictionary<InputHandler, double>();
		readonly Dictionary<Identifier, int> _targetActiveCount = new Dictionary<Identifier, int>();
		readonly Dictionary<InputIdentifier, float> _vect = new Dictionary<InputIdentifier, float>();
		readonly Dictionary<ProxiedInputIdentifier, Vector4> _vecs = new Dictionary<ProxiedInputIdentifier, Vector4>();
		double? _lockTime = null;
		unsafe void OnInput(InputIdentifier id, InputFrame frame) {
			var rc = id.Source.Handler.ReferenceCue;
			if (rc.RelativeUnit == RelativeUnit.Pixel) {
				frame = rc.InverseTransform(frame, _screenSize);
				var vec = frame.Vector;
				vec.X /= _screenSize.X; vec.Y /= _screenSize.Y;
				vec.X -= 0.5f; vec.Y -= 0.5f;
				vec.X *= ChartPlayer.hitRect.width; vec.Y *= ChartPlayer.hitRect.height;
				frame.Vector = vec;
			}
			else frame = rc.InverseTransform(frame);
			Monitor.Enter(_etor);
			try {
				Profiler.BeginSample("InputProxy.OnInput");
				InputProxyEntry proxy;
				if (_sproxies.TryGetValue(id.Source, out proxy)) {
					float ft, tt = (float)GetSyncedTime(frame.Time, id.Source.Handler);
					if (!_vect.TryGetValue(id, out ft)) ft = tt;
					_etor.ContextCascadeInsert();
					try {
						if (frame.IsNull) {
							_etor.ContextCascadeUpdate(_var_input_vec, PropSrc.Null);
							OnInput(id, proxy.Target, ft, tt, true);
						}
						else {
							_vecsrcs[0].Set(new RVector4(frame.Vector.X, frame.Vector.Y, frame.Vector.Z, frame.Vector.W));
							_etor.ContextCascadeUpdate(_var_input_vec, _vecsrcs[0]);
							OnInput(id, proxy.Target, ft, tt, false);
						}
					}
					finally {
						_etor.ContextCascadeDiscard();
					}
					_vect[id] = tt;
				}
			}
			finally {
				Profiler.EndSample();
				Monitor.Exit(_etor);
			}
		}
		static readonly int _var_fv = IdentifierManager.Shared.Request("input_vec_from");
		static readonly int _var_tv = IdentifierManager.Shared.Request("input_vec_to");
		readonly InputVectorSrc _vecsrc = new InputVectorSrc();
		unsafe void OnInput(InputIdentifier id, Identifier target, float ft, float tt, bool nullFlag, int depth = 0) {
			if (depth >= MAX_DEPTH) throw new InputProxyException("Input propagation limit reached\nThe ruleset has invalid input definitions");
			var def = _ruleset.inputs[target];
			if (def.pass != null) {
				foreach (var p in def.pass) {
					_etor.ContextCascadeInsert();
					try {
						bool newNullFlag = nullFlag;
						if (!newNullFlag) {
							_etor.Evaluate(_vecops[depth + 1], p.Value);
							newNullFlag = _vecsrcs[depth + 1].IsNull;
							if (newNullFlag) _etor.ContextCascadeUpdate(_var_input_vec, PropSrc.Null);
							else _etor.ContextCascadeUpdate(_var_input_vec, _vecsrcs[depth + 1]);
						}
						OnInput(id, p.Key, ft, tt, newNullFlag, depth + 1);
					}
					finally {
						_etor.ContextCascadeDiscard();
					}
				}
			}
			else {
				var pid = new ProxiedInputIdentifier { Source = id, Target = target };
				PropSrc fv, tv = _etor.ContextCascadeLookup(_var_input_vec);
				bool hfv; Vector4 ifv;
				if (hfv = _vecs.TryGetValue(pid, out ifv)) {
					_vecsrc.Set(ifv);
					fv = _vecsrc;
				}
				else fv = PropSrc.Null;
				if (fv.Type != PdtInternalType.Null || tv.Type != PdtInternalType.Null) {
					if (fv.Type == PdtInternalType.Null) {
						_targetActiveCount[target]++;
					}
					_etor.ContextCascadeInsert();
					try {
						_etor.ContextCascadeUpdate(_var_fv, fv);
						_etor.ContextCascadeUpdate(_var_tv, tv);
						_judge.Feed(target, ft, tt);
					}
					finally {
						_etor.ContextCascadeDiscard();
					}
					if (tv.Type == PdtInternalType.Null) {
						_vecs.Remove(pid);
						_targetActiveCount[target]--;
					}
					else {
						var itv = ((InputVectorSrc)tv).Get();
						if (!hfv || ifv != itv) _vecs[pid] = itv;
					}
				}
			}
		}
		void OnBatch(InputHandler handler, double time) {
			lock (_etor) {
				foreach (var vec in _vecs) {
					if (vec.Key.Source.Source.Handler != handler) continue;
					InputProxyEntry proxy;
					if (!_sproxies.TryGetValue(vec.Key.Source.Source, out proxy)) continue;

					float ft, tt = (float)GetSyncedTime(time, handler);
					if (_vect.TryGetValue(vec.Key.Source, out ft) && ft < tt) {
						_etor.ContextCascadeInsert();
						_vecsrcs[0].Set(vec.Value);
						_etor.ContextCascadeUpdate(_var_input_vec, _vecsrcs[0]);
						OnInput(vec.Key.Source, proxy.Target, ft, tt, false);
						_etor.ContextCascadeDiscard();
						_vect[vec.Key.Source] = tt;
					}

					Cleanup(proxy.Target, tt, true);
				}
			}
		}
		void Cleanup(Identifier target, float tt, bool batching = false, int depth = 0) {
			if (depth >= MAX_DEPTH) throw new InputProxyException("Input propagation limit reached\nThe ruleset has invalid input definitions");
			var def = _ruleset.inputs[target];
			if (def.pass != null) {
				foreach (var p in def.pass) {
					Cleanup(p.Key, tt, batching, depth + 1);
				}
			}
			else if (batching || _targetActiveCount[target] == 0) {
				_judge.Cleanup(target, tt);
			}
		}
		public void SyncTime(double time) {
			foreach (var s in _sproxies) {
				var h = s.Key.Handler;
				_timeOrigins[h] = h.GetCurrentTimestamp() - time;
			}
		}
		const double batchDelayTolerance = 0.02;
		public void ForceTick() {
			lock (_etor) {
				foreach (var s in _sproxies) {
					var handler = s.Key.Handler;
					Cleanup(s.Value.Target, (float)GetSyncedTime(handler.GetCurrentTimestamp() - batchDelayTolerance, handler));
				}
			}
		}
		public double GetTimestampAverage() {
			double result = 0;
			foreach (var s in _sproxies) {
				var src = s.Key;
				result += src.Handler.GetCurrentTimestamp() - _timeOrigins[src.Handler];
			}
			return result / _sproxies.Count;
		}
		double GetSyncedTime(double time, InputHandler handler) {
			return _lockTime != null ? _lockTime.Value : (time - _timeOrigins[handler]);
		}
		public void LockTime() { _lockTime = GetTimestampAverage(); }
		public void UnlockTime() { _lockTime = null; }
		#endregion
	}

	public class ProxyChangedEventArgs : EventArgs {
		public Identifier Name { get; private set; }
		public InputSource? Proxy { get; private set; }
		public bool Used { get; private set; }
		public bool Required { get; private set; }
		public ProxyChangedEventArgs(Identifier name, InputSource? src, bool used, bool required) {
			Name = name;
			Proxy = src;
			Used = used;
			Required = required;
		}
	}

	public class InputProxyEntry {
		public InputSource? Source { get; set; }
		public Identifier Target { get; set; }
		public byte[] Mapping { get; private set; }
	}

	public struct ProxiedInputIdentifier : IEquatable<ProxiedInputIdentifier> {
		public InputIdentifier Source { get; set; }
		public Identifier Target { get; set; }
		public override bool Equals(object obj) {
			if (obj == null || !(obj is ProxiedInputIdentifier)) return false;
			return Equals((ProxiedInputIdentifier)obj);
		}
		public bool Equals(ProxiedInputIdentifier other) {
			return Source == other.Source && Target == other.Target;
		}
		public override int GetHashCode() {
			return Source.GetHashCode() ^ Target.GetHashCode();
		}
		public override string ToString() {
			return string.Format("{0}->{1}", Source, Target);
		}
		public static bool operator ==(ProxiedInputIdentifier lhs, ProxiedInputIdentifier rhs) {
			return lhs.Equals(rhs);
		}
		public static bool operator !=(ProxiedInputIdentifier lhs, ProxiedInputIdentifier rhs) {
			return !lhs.Equals(rhs);
		}
	}

	[Serializable]
	public class InputProxyException : Exception {
		public InputProxyException() { }
		public InputProxyException(string message) : base(message) { }
		public InputProxyException(string message, Exception inner) : base(message, inner) { }
		protected InputProxyException(SerializationInfo info, StreamingContext context) : base(info, context) { }
	}
}
