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

namespace Cryville.Crtr.Extensions.Bestdori {
	public class BestdoriChartConverter : ResourceConverter {
		static readonly string[] SUPPORTED_FORMATS = { ".json" };
		public override string[] GetSupportedFormats() {
			return SUPPORTED_FORMATS;
		}

		public override IEnumerable<Resource> ConvertFrom(FileInfo file) {
			List<Resource> result = new List<Resource>();
			List<BestdoriChartEvent> src;
			using (var reader = new StreamReader(file.FullName)) {
				src = JsonConvert.DeserializeObject<List<BestdoriChartEvent>>(reader.ReadToEnd());
			}
			var group = new Chart.Group() {
				tracks = new List<Chart.Track>(),
				notes = new List<Chart.Note>(),
				motions = new List<Chart.Motion>(),
			};
			Chart chart = new Chart {
				format = 2,
				time = new BeatTime(0, 0, 1),
				ruleset = "bang_dream_girls_band_party",
				sigs = new List<Chart.Signature>(),
				sounds = new List<Chart.Sound>(),
				motions = new List<Chart.Motion>(),
				groups = new List<Chart.Group> { group },
			};
			var tm = new BeatTimeTimingModel();
			string bgm = null;
			double endbeat = 0;
			foreach (var ev in src) {
				tm.ForwardTo(ev.StartBeat);
				if (ev.StartBeat > endbeat) endbeat = ev.StartBeat;
				if (ev is BestdoriChartEvent.System) {
					if (bgm != null) continue;
					var tev = (BestdoriChartEvent.System)ev;
					bgm = StringUtils.TrimExt(tev.data);
					var name = "bang_dream_girls_band_party__" + bgm;
					result.Add(new SongResource(name, new FileInfo(Path.Combine(file.Directory.FullName, tev.data))));
					chart.sounds.Add(new Chart.Sound { time = ToBeatTime(tev.beat), id = name });
				}
				else if (ev is BestdoriChartEvent.BPM) {
					var tev = (BestdoriChartEvent.BPM)ev;
					tm.BPM = tev.bpm;
					chart.sigs.Add(new Chart.Signature { time = ToBeatTime(tev.beat), tempo = (float)tev.bpm });
				}
				else if (ev is BestdoriChartEvent.Single) {
					var tev = (BestdoriChartEvent.Single)ev;
					group.notes.Add(new Chart.Note {
						time = ToBeatTime(tev.beat),
						judges = new List<Chart.Judge> { new Chart.Judge { name = tev.flick ? "single_flick" : "single" } },
						motions = new List<Chart.Motion> { new Chart.Motion { motion = "track:" + tev.lane.ToString(CultureInfo.InvariantCulture) } },
					});
				}
				else if (ev is BestdoriChartEvent.Long) {
					var tev = (BestdoriChartEvent.Long)ev;
					var c1 = tev.connections[tev.connections.Count - 1];
					var note = new Chart.Note {
						time = ToBeatTime(tev.connections[0].beat),
						endtime = ToBeatTime(c1.beat),
						judges = new List<Chart.Judge>(),
					};
					for (int i = 0; i < tev.connections.Count; i++) {
						BestdoriChartEvent.Connection c = tev.connections[i];
						if (i == 0) {
							note.judges.Add(new Chart.Judge { name = "single" });
							note.motions.Add(new Chart.Motion { motion = "track:" + c.lane.ToString(CultureInfo.InvariantCulture) });
						}
						else {
							note.motions.Add(new Chart.Motion { time = ToBeatTime(tev.connections[i - 1].beat), endtime = ToBeatTime(c.beat), motion = "track:" + c.lane.ToString(CultureInfo.InvariantCulture) });
							if (i == tev.connections.Count - 1)
								note.judges.Add(new Chart.Judge { time = ToBeatTime(c.beat), name = c.flick ? "longend_flick" : "longend" });
							else if (!c.hidden)
								note.judges.Add(new Chart.Judge { time = ToBeatTime(c.beat), name = "longnode" });
						}
					}
					if (c1.beat > endbeat) endbeat = c1.beat;
					group.notes.Add(note);
				}
				else throw new NotImplementedException("Unsupported event: " + ev.type);
			}
			if (bgm == null) throw new FormatException("Chart contains no song");
			chart.endtime = ToBeatTime(endbeat + 4);
			if (endbeat > tm.BeatTime) tm.ForwardTo(endbeat);
			result.Add(new RawChartResource(string.Format("bang_dream_girls_band_party__{0}__{1}", bgm, StringUtils.TrimExt(file.Name)), chart, new ChartMeta {
				name = string.Format("Bandori {0} {1}", bgm, StringUtils.TrimExt(file.Name)),
				author = "©BanG Dream! Project ©Craft Egg Inc. ©bushiroad",
				ruleset = "bang_dream_girls_band_party",
				note_count = group.notes.Count,
				length = (float)tm.Time,
				song = new SongMetaInfo {
					name = bgm,
					author = "©BanG Dream! Project ©Craft Egg Inc. ©bushiroad",
				}
			}));
			return result;
		}

		BeatTime ToBeatTime(double beat, double error = 1e-4) {
			int i, n, d;
			FractionUtils.ToFraction(beat, error, out n, out d);
			i = n / d; n %= d;
			return new BeatTime(i, n, d);
		}

#pragma warning disable IDE1006
		[JsonConverter(typeof(BestdoriChartEventCreator))]
		abstract class BestdoriChartEvent {
			public abstract string type { get; }
			public abstract double StartBeat { get; }
			public abstract class InstantEvent : BestdoriChartEvent {
				public double beat;
				public override double StartBeat { get { return beat; } }
			}
			public class BPM : InstantEvent {
				public override string type { get { return "BPM"; } }
				public double bpm;
			}
			public class System : InstantEvent {
				public override string type { get { return "System"; } }
				public string data;
			}
			public abstract class SingleBase : InstantEvent {
				public double lane;
				public bool skill;
				public bool flick;
			}
			public class Single : SingleBase {
				public override string type { get { return "Single"; } }
			}
			public class Directional : SingleBase {
				public override string type { get { return "Directional"; } }
				public string direction;
				public int width;
			}
			public class Connection : SingleBase {
				public override string type { get { return null; } }
				public bool hidden;
			}
			public class Long : BestdoriChartEvent {
				public override string type { get { return "Long"; } }
				public List<Connection> connections;
				public override double StartBeat { get { return connections[0].beat; } }
			}
			public class Slide : Long {
				public override string type { get { return "Slide"; } }
			}
		}
#pragma warning restore IDE1006
		class BestdoriChartEventCreator : CustomCreationConverter<BestdoriChartEvent> {
			string _currentType;

			public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) {
				var obj = JObject.ReadFrom(reader);
				var type = obj["type"];
				if (type == null) _currentType = null;
				else _currentType = obj["type"].ToObject<string>();
				return base.ReadJson(obj.CreateReader(), objectType, existingValue, serializer);
			}

			public override BestdoriChartEvent Create(Type objectType) {
				switch (_currentType) {
					case "BPM": return new BestdoriChartEvent.BPM();
					case "System": return new BestdoriChartEvent.System();
					case "Single": return new BestdoriChartEvent.Single();
					case "Directional": return new BestdoriChartEvent.Directional();
					case null: return new BestdoriChartEvent.Connection();
					case "Long": return new BestdoriChartEvent.Long();
					case "Slide": return new BestdoriChartEvent.Slide();
					default: throw new ArgumentException("Unknown event type: " + _currentType);
				}
			}
		}
	}
}
