using System;
using System.IO;

using Newtonsoft.Json;

using BizHawk.Emulation.Cores.Sony.PSX;

namespace BizHawk.Client.Common
{
	[ImportExtension(".pjm")]
	public class PjmImport : MovieImporter
	{
		protected override void RunImport()
		{
			Bk2Movie movie = Result.Movie;

			movie.HeaderEntries[HeaderKeys.PLATFORM] = "PSX";

			using (var fs = SourceFile.OpenRead())
			{
				using (var br = new BinaryReader(fs))
				{
					var info = ParseHeader(movie, "PJM ", br);

					fs.Seek(info.ControllerDataOffset, SeekOrigin.Begin);

					if (info.BinaryFormat)
					{
						ParseBinaryInputLog(br, movie, info);
					}
					else
					{
						ParseTextInputLog(br, movie, info);
					}
				}
			}

			movie.Save();
		}

		protected MiscHeaderInfo ParseHeader(Bk2Movie movie, string expectedMagic, BinaryReader br)
		{
			var info = new MiscHeaderInfo();

			string magic = new string(br.ReadChars(4));
			if (magic != expectedMagic)
			{
				Result.Errors.Add("Not a " + expectedMagic + "file: invalid magic number in file header.");
				return info;
			}

			uint movieVersionNumber = br.ReadUInt32();
			if (movieVersionNumber != 2)
			{
				Result.Warnings.Add($"Unexpected movie version: got {movieVersionNumber}, expecting 2");
			}

			// 008: UInt32 emulator version.
			br.ReadUInt32();

			byte flags = br.ReadByte();
			byte flags2 = br.ReadByte();
			if ((flags & 0x02) != 0)
			{
				Result.Errors.Add("Movie starts from savestate; this is currently unsupported.");
			}

			if ((flags & 0x04) != 0)
			{
				movie.HeaderEntries[HeaderKeys.PAL] = "1";
			}

			if ((flags & 0x08) != 0)
			{
				Result.Errors.Add("Movie contains embedded memory cards; this is currently unsupported.");
			}

			if ((flags & 0x10) != 0)
			{
				Result.Errors.Add("Movie contains embedded cheat list; this is currently unsupported.");
			}

			if ((flags & 0x20) != 0 || (flags2 & 0x06) != 0)
			{
				Result.Errors.Add("Movie relies on emulator hacks; this is currently unsupported.");
			}

			if ((flags & 0x40) != 0)
			{
				info.BinaryFormat = false;
			}

			if ((flags & 0x80) != 0 || (flags2 & 0x01) != 0)
			{
				Result.Errors.Add("Movie uses multitap; this is currently unsupported.");
				return info;
			}

			// Player 1 controller type
			switch (br.ReadByte())
			{
				// It seems to be inconsistent in the files I looked at which of these is used
				// to mean no controller present.
				case 0:
				case 8:
					info.Player1Type = OctoshockDll.ePeripheralType.None;
					break;
				case 4:
					info.Player1Type = OctoshockDll.ePeripheralType.Pad;
					break;
				case 7:
					info.Player1Type = OctoshockDll.ePeripheralType.DualShock;
					break;
				default:
					Result.Errors.Add("Movie has unrecognised controller type for Player 1.");
					return info;
			}

			// Player 2 controller type
			switch (br.ReadByte())
			{
				case 0:
				case 8:
					info.Player2Type = OctoshockDll.ePeripheralType.None;
					break;
				case 4:
					info.Player2Type = OctoshockDll.ePeripheralType.Pad;
					break;
				case 7:
					info.Player2Type = OctoshockDll.ePeripheralType.DualShock;
					break;
				default:
					Result.Errors.Add("Movie has unrecognised controller type for Player 2.");
					return info;
			}

			var syncsettings = new Octoshock.SyncSettings
			{
				FIOConfig =
				{
					Devices8 = new[]
					{
						info.Player1Type,
						OctoshockDll.ePeripheralType.None,
						OctoshockDll.ePeripheralType.None,
						OctoshockDll.ePeripheralType.None,
						info.Player2Type,
						OctoshockDll.ePeripheralType.None,
						OctoshockDll.ePeripheralType.None,
						OctoshockDll.ePeripheralType.None
					}
				}
			};

			// Annoying kludge to force the json serializer to serialize the type name for "o" object.
			// For just the "o" object to have type information, it must be cast to a superclass such
			// that the TypeNameHandling.Auto decides to serialize the type as well as the object
			// contents.  As such, the object cast is NOT redundant
			var jsonSettings = new JsonSerializerSettings
			{
				TypeNameHandling = TypeNameHandling.Auto
			};
			movie.SyncSettingsJson = JsonConvert.SerializeObject(new { o = (object)syncsettings }, jsonSettings);

			info.FrameCount = br.ReadUInt32();
			uint rerecordCount = br.ReadUInt32();
			movie.HeaderEntries[HeaderKeys.RERECORDS] = rerecordCount.ToString();

			// 018: UInt32 savestateOffset
			// 01C: UInt32 memoryCard1Offset
			// 020: UInt32 memoryCard2Offset
			// 024: UInt32 cheatListOffset

			// 028: UInt32 cdRomIdOffset
			// Source format is just the first up-to-8 alphanumeric characters of the CD label, 
			// so not so useful.
			br.ReadBytes(20);

			info.ControllerDataOffset = br.ReadUInt32();

			uint authorNameLength = br.ReadUInt32();
			char[] authorName = br.ReadChars((int)authorNameLength);

			movie.HeaderEntries[HeaderKeys.AUTHOR] = new string(authorName);

			info.ParseSuccessful = true;
			return info;
		}

