using Cryville.Crtr.Browsing;
using Quaver.API.Maps;
using Quaver.API.Maps.Structures;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;

namespace Cryville.Crtr.Extensions.Quaver {
	public class QuaverChartConverter : ResourceConverter {
		static readonly string[] SUPPORTED_FORMATS = { ".qua" };
		const double OFFSET = 0.05;

		public override string[] GetSupportedFormats() {
			return SUPPORTED_FORMATS;
		}

		public override IEnumerable<Resource> ConvertFrom(FileInfo file) {
			List<Resource> result = new List<Resource>();
			var src = Qua.Parse(file.FullName);
			var ruleset = "quaver!" + src.Mode.ToString().ToLower();
			var meta = new ChartMeta {
				name = src.DifficultyName,
				author = src.Creator,
				song = new SongMetaInfo {
					name = src.Title,
					author = src.Artist,
				},
				ruleset = ruleset,
				cover = src.BackgroundFile,
				note_count = src.HitObjects.Count,
			};
			var chart = new Chart {
				format = 2,
				time = new BeatTime(-4, 0, 1),
				ruleset = ruleset,
				sigs = new List<Chart.Signature>(),
				sounds = new List<Chart.Sound> {
					new Chart.Sound { time = new BeatTime(0, 0, 1), id = src.Title, offset = (float)(src.TimingPoints[0].StartTime / 1e3 + OFFSET) }
				},
				motions = new List<Chart.Motion>(),
				groups = new List<Chart.Group>(),
			};
			var group = new Chart.Group() {
				tracks = new List<Chart.Track>(),
				notes = new List<Chart.Note>(),
				motions = new List<Chart.Motion>(),
			};
			chart.groups.Add(group);
			result.Add(new RawChartResource(string.Format("{0} - {1}", meta.song.name, meta.name), chart, meta));
			result.Add(new SongResource(meta.song.name, new FileInfo(Path.Combine(file.DirectoryName, src.AudioFile))));

			var evs = new List<EventWrapper>();
			foreach (var e in src.HitObjects) evs.Add(new EventWrapper.HitObject(e));
			foreach (var e in src.SliderVelocities) evs.Add(new EventWrapper.SliderVelocity(e));
			foreach (var e in src.SoundEffects) evs.Add(new EventWrapper.SoundEffect(e));
			foreach (var e in src.TimingPoints) evs.Add(new EventWrapper.TimingPoint(e));
			var evc = evs.Count;
			for (int i = 0; i < evc; i++) if (evs[i].IsLong) evs.Add(new EventWrapper.EndEvent(evs[i]));
			evs.Sort();

			var longevs = new Dictionary<EventWrapper, ChartEvent>();
			var tm = new TimeTimingModel(src.TimingPoints[0].StartTime / 1e3);
			foreach (var ev in evs) {
				tm.ForwardTo(ev.StartTime / 1e3);
				if (ev is EventWrapper.HitObject) {
					var tev = (EventWrapper.HitObject)ev;
					var rn = new Chart.Note {
						time = tm.FractionalBeatTime,
						motions = new List<Chart.Motion> {
							new Chart.Motion { motion = string.Format(CultureInfo.InvariantCulture, "track:{0}", tev.Event.Lane - 1) }
						},
					};
					if (ev.IsLong) longevs.Add(ev, rn);
					group.notes.Add(rn);
				}
				else if (ev is EventWrapper.SliderVelocity) {
					var tev = (EventWrapper.SliderVelocity)ev;
					group.motions.Add(new Chart.Motion {
						time = tm.FractionalBeatTime,
						motion = string.Format(CultureInfo.InvariantCulture, "svm:{0}", tev.Event.Multiplier),
					});
				}
				else if (ev is EventWrapper.TimingPoint) {
					var tev = (EventWrapper.TimingPoint)ev;
					tm.BPM = tev.Event.Bpm;
					chart.sigs.Add(new Chart.Signature {
						time = tm.FractionalBeatTime,
						tempo = tev.Event.Bpm,
					});
				}
				else if (ev is EventWrapper.EndEvent) {
					var tev = (EventWrapper.EndEvent)ev;
					var oev = tev.Original;
					longevs[oev].endtime = tm.FractionalBeatTime;
				}
				else throw new NotSupportedException("Sound effects are not supported yet");
			}
			var endbeat = tm.FractionalBeatTime;
			endbeat.b += 4;
			chart.endtime = endbeat;
			meta.length = (float)tm.Time;
			return result;
		}

		abstract class EventWrapper : IComparable<EventWrapper> {
			public abstract int StartTime { get; }
			public abstract int EndTime { get; }
			public bool IsLong { get { return EndTime > 0; } }
			public abstract int Priority { get; }
			public int CompareTo(EventWrapper other) {
				var c = StartTime.CompareTo(other.StartTime);
				if (c != 0) return c;
				return Priority.CompareTo(other.Priority);
			}
			public class HitObject : EventWrapper {
				public HitObjectInfo Event;
				public HitObject(HitObjectInfo ev) { Event = ev; }
				public override int StartTime { get { return Event.StartTime; } }
				public override int EndTime { get { return Event.EndTime; } }
				public override int Priority { get { return 0; } }
			}
			public class SliderVelocity : EventWrapper {
				public SliderVelocityInfo Event;
				public SliderVelocity(SliderVelocityInfo ev) { Event = ev; }
				public override int StartTime { get { return (int)Event.StartTime; } }
				public override int EndTime { get { return 0; } }
				public override int Priority { get { return 0; } }
			}
			public class SoundEffect : EventWrapper {
				public SoundEffectInfo Event;
				public SoundEffect(SoundEffectInfo ev) { Event = ev; }
				public override int StartTime { get { return (int)Event.StartTime; } }
				public override int EndTime { get { return 0; } }
				public override int Priority { get { return 0; } }
			}
			public class TimingPoint : EventWrapper {
				public TimingPointInfo Event;
				public TimingPoint(TimingPointInfo ev) { Event = ev; }
				public override int StartTime { get { return (int)Event.StartTime; } }
				public override int EndTime { get { return 0; } }
				public override int Priority { get { return -2; } }
			}
			public class EndEvent : EventWrapper {
				public EventWrapper Original;
				public EndEvent(EventWrapper ev) { if (!ev.IsLong) throw new ArgumentException("Event is not long"); Original = ev; }
				public override int StartTime { get { return Original.EndTime; } }
				public override int EndTime { get { return 0; } }
				public override int Priority { get { return Original.Priority - 1; } }
			}
		}
	}
}
