using Cryville.Crtr.Browsing;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;

namespace Cryville.Crtr.Extensions.Malody {
	public class MalodyChartConverter : ResourceConverter {
		static readonly string[] SUPPORTED_FORMATS = { ".mc", ".mcz" };
		static readonly string[] MODES = { "key", "step", "dj", "catch", "pad", "taiko", "ring", "slide", "live" };

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

		public override IEnumerable<Resource> ConvertFrom(FileInfo file) {
			List<Resource> result = new List<Resource>();
			MalodyChart src;
			using (var reader = new StreamReader(file.FullName)) {
				src = JsonConvert.DeserializeObject<MalodyChart>(reader.ReadToEnd());
			}
			if (src.meta.mode != 0) throw new NotImplementedException("The chart mode is not supported");
			if (src.meta.mode_ext.column != 4) throw new NotImplementedException("The key count is not supported");

			var ruleset = "malody!" + MODES[src.meta.mode];
			if (src.meta.mode == 0) {
				ruleset += "." + src.meta.mode_ext.column.ToString(CultureInfo.InvariantCulture) + "k";
			}

			ChartMeta meta = new ChartMeta() {
				song = new ChartMeta.MetaInfo() {
					name = src.meta.song.titleorg != null ? src.meta.song.titleorg : src.meta.song.title,
					author = src.meta.song.artistorg != null ? src.meta.song.artistorg : src.meta.song.artist,
				},
				ruleset = ruleset,
			};

			Chart chart = new Chart {
				format = 2,
				time = new BeatTime(-4, 0, 1),
				ruleset = ruleset,
				sigs = new List<Chart.Signature>(),
				sounds = new List<Chart.Sound>(),
				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);
			int col = src.meta.mode_ext.column;

			IEnumerable<MalodyChart.IEvent> events = src.time.Cast<MalodyChart.IEvent>();
			if (src.effect != null) events = events.Concat(src.effect.Cast<MalodyChart.IEvent>());
			events = events.Concat(src.note.Cast<MalodyChart.IEvent>());
			List<MalodyChart.IEvent> endEvents = new List<MalodyChart.IEvent>();
			foreach (var ev in events)
				if (ev.endbeat != null)
					endEvents.Add(new MalodyChart.EndEvent {
						beat = ev.endbeat,
						StartEvent = ev,
					});
			events = events.Concat(endEvents)
				.OrderBy(e => e.beat[0] + (float)e.beat[1] / e.beat[2])
				.ThenBy(e => e.Priority);

			Dictionary<MalodyChart.IEvent, StartEventState> longEvents
				= new Dictionary<MalodyChart.IEvent, StartEventState>();

			float? baseBpm = null;
			float pbeat = 0f, ctime = 0f;
			int[] endbeat = new int[] { 0, 0, 1 };
			foreach (var ev in events) {
				float cbeat = ConvertBeat(ev.beat);
				ctime += baseBpm == null ? 0 : (cbeat - pbeat) / baseBpm.Value * 60f;
				pbeat = cbeat;
				if (ev is MalodyChart.Time) {
					var tev = (MalodyChart.Time)ev;
					if (baseBpm == null) baseBpm = tev.bpm;
					chart.sigs.Add(new Chart.Signature {
						time = new BeatTime(ev.beat[0], ev.beat[1], ev.beat[2]),
						tempo = tev.bpm,
					});
					chart.motions.Add(new Chart.Motion {
						time = new BeatTime(ev.beat[0], ev.beat[1], ev.beat[2]),
						motion = "svm:" + (tev.bpm / baseBpm).ToString()
					});
				}
				else if (ev is MalodyChart.Effect) {
					var tev = (MalodyChart.Effect)ev;
					if (tev.scroll != null) group.motions.Add(new Chart.Motion {
						time = new BeatTime(ev.beat[0], ev.beat[1], ev.beat[2]),
						motion = "svm:" + tev.scroll.ToString()
					});
				}
				else if (ev is MalodyChart.Note) {
					var tev = (MalodyChart.Note)ev;
					if (tev.type == 1) {
						if (tev.beat[0] == 0 && tev.beat[1] == 0) {
							var res = new SongResource(meta.song.name, new FileInfo(file.DirectoryName + "/" + tev.sound));
							result.Add(res);
							chart.sounds.Add(new Chart.Sound {
								time = new BeatTime(0, 0, 1),
								id = res.Name,
								offset = -tev.offset / 1000f,
							});
						}
						else throw new NotImplementedException();
					}
					else {
						if (ConvertBeat(tev.beat) > ConvertBeat(endbeat)) endbeat = tev.beat;
						var rn = new Chart.Note() {
							time = new BeatTime(tev.beat[0], tev.beat[1], tev.beat[2]),
							motions = new List<Chart.Motion> {
								new Chart.Motion() { motion = "track:" + tev.column.ToString() }
							},
						};
						if (tev.endbeat != null) {
							if (ConvertBeat(tev.endbeat) > ConvertBeat(endbeat)) endbeat = tev.endbeat;
							rn.endtime = new BeatTime(tev.endbeat[0], tev.endbeat[1], tev.endbeat[2]);
							longEvents.Add(ev, new StartEventState {
								Destination = rn,
								Time = ctime,
							});
						}
						group.notes.Add(rn);
					}
				}
				else if (ev is MalodyChart.EndEvent) {
					var tev = (MalodyChart.EndEvent)ev;
					if (tev.StartEvent is MalodyChart.Note) {
						var sev = tev.StartEvent;
						longEvents.Remove(sev);
					}
					else throw new NotSupportedException();
				}
				else throw new NotSupportedException();
			}
			chart.endtime = new BeatTime(endbeat[0] + 4, endbeat[1], endbeat[2]);
			meta.chart = new ChartMeta.MetaInfo() {
				name = src.meta.version,
				author = src.meta.creator,
				length = ctime,
			};
			meta.note_count = group.notes.Count;
			string chartName = string.Format("{0} - {1}", meta.song.name, meta.chart.name);
			result.Add(new ChartResource(chartName, chart, meta));
			if (src.meta.background != null)
				result.Add(new CoverResource(chartName, new FileInfo(file.DirectoryName + "/" + src.meta.background)));
			return result;
		}