		protected void ParseBinaryInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info)
		{
			Octoshock.SyncSettings settings = new Octoshock.SyncSettings();
			SimpleController controllers = new SimpleController();
			settings.FIOConfig.Devices8 = new[]
			{ 
				info.Player1Type,
				OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None,
				info.Player2Type,
				OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None
			};
			controllers.Definition = Octoshock.CreateControllerDefinition(settings);

			string[] buttons =
			{
				"Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left",
				"L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"
			};

			bool isCdTrayOpen = false;
			int cdNumber = 1;

			for (int frame = 0; frame < info.FrameCount; ++frame)
			{
				if (info.Player1Type != OctoshockDll.ePeripheralType.None)
				{
					ushort controllerState = br.ReadUInt16();

					// As L3 and R3 don't exist on a standard gamepad, handle them separately later.  Unfortunately
					// due to the layout, we handle select separately too first.
					controllers["P1 Select"] = (controllerState & 0x1) != 0;

					for (int button = 3; button < buttons.Length; button++)
					{
						controllers["P1 " + buttons[button]] = ((controllerState >> button) & 0x1) != 0;
						if (((controllerState >> button) & 0x1) != 0 && button > 15)
						{
							continue;
						}
					}

					if (info.Player1Type == OctoshockDll.ePeripheralType.DualShock)
					{
						controllers["P1 L3"] = (controllerState & 0x2) != 0;
						controllers["P1 R3"] = (controllerState & 0x4) != 0;
						Tuple<string, float> leftX = new Tuple<string, float>("P1 LStick X", (float)br.ReadByte());
						Tuple<string, float> leftY = new Tuple<string, float>("P1 LStick Y", (float)br.ReadByte());
						Tuple<string, float> rightX = new Tuple<string, float>("P1 RStick X", (float)br.ReadByte());
						Tuple<string, float> rightY = new Tuple<string, float>("P1 RStick Y", (float)br.ReadByte());

						controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY });
					}
				}

