using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

namespace Cryville.Crtr.Event {
	public class EventBatcher : StateBase<ChartEvent> {
		public EventBus Bus {
			get;
			private set;
		}
		readonly Chart chart;
		ContainerState rootState;
		readonly Dictionary<ChartEvent, ContainerState> containerMap
			= new Dictionary<ChartEvent, ContainerState>();
		readonly Dictionary<EventContainer, ContainerState> stateMap
			= new Dictionary<EventContainer, ContainerState>();
		readonly Dictionary<ChartEvent, StampedEvent> map
			= new Dictionary<ChartEvent, StampedEvent>();
		readonly Dictionary<EventContainer, List<StampedEvent>> coeventMap
			= new Dictionary<EventContainer, List<StampedEvent>>();
		readonly HashSet<ChartEvent> coevents = new HashSet<ChartEvent>();
		readonly List<StampedEvent> stampedEvents = new List<StampedEvent>();
		readonly List<EventBatch> batches = new List<EventBatch>();

		double beat;
		float tempo;
		
		public EventBatcher(Chart c) : base(new List<ChartEvent>()) {
			chart = c;
			beat = chart.BeatPosition;
			tempo = (float)c.sigs[0].tempo;
			Events.Add(c);
			Events.Add(c.ReleaseEvent);
			AddEventContainer(c);
			Events.Sort((a, b) => a.BeatPosition.CompareTo(b.BeatPosition));
		}

		void AddEventContainer(EventContainer c, ContainerState parent = null) {
			var cs = new ContainerState(c, parent);
			stateMap.Add(c, cs);
			if (parent == null) {
				cs.Depth = 0;
				rootState = cs;
			}
			else {
				cs.Depth = (ushort)(parent.Depth + 1);
			}
			foreach (var ev in c.Events) {
				if (ev.time == null) {
					ev.time = c.time;
					if (ev is EventContainer) ev.endtime = c.endtime;
					if ((ev.CoeventMode & CoeventMode.Coevent) != 0) coevents.Add(ev);
				}
				if (ev.IsLong) {
					Events.Add(ev.ReleaseEvent);
					containerMap.Add(ev.ReleaseEvent, cs);
				}
				Events.Add(ev);
				containerMap.Add(ev, cs);
				if (ev is EventContainer)
					AddEventContainer((EventContainer)ev, cs);
			}
		}
		
		public override void ForwardOnceToTime(double toTime) {
			double toBeat = Math.Round(beat + (toTime - Time) * tempo / 60f, 6);
			if (EventId >= Events.Count)
				goto return_ahead;
			double ebeat = Events[EventId].BeatPosition;
			double etime = Math.Round((ebeat - beat) / tempo * 60f + Time, 6);
			if (etime > toTime)
				goto return_ahead;
			var batch = GetEventBatch();
			Time = etime;
			beat = ebeat;
			foreach (var ev in batch) {
				EventContainer con = null;
				if (containerMap.ContainsKey(ev)) con = containerMap[ev].Container;
				var sev = new StampedEvent() {
					Time = etime,
					Unstamped = ev,
					Container = con
				};
				if (ev is EventContainer) {
					var tev = (EventContainer)ev;
					stateMap[tev].StampedContainer = sev;
					stampedEvents.Add(new StampedEvent.ClipBehind {
						Container = con,
						Origin = sev,
						Time = etime + tev.Clip.Behind,
					});
					if (!ev.IsLong) {
						stampedEvents.Add(new StampedEvent.ClipAhead {
							Container = con,
							Origin = sev,
							Time = etime + tev.Clip.Ahead,
						});
					}
				}
				if (ev is ReleaseEvent) {
					var tev = (ReleaseEvent)ev;
					var oev = tev.Original;
					var pev = map[oev];
					pev.ReleaseEvent = sev;
					sev.Origin = pev;
					if (oev is EventContainer) {
						stampedEvents.Add(new StampedEvent.ClipAhead {
							Container = con,
							Origin = pev,
							Time = etime + ((EventContainer)oev).Clip.Ahead,
						});
					}
				}
				if (con != null && coevents.Contains(ev)) {
					List<StampedEvent> cevs;
					if (!coeventMap.TryGetValue(con, out cevs)) {
						coeventMap.Add(con, cevs = new List<StampedEvent>());
					}
					cevs.Add(sev);
					if ((ev.CoeventMode & CoeventMode.Standalone) != 0) {
						stampedEvents.Add(sev);
					}
				}
				else stampedEvents.Add(sev);
				map.Add(ev, sev);
				if (ev is Chart.Signature) {
					var tev = (Chart.Signature)ev;
					if (tev.tempo != null) tempo = (float)tev.tempo;
				}
				EventId++;
			}
			return;
		return_ahead:
			Time = toTime;
			beat = toBeat;
		}
		
		IOrderedEnumerable<ChartEvent> GetEventBatch() {
			float cbeat = Events[EventId].BeatPosition;
			int b = EventId;
			while (Mathf.Approximately(Events[b].BeatPosition, cbeat)) {
				b--;
				if (b == -1) break;
			}
			int a = EventId;
			while (Mathf.Approximately(Events[a].BeatPosition, cbeat)) {
				a++;
				if (a == Events.Count) break;
			}
			return from ev in Events.GetRange(b + 1, a - b - 1) orderby ev.Priority select ev;
		}
		
		public EventBus Batch() {
			stampedEvents.Sort(CompareStampedEvents);
			var cb = new EventBatch(stampedEvents[0].Time);
			foreach (var ev in stampedEvents) {
				if (ev.Time != cb.Time) {
					batches.Add(cb);
					cb = new EventBatch(ev.Time);
				}
				cb.Enqueue(ev);
				BatchCoevents(ev);
			}
			batches.Add(cb);
			Bus = new EventBus(rootState, batches);
			return Bus;
		}

		void BatchCoevents(StampedEvent ev, List<StampedEvent> ocevs = null) {
			if (!(ev.Unstamped is EventContainer)) return;
			List<StampedEvent> cevs;
			if (coeventMap.TryGetValue((EventContainer)ev.Unstamped, out cevs)) {
				var rootFlag = ocevs == null;
				if (rootFlag) ev.Coevents = ocevs = new List<StampedEvent>();
				foreach (var cev in cevs) {
					ocevs.Add(cev);
					BatchCoevents(cev, ocevs);
				}
				if (rootFlag) ocevs.Sort(CompareStampedEvents);
				else ocevs.Add(ev);
			}
		}

		int CompareStampedEvents(StampedEvent a, StampedEvent b) {
			int u = a.CompareTo(b);
			if (u != 0) return u;
			if (a.Unstamped != null && b.Unstamped != null && containerMap.ContainsKey(a.Unstamped) && containerMap.ContainsKey(b.Unstamped)) {
				u = containerMap[a.Unstamped].Depth.CompareTo(containerMap[b.Unstamped].Depth);
				if (u != 0) return u;
			}
			return 0;
		}
	}
}