		struct StartEventState {
			public float Time { get; set; }
			public ChartEvent Destination { get; set; }
		}

		float ConvertBeat(int[] beat) {
			return beat[0] + (float)beat[1] / beat[2];
		}
	}

#pragma warning disable IDE1006
	public struct MalodyChart {
		public interface IEvent {
			int[] beat { get; set; }
			int[] endbeat { get; set; }
			int Priority { get; }
		}
		public struct EndEvent : IEvent {
			public int[] beat { get; set; }
			public int[] endbeat { get; set; }
			public IEvent StartEvent { get; set; }
			public int Priority { get { return StartEvent.Priority - 1; } }
		}

		public Meta meta;
		public struct Meta {
			public SongInfo song;
			public struct SongInfo {
				public string title;
				public string artist;
				public string titleorg;
				public string artistorg;
			}

			public string background;

			public string creator;
			public string version;

			public int mode;
			public ModeExt mode_ext;
			public struct ModeExt {
				public int column;
			}
		}

		public List<Time> time;
		public struct Time : IEvent {
			public int[] beat { get; set; }
			public int[] endbeat { get; set; }
			public float bpm;
			public int Priority { get { return -2; } }
		}

		public List<Effect> effect;
		public struct Effect : IEvent {
			public int[] beat { get; set; }
			public int[] endbeat { get; set; }
			public float? scroll;
			public int Priority { get { return 0; } }
		}

		public List<Note> note;
		public struct Note : IEvent {
			public int[] beat { get; set; }
			public int[] endbeat { get; set; }
			public int column;
			public string sound;
			public int offset;
			public int type;
			public int Priority { get { return 0; } }
		}
	}
#pragma warning restore IDE1006
}