				if (info.Player2Type != OctoshockDll.ePeripheralType.None)
				{
					ushort controllerState = br.ReadUInt16();
					for (int button = 0; button < buttons.Length; button++)
					{
						controllers["P2 " + buttons[button]] = ((controllerState >> button) & 0x1) != 0;
						if (((controllerState >> button) & 0x1) != 0 && button > 15)
						{
							continue;
						}
					}

					if (info.Player2Type == OctoshockDll.ePeripheralType.DualShock)
					{
						Tuple<string, float> leftX = new Tuple<string, float>("P2 LStick X", (float)br.ReadByte());
						Tuple<string, float> leftY = new Tuple<string, float>("P2 LStick Y", (float)br.ReadByte());
						Tuple<string, float> rightX = new Tuple<string, float>("P2 RStick X", (float)br.ReadByte());
						Tuple<string, float> rightY = new Tuple<string, float>("P2 RStick Y", (float)br.ReadByte());

						controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY });
					}
				}

				byte controlState = br.ReadByte();
				controllers["Reset"] = (controlState & 0x02) != 0;
				if ((controlState & 0x04) != 0)
				{
					if (isCdTrayOpen)
					{
						controllers["Close"] = true;
						cdNumber++;
					}
					else
					{
						controllers["Open"] = true;
					}

					isCdTrayOpen = !isCdTrayOpen;
				}
				else
				{
					controllers["Close"] = false;
					controllers["Open"] = false;
				}

				Tuple<string, float> discSelect = new Tuple<string, float>("Disc Select", cdNumber);
				controllers.AcceptNewFloats(new[] { discSelect });

				if ((controlState & 0xFC) != 0)
				{
					Result.Warnings.Add("Ignored toggle hack flag on frame " + frame);
				}

				movie.AppendFrame(controllers);
			}
		}

		protected void ParseTextInputLog(BinaryReader br, Bk2Movie movie, MiscHeaderInfo info)
		{
			Octoshock.SyncSettings settings = new Octoshock.SyncSettings();
			SimpleController controllers = new SimpleController();
			settings.FIOConfig.Devices8 = new[]
			{
				info.Player1Type,
				OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None,
				info.Player2Type,
				OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None, OctoshockDll.ePeripheralType.None
			};
			controllers.Definition = Octoshock.CreateControllerDefinition(settings);

			string[] buttons =
			{
				"Select", "L3", "R3", "Start", "Up", "Right", "Down", "Left",
				"L2", "R2", "L1", "R1", "Triangle", "Circle", "Cross", "Square"
			};

			bool isCdTrayOpen = false;
			int cdNumber = 1;

			for (int frame = 0; frame < info.FrameCount; ++frame)
			{
				if (info.Player1Type != OctoshockDll.ePeripheralType.None)
				{
					// As L3 and R3 don't exist on a standard gamepad, handle them separately later.  Unfortunately
					// due to the layout, we handle select separately too first.
					controllers["P1 Select"] = br.ReadChar() != '.';

					if (info.Player1Type == OctoshockDll.ePeripheralType.DualShock)
					{
						controllers["P1 L3"] = br.ReadChar() != '.';
						controllers["P1 R3"] = br.ReadChar() != '.';
					}

					for (int button = 3; button < buttons.Length; button++)
					{
						controllers["P1 " + buttons[button]] = br.ReadChar() != '.';
					}

					if (info.Player1Type == OctoshockDll.ePeripheralType.DualShock)
					{
						// The analog controls are encoded as four space-separated numbers with a leading space
						string leftXRaw = new string(br.ReadChars(4)).Trim();
						string leftYRaw = new string(br.ReadChars(4)).Trim();
						string rightXRaw = new string(br.ReadChars(4)).Trim();
						string rightYRaw = new string(br.ReadChars(4)).Trim();

						Tuple<string, float> leftX = new Tuple<string, float>("P1 LStick X", float.Parse(leftXRaw));
						Tuple<string, float> leftY = new Tuple<string, float>("P1 LStick Y", float.Parse(leftYRaw));
						Tuple<string, float> rightX = new Tuple<string, float>("P1 RStick X", float.Parse(rightXRaw));
						Tuple<string, float> rightY = new Tuple<string, float>("P1 RStick Y", float.Parse(rightYRaw));

						controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY });
					}
				}

				// Each controller is terminated with a pipeline.
				br.ReadChar();

				if (info.Player2Type != OctoshockDll.ePeripheralType.None)
				{
					// As L3 and R3 don't exist on a standard gamepad, handle them separately later.  Unfortunately
					// due to the layout, we handle select separately too first.
					controllers["P2 Select"] = br.ReadChar() != '.';

					if (info.Player2Type == OctoshockDll.ePeripheralType.DualShock)
					{
						controllers["P2 L3"] = br.ReadChar() != '.';
						controllers["P2 R3"] = br.ReadChar() != '.';
					}

					for (int button = 3; button < buttons.Length; button++)
					{
						controllers["P2 " + buttons[button]] = br.ReadChar() != '.';
					}

					if (info.Player2Type == OctoshockDll.ePeripheralType.DualShock)
					{
						// The analog controls are encoded as four space-separated numbers with a leading space
						string leftXRaw = new string(br.ReadChars(4)).Trim();
						string leftYRaw = new string(br.ReadChars(4)).Trim();
						string rightXRaw = new string(br.ReadChars(4)).Trim();
						string rightYRaw = new string(br.ReadChars(4)).Trim();

						Tuple<string, float> leftX = new Tuple<string, float>("P2 LStick X", float.Parse(leftXRaw));
						Tuple<string, float> leftY = new Tuple<string, float>("P2 LStick Y", float.Parse(leftYRaw));
						Tuple<string, float> rightX = new Tuple<string, float>("P2 RStick X", float.Parse(rightXRaw));
						Tuple<string, float> rightY = new Tuple<string, float>("P2 RStick Y", float.Parse(rightYRaw));

						controllers.AcceptNewFloats(new[] { leftX, leftY, rightX, rightY });
					}
				}

				// Each controller is terminated with a pipeline.
				br.ReadChar();

				byte controlState = br.ReadByte();
				controllers["Reset"] = (controlState & 0x02) != 0;
				if ((controlState & 0x04) != 0)
				{
					if (isCdTrayOpen)
					{
						controllers["Close"] = true;
						cdNumber++;
					}
					else
					{
						controllers["Open"] = true;
					}

					isCdTrayOpen = !isCdTrayOpen;
				}
				else
				{
					controllers["Close"] = false;
					controllers["Open"] = false;
				}

				Tuple<string, float> discSelect = new Tuple<string, float>("Disc Select", cdNumber);
				controllers.AcceptNewFloats(new[] { discSelect });

				if ((controlState & 0xFC) != 0)
				{
					Result.Warnings.Add("Ignored toggle hack flag on frame " + frame);
				}

				// Each controller is terminated with a pipeline.
				br.ReadChar();

				movie.AppendFrame(controllers);
			}
		}

		protected class MiscHeaderInfo
		{
			public bool BinaryFormat { get; set; } = true;
			public uint ControllerDataOffset { get; set; }
			public uint FrameCount { get; set; }
			public OctoshockDll.ePeripheralType Player1Type { get; set; }
			public OctoshockDll.ePeripheralType Player2Type { get; set; }
			public bool ParseSuccessful { get; set; }
		}
	}
}
