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;
		}
		ContainerState RootState;
		/// <summary>
		/// event => the container state that handles the event
		/// </summary>
		readonly Dictionary<ChartEvent, ContainerState> table
			= new Dictionary<ChartEvent, ContainerState>();
		public List<StampedEvent> stampedEvents = new List<StampedEvent>();
		readonly List<EventBatch> batches = new List<EventBatch>();
		
		float beat;
		float tempo;
		
		public EventBatcher(Chart c) : base(c, new List<ChartEvent>()) {
			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(chart, c, parent);
			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.IsLong) {
					events.Add(ev.ReleaseEvent);
					table.Add(ev.ReleaseEvent, cs);
				}
				events.Add(ev);
				table.Add(ev, cs);
				if (ev is EventContainer)
					AddEventContainer((EventContainer)ev, cs);
			}
		}
		
		public override void ForwardOnceToTime(float toTime, Action<ChartEvent> callback) {
			float toBeat = (float)Math.Round(beat + (toTime - Time) * tempo / 60f, 6);
			if (EventId >= events.Count)
				goto return_ahead;
			float ebeat = events[EventId].BeatPosition;
			float etime = (float)Math.Round((ebeat - beat) / tempo * 60f + Time, 6);
			if (etime > toTime)
				goto return_ahead;
			var batch = GetEventBatch();
			Time = etime;
			beat = ebeat;
			bool flag = false;
			foreach (var ev in batch) {
				EventContainer con = null;
				if (table.ContainsKey(ev)) con = table[ev].Container;
				var sev = new StampedEvent() {
					Time = etime,
					Unstamped = ev,
					Container = con
				};
				if (ev is InstantEvent) {
					var tev = (InstantEvent)ev;
					var pev = stampedEvents.First(tpev => tpev.Unstamped == tev.Original);
					pev.Subevents.Add(sev);
					sev.Origin = pev;
				}
				stampedEvents.Add(sev);
				if (ev.Priority >= 0) {
					if (callback != null) callback(ev);
					flag = true;
				}
				if (ev is Chart.Signature) {
					var tev = (Chart.Signature)ev;
					if (tev.tempo != null) tempo = (float)tev.tempo;
				}
				EventId++;
			}
			if (callback != null && !flag) callback(batch.First());
			return;
		return_ahead:
			Time = toTime;
			beat = toBeat;
			if (callback != null) callback(null);
		}
		
		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((a, b) => {
				int u = a.CompareTo(b);
				if (u != 0) return u;
				if (a.Unstamped != null && b.Unstamped != null)
					if (table.ContainsKey(a.Unstamped) && table.ContainsKey(b.Unstamped)) {
						u = table[a.Unstamped].Depth.CompareTo(table[b.Unstamped].Depth);
						if (u != 0) return u;
					}
				return a.GetHashCode().CompareTo(b.GetHashCode());
			});
			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);
			}
			batches.Add(cb);
			Bus = new EventBus(chart, RootState, batches);
			return Bus;
		}
	}
}
