using System.IO;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores;
using BizHawk.Emulation.Cores.Nintendo.GBA;
using BizHawk.Emulation.Cores.Nintendo.GBHawk;
using BizHawk.Emulation.Cores.Nintendo.Gameboy;

namespace BizHawk.Client.Common.movie.import
{
	// VBM file format: http://code.google.com/p/vba-rerecording/wiki/VBM
	// ReSharper disable once UnusedMember.Global
	[ImporterFor("Visual Boy Advance", ".vbm")]
	internal class VbmImport : MovieImporter
	{
		protected override void RunImport()
		{
			using var fs = SourceFile.Open(FileMode.Open, FileAccess.Read);
			using var r = new BinaryReader(fs);
			bool is_GBC = false;

			// 000 4-byte signature: 56 42 4D 1A "VBM\x1A"
			string signature = new string(r.ReadChars(4));
			if (signature != "VBM\x1A")
			{
				Result.Errors.Add("This is not a valid .VBM file.");
				return;
			}

			// 004 4-byte little-endian unsigned int: major version number, must be "1"
			uint majorVersion = r.ReadUInt32();
			if (majorVersion != 1)
			{
				Result.Errors.Add(".VBM major movie version must be 1.");
				return;
			}

			/*
			 008 4-byte little-endian integer: movie "uid" - identifies the movie-savestate relationship, also used as the
			 recording time in Unix epoch format
			*/
			uint uid = r.ReadUInt32();

			// 00C 4-byte little-endian unsigned int: number of frames
			uint frameCount = r.ReadUInt32();

			// 010 4-byte little-endian unsigned int: rerecord count
			uint rerecordCount = r.ReadUInt32();
			Result.Movie.Rerecords = rerecordCount;

			// 014 1-byte flags: (movie start flags)
			byte flags = r.ReadByte();

			// bit 0: if "1", movie starts from an embedded "quicksave" snapshot
			bool startFromSavestate = (flags & 0x1) != 0;

			// bit 1: if "1", movie starts from reset with an embedded SRAM
			bool startFromSram = ((flags >> 1) & 0x1) != 0;

			// other: reserved, set to 0
			// (If both bits 0 and 1 are "1", the movie file is invalid)
			if (startFromSavestate && startFromSram)
			{
				Result.Errors.Add("This is not a valid .VBM file.");
				return;
			}

			if (startFromSavestate)
			{
				Result.Errors.Add("Movies that begin with a savestate are not supported.");
				return;
			}

			if (startFromSram)
			{
				Result.Errors.Add("Movies that begin with SRAM are not supported.");
				return;
			}

			// 015 1-byte flags: controller flags
			byte controllerFlags = r.ReadByte();
			/*
			 * bit 0: controller 1 in use
			 * bit 1: controller 2 in use (SGB games can be 2-player multiplayer)
			 * bit 2: controller 3 in use (SGB games can be 3- or 4-player multiplayer with multitap)
			 * bit 3: controller 4 in use (SGB games can be 3- or 4-player multiplayer with multitap)
			*/
			bool[] controllersUsed = new bool[4];
			for (int controller = 1; controller <= controllersUsed.Length; controller++)
			{
				controllersUsed[controller - 1] = ((controllerFlags >> (controller - 1)) & 0x1) != 0;
			}

			if (!controllersUsed[0])
			{
				Result.Errors.Add("Controller 1 must be in use.");
				return;
			}

			// other: reserved
			// 016 1-byte flags: system flags (game always runs at 60 frames/sec)
			flags = r.ReadByte();

			// bit 0: if "1", movie is for the GBA system
			bool isGBA = (flags & 0x1) != 0;

			// bit 1: if "1", movie is for the GBC system
			bool isGBC = ((flags >> 1) & 0x1) != 0;

			// bit 2: if "1", movie is for the SGB system
			bool isSGB = ((flags >> 2) & 0x1) != 0;

			// (If all 3 of these bits are "0", it is for regular GB.)
			string platform = "GB";
			if (isGBA)
			{
				platform = "GBA";
				Result.Movie.HeaderEntries[HeaderKeys.Core] = CoreNames.Mgba;
			}

			if (isGBC)
			{
				is_GBC = true;
				platform = "GB";
				Result.Movie.HeaderEntries.Add("IsCGBMode", "1");
			}

			if (isSGB)
			{
				Result.Errors.Add("SGB imports are not currently supported");
			}

			Result.Movie.HeaderEntries[HeaderKeys.Platform] = platform;

			// 017 1-byte flags: (values of some boolean emulator options)
			flags = r.ReadByte();
			/*
			 * bit 0: (useBiosFile) if "1" and the movie is of a GBA game, the movie was made using a GBA BIOS file.
			 * bit 1: (skipBiosFile) if "0" and the movie was made with a GBA BIOS file, the BIOS intro is included in the
			 * movie.
			 * bit 2: (rtcEnable) if "1", the emulator "real time clock" feature was enabled.
			 * bit 3: (unsupported) must be "0" or the movie file is considered invalid (legacy).
			*/
			if (((flags >> 3) & 0x1) != 0)
			{
				Result.Errors.Add("This is not a valid .VBM file.");
				return;
			}

			/*
			 018 4-byte little-endian unsigned int: theApp.winSaveType (value of that emulator option)
			 01C 4-byte little-endian unsigned int: theApp.winFlashSize (value of that emulator option)
			 020 4-byte little-endian unsigned int: gbEmulatorType (value of that emulator option)
			*/
			r.ReadBytes(12);
			/*
			 024 12-byte character array: the internal game title of the ROM used while recording, not necessarily
			 null-terminated (ASCII?)
			*/
			string gameName = NullTerminated(new string(r.ReadChars(12)));
			Result.Movie.HeaderEntries[HeaderKeys.GameName] = gameName;

			// 030 1-byte unsigned char: minor version/revision number of current VBM version, the latest is "1"
			byte minorVersion = r.ReadByte();
			Result.Movie.Comments.Add($"{MovieOrigin} .VBM version {majorVersion}.{minorVersion}");
			Result.Movie.Comments.Add($"{EmulationOrigin} Visual Boy Advance");

			// 031 1-byte unsigned char: the internal CRC of the ROM used while recording
			r.ReadByte();
			/*
			 032 2-byte little-endian unsigned short: the internal Checksum of the ROM used while recording, or a
			 calculated CRC16 of the BIOS if GBA
			*/
			ushort checksumCRC16 = r.ReadUInt16();
			/*
			 034 4-byte little-endian unsigned int: the Game Code of the ROM used while recording, or the Unit Code if not
			 GBA
			*/
			uint gameCodeUnitCode = r.ReadUInt32();
			if (platform == "GBA")
			{
				Result.Movie.HeaderEntries["CRC16"] = checksumCRC16.ToString();
				Result.Movie.HeaderEntries["GameCode"] = gameCodeUnitCode.ToString();
			}
			else
			{
				Result.Movie.HeaderEntries["InternalChecksum"] = checksumCRC16.ToString();
				Result.Movie.HeaderEntries["UnitCode"] = gameCodeUnitCode.ToString();
			}

			// 038 4-byte little-endian unsigned int: offset to the savestate or SRAM inside file, set to 0 if unused
			r.ReadBytes(4);

			// 03C 4-byte little-endian unsigned int: offset to the controller data inside file
			uint firstFrameOffset = r.ReadUInt32();

			// After the header is 192 bytes of text. The first 64 of these 192 bytes are for the author's name (or names).
			string author = NullTerminated(new string(r.ReadChars(64)));
			Result.Movie.HeaderEntries[HeaderKeys.Author] = author;

			// The following 128 bytes are for a description of the movie. Both parts must be null-terminated.
			string movieDescription = NullTerminated(new string(r.ReadChars(128)));
			Result.Movie.Comments.Add(movieDescription);
			r.BaseStream.Position = firstFrameOffset;

			SimpleController controllers = isGBA
				? GbaController()
				: GbController();

			/*
			 * 01 00 A
			 * 02 00 B
			 * 04 00 Select
			 * 08 00 Start
			 * 10 00 Right
			 * 20 00 Left
			 * 40 00 Up
			 * 80 00 Down
			 * 00 01 R
			 * 00 02 L
			*/
			string[] buttons = { "A", "B", "Select", "Start", "Right", "Left", "Up", "Down", "R", "L" };
			/*
			 * 00 04 Reset (old timing)
			 * 00 08 Reset (new timing since version 1.1)
			 * 00 10 Left motion sensor
			 * 00 20 Right motion sensor
			 * 00 40 Down motion sensor
			 * 00 80 Up motion sensor
			*/
			string[] other =
			{
				"Reset (old timing)", "Reset (new timing since version 1.1)", "Left motion sensor",
				"Right motion sensor", "Down motion sensor", "Up motion sensor"
			};

			for (int frame = 1; frame <= frameCount; frame++)
			{
				/*
				 A stream of 2-byte bitvectors which indicate which buttons are pressed at each point in time. They will
				 come in groups of however many controllers are active, in increasing order.
				*/
				ushort controllerState = r.ReadUInt16();
				for (int button = 0; button < buttons.Length; button++)
				{
					controllers[buttons[button]] = ((controllerState >> button) & 0x1) != 0;
					if (((controllerState >> button) & 0x1) != 0 && button > 7)
					{
						continue;
					}
				}

				// TODO: Handle the other buttons.
				for (int button = 0; button < other.Length; button++)
				{
					if (((controllerState >> (button + 10)) & 0x1) != 0)
					{
						Result.Warnings.Add($"Unable to import {other[button]} at frame {frame}.");
						break;
					}
				}

				// TODO: Handle the additional controllers.
				for (int player = 2; player <= controllersUsed.Length; player++)
				{
					if (controllersUsed[player - 1])
					{
						r.ReadBytes(2);
					}
				}

				Result.Movie.AppendFrame(controllers);
			}

			if (isGBA)
			{
				var ss = new MGBAHawk.SyncSettings { SkipBios = true };
				Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(ss);
			}
			else
			{
				if (Config.PreferredCores["GB"] == CoreNames.GbHawk || Config.PreferredCores["GB"] == CoreNames.SubGbHawk)
				{
					var tempSync = new GBHawk.GBSyncSettings();
					if (is_GBC) { tempSync.ConsoleMode = GBHawk.GBSyncSettings.ConsoleModeType.GBC; }
					else { tempSync.ConsoleMode = GBHawk.GBSyncSettings.ConsoleModeType.GB; }
					Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(tempSync);
				}
				else
				{
					var temp_sync = new Gameboy.GambatteSyncSettings();
					if (is_GBC) { temp_sync.ConsoleMode = Gameboy.GambatteSyncSettings.ConsoleModeType.GBC; }
					else { temp_sync.ConsoleMode = Gameboy.GambatteSyncSettings.ConsoleModeType.GB; }
					Result.Movie.SyncSettingsJson = ConfigService.SaveWithType(temp_sync);
				}
			}
		}

		private static SimpleController GbController()
		{
			return new SimpleController
			{
				Definition = new ControllerDefinition
				{
					BoolButtons = { "Up", "Down", "Left", "Right", "Start", "Select", "B", "A", "Power" }
				}
			};
		}

		private static SimpleController GbaController()
			=> new SimpleController { Definition = MGBAHawk.GBAController };
	}
}
