using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using BizHawk.Common;
/**
  This is the cartridge class for Arcadia (aka Starpath) Supercharger 
  games.  Christopher Salomon provided most of the technical details 
  used in creating this class.  A good description of the Supercharger
  is provided in the Cuttle Cart's manual.

  The Supercharger has four 2K banks.  There are three banks of RAM 
  and one bank of ROM.  All 6K of the RAM can be read and written.
 
  D7-D5 of this byte: Write Pulse Delay (n/a for emulator)
  
  D4-D0: RAM/ROM configuration:
        $F000-F7FF    $F800-FFFF Address range that banks map into
   000wp     2            ROM
   001wp     0            ROM
   010wp     2            0      as used in Commie Mutants and many others
   011wp     0            2      as used in Suicide Mission
   100wp     2            ROM
   101wp     1            ROM
   110wp     2            1      as used in Killer Satellites
   111wp     1            2      as we use for 2k/4k ROM cloning
  
   w = Write Enable (1 = enabled; accesses to $F000-$F0FF cause writes
     to happen.  0 = disabled, and the cart acts like ROM.)
   p = ROM Power (0 = enabled, 1 = off.)  Only power the ROM if you're
     wanting to access the ROM for multiloads.  Otherwise set to 1.
*/
namespace BizHawk.Emulation.Cores.Atari.Atari2600
{
	internal class mAR : MapperBase
	{
		public mAR(Atari2600 core)
		{
			Core = core;
			InitializeSettings();
		}

		private ByteBuffer _superChargerImage = new ByteBuffer(8192);
		private IntBuffer _imageOffsets = new IntBuffer(2);
		private bool _writePending = false;
		private int _distinctAccesses = 0;
		private bool _writeEnabled = false;
		private byte _dataHoldRegister;
		private byte _numberOfLoadImages;
		private ByteBuffer _loadedImages;
		private ByteBuffer _header = new ByteBuffer(256);
		private bool _powerIndicator; // Indicates if the ROM's power is on or off
		private int _powerRomCycle; // Indicates when the power was last turned on
		private int _size;
		private ulong _elapsedCycles;

		private void InitializeSettings()
		{
			// TODO: clean this stuff up
			/*****************************************/
			int size = Core.Rom.Length;
			_size = Core.Rom.Length < 8448 ? 8448 : Core.Rom.Length; //8448 or Rom size, whichever is bigger

			_numberOfLoadImages = (byte)(_size / 8448);

			// TODO: why are we making a redundant copy?
			_loadedImages = new ByteBuffer(_size);
			for (int i = 0; i < size; i++)
			{
				_loadedImages[i] = Core.Rom[i];
			}

			if (size < 8448)
			{
				for (int i = size; i < _size; i++)
				{
					_loadedImages[i] = DefaultHeader[i];
				}
			}
			/*****************************************/

			InitializeRom();
			BankConfiguration(0);
		}

		#region SuperCharger Data

		private readonly byte[] DummyRomCode = {
			0xa5, 0xfa, 0x85, 0x80, 0x4c, 0x18, 0xf8, 0xff,
			0xff, 0xff, 0x78, 0xd8, 0xa0, 0x00, 0xa2, 0x00,
			0x94, 0x00, 0xe8, 0xd0, 0xfb, 0x4c, 0x50, 0xf8,
			0xa2, 0x00, 0xbd, 0x06, 0xf0, 0xad, 0xf8, 0xff,
			0xa2, 0x00, 0xad, 0x00, 0xf0, 0xea, 0xbd, 0x00,
			0xf7, 0xca, 0xd0, 0xf6, 0x4c, 0x50, 0xf8, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xa2, 0x03, 0xbc, 0x22, 0xf9, 0x94, 0xfa, 0xca,
			0x10, 0xf8, 0xa0, 0x00, 0xa2, 0x28, 0x94, 0x04,
			0xca, 0x10, 0xfb, 0xa2, 0x1c, 0x94, 0x81, 0xca,
			0x10, 0xfb, 0xa9, 0xff, 0xc9, 0x00, 0xd0, 0x03,
			0x4c, 0x13, 0xf9, 0xa9, 0x00, 0x85, 0x1b, 0x85,
			0x1c, 0x85, 0x1d, 0x85, 0x1e, 0x85, 0x1f, 0x85,
			0x19, 0x85, 0x1a, 0x85, 0x08, 0x85, 0x01, 0xa9,
			0x10, 0x85, 0x21, 0x85, 0x02, 0xa2, 0x07, 0xca,
			0xca, 0xd0, 0xfd, 0xa9, 0x00, 0x85, 0x20, 0x85,
			0x10, 0x85, 0x11, 0x85, 0x02, 0x85, 0x2a, 0xa9,
			0x05, 0x85, 0x0a, 0xa9, 0xff, 0x85, 0x0d, 0x85,
			0x0e, 0x85, 0x0f, 0x85, 0x84, 0x85, 0x85, 0xa9,
			0xf0, 0x85, 0x83, 0xa9, 0x74, 0x85, 0x09, 0xa9,
			0x0c, 0x85, 0x15, 0xa9, 0x1f, 0x85, 0x17, 0x85,
			0x82, 0xa9, 0x07, 0x85, 0x19, 0xa2, 0x08, 0xa0,
			0x00, 0x85, 0x02, 0x88, 0xd0, 0xfb, 0x85, 0x02,
			0x85, 0x02, 0xa9, 0x02, 0x85, 0x02, 0x85, 0x00,
			0x85, 0x02, 0x85, 0x02, 0x85, 0x02, 0xa9, 0x00,
			0x85, 0x00, 0xca, 0x10, 0xe4, 0x06, 0x83, 0x66,
			0x84, 0x26, 0x85, 0xa5, 0x83, 0x85, 0x0d, 0xa5,
			0x84, 0x85, 0x0e, 0xa5, 0x85, 0x85, 0x0f, 0xa6,
			0x82, 0xca, 0x86, 0x82, 0x86, 0x17, 0xe0, 0x0a,
			0xd0, 0xc3, 0xa9, 0x02, 0x85, 0x01, 0xa2, 0x1c,
			0xa0, 0x00, 0x84, 0x19, 0x84, 0x09, 0x94, 0x81,
			0xca, 0x10, 0xfb, 0xa6, 0x80, 0xdd, 0x00, 0xf0,
			0xa9, 0x9a, 0xa2, 0xff, 0xa0, 0x00, 0x9a, 0x4c,
			0xfa, 0x00, 0xcd, 0xf8, 0xff, 0x4c
		};

		private readonly byte[] DefaultHeader = {
			0xac, 0xfa, 0x0f, 0x18, 0x62, 0x00, 0x24, 0x02,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0x00, 0x04, 0x08, 0x0c, 0x10, 0x14, 0x18, 0x1c,
			0x01, 0x05, 0x09, 0x0d, 0x11, 0x15, 0x19, 0x1d,
			0x02, 0x06, 0x0a, 0x0e, 0x12, 0x16, 0x1a, 0x1e,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
			0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00
		};

		#endregion

		public override bool HasCartRam
		{
			get { return true; }
		}

		public override ByteBuffer CartRam
		{
			get { return _superChargerImage; }
		}

		public override void HardReset()
		{
			_superChargerImage = new ByteBuffer(8192);
			_imageOffsets = new IntBuffer(2);
			_writePending = false;
			_distinctAccesses = 0;

			_writeEnabled = false;
			_dataHoldRegister = 0;
			_numberOfLoadImages = 0;
			_loadedImages = null;
			
			_header = new ByteBuffer(256);
			_powerIndicator = false;
			_powerRomCycle = 0;
			_size = 0;
			
			_elapsedCycles = 0;

			InitializeSettings();
			base.HardReset();
		}

		public override void Dispose()
		{
			_superChargerImage.Dispose();
			_imageOffsets.Dispose();
			_loadedImages.Dispose();
			base.Dispose();
		}

		public override void SyncState(Serializer ser)
		{
			ser.Sync("superChargerImage", ref _superChargerImage);
			ser.Sync("imageOffsets", ref _imageOffsets);
			ser.Sync("writePending", ref _writePending);
			ser.Sync("distinctAccesses", ref _distinctAccesses);

			ser.Sync("writeEnabled", ref _writeEnabled);
			ser.Sync("dataHoldRegister", ref _dataHoldRegister);
			ser.Sync("numberOfLoadImages", ref _numberOfLoadImages);
			ser.Sync("loadedImages", ref _loadedImages);

			ser.Sync("header", ref _header);
			ser.Sync("powerIndicator", ref _powerIndicator);
			ser.Sync("powerRomCycle", ref _powerRomCycle);
			ser.Sync("size", ref _size);

			ser.Sync("elapsedCycles", ref _elapsedCycles);

			base.SyncState(ser);
		}

		public override void ClockCpu()
		{
			_elapsedCycles++;
			
		}

		private byte ReadMem(ushort addr, bool peek)
		{
			if (addr < 0x1000) { if (peek) { return base.PeekMemory(addr); } else { return base.ReadMemory(addr); } }

			/*---------------------------*/

			if (addr == 0x1850 && _imageOffsets[1] == (3 << 11))
			{
				LoadIntoRam(Core.MemoryDomains["System Bus"].PeekByte(0x80)); // Get load that's being accessed (BIOS places load number at 0x80) // TODO: a better way to do this
				return _superChargerImage[(addr & 0x7FF) + _imageOffsets[1]];
			}

			if (_writePending && // Cancel any pending write if more than 5 distinct accesses have occurred // TODO: Modify to handle when the distinct counter wraps around...
				(Core.DistinctAccessCount >  _distinctAccesses + 5))
			{
				_writePending = false;
			}

			/*---------------------------*/

			if (!((addr & 0x0F00) > 0) && (!_writeEnabled || !_writePending))
			{
				_dataHoldRegister = (byte)addr;
				_distinctAccesses = Core.DistinctAccessCount;
				_writePending = true;
			}
			else if ((addr & 0x1FFF) == 0x1FF8) // Is the bank configuration hotspot being accessed?
			{
				_writePending = false;
				BankConfiguration(_dataHoldRegister);

			}
			else if (_writeEnabled && _writePending &&
					Core.DistinctAccessCount == (_distinctAccesses + 5))
			{
				if ((addr & 0x800) == 0)
				{
					_superChargerImage[(addr & 0x07FF) + _imageOffsets[0]] = _dataHoldRegister;
				}
				else if (_imageOffsets[1] != (3 << 11)) // Don't poke Rom
				{
					_superChargerImage[(addr & 0x07FF) + _imageOffsets[1]] = _dataHoldRegister;
				}

				_writePending = false;
			}

			/*---------------------------*/

			return _superChargerImage[(addr & 0x07FF) + _imageOffsets[((addr & 0x800) > 0) ? 1 : 0]];
		}

		public override byte ReadMemory(ushort addr)
		{
			return ReadMem(addr, false);
		}

		public override byte PeekMemory(ushort addr)
		{
			return ReadMem(addr, true);
		}

		private void WriteMem(ushort addr, byte value, bool poke)
		{
			if (addr < 0x1000)
			{
				base.WriteMemory(addr, value);
				return;
			}

			if (!poke && _writePending && (Core.DistinctAccessCount > _distinctAccesses + 5))
			{
				_writePending = false;
			}

			// Is the data hold register being set?
			if (!poke && !((addr & 0x0F00) > 0) && (!_writeEnabled || !_writePending))
			{
				_dataHoldRegister = (byte)addr;
				_distinctAccesses = Core.DistinctAccessCount;
				_writePending = true;
			}
			// Is the bank configuration hotspot being accessed?
			else if (!poke && (addr & 0x1FFF) == 0x1FF8)
			{
				// Yes, so handle bank configuration
				_writePending = false;
				BankConfiguration(_dataHoldRegister);
			}

			// Handle poke if writing enabled
			else if (_writeEnabled && _writePending &&
				(Core.DistinctAccessCount == (_distinctAccesses + 5)))
			{
				if ((addr & 0x0800) == 0)
				{
					_superChargerImage[(addr & 0x07FF) + _imageOffsets[0]] = _dataHoldRegister;
				}
				else if (_imageOffsets[1] != (3 << 11))    // Can't poke to ROM
				{
					_superChargerImage[(addr & 0x07FF) + _imageOffsets[1]] = _dataHoldRegister;
				}

				_writePending = false;
			}
		}

		public override void WriteMemory(ushort addr, byte value)
		{
			WriteMem(addr, value, poke: false);
		}

		public override void PokeMemory(ushort addr, byte value)
		{
			WriteMem(addr, value, poke: true);
		}

		private void InitializeRom()
		{
			/* scrom.asm data borrowed from Stella:
			// Note that the following offsets depend on the 'scrom.asm' file
			// in src/emucore/misc.  If that file is ever recompiled (and its
			// contents placed in the ourDummyROMCode array), the offsets will
			// almost definitely change
			*/

			// The scrom.asm code checks a value at offset 109 as follows:
			//   0xFF -> do a complete jump over the SC BIOS progress bars code
			//   0x00 -> show SC BIOS progress bars as normal
			DummyRomCode[109] = (byte)(Core.SyncSettings.FastScBios ? 0xFF : 0x00);

			// Stella does this, but randomness is bad for determinacy! Hopefully we don't really need it
			//ourDummyROMCode[281] = mySystem->randGenerator().next();

			// Initialize ROM with illegal 6502 opcode that causes a real 6502 to jam
			for (int i = 0; i < 2048; i++)
			{
				_superChargerImage[(3 << 11) + i] = 0x02;
			}

			// Copy the "dummy" Supercharger BIOS code into the ROM area
			for (int i = 0; i < DummyRomCode.Length; i++)
			{
				_superChargerImage[(3 << 11) + i] = DummyRomCode[i];
			}

			// Finally set 6502 vectors to point to initial load code at 0xF80A of BIOS
			_superChargerImage[(3 << 11) + 2044] = 0x0A;
			_superChargerImage[(3 << 11) + 2045] = 0xF8;
			_superChargerImage[(3 << 11) + 2046] = 0x0A;
			_superChargerImage[(3 << 11) + 2047] = 0xF8;
		}

		private void BankConfiguration(byte configuration)
		{
			// D7-D5 of this byte: Write Pulse Delay (n/a for emulator)
			//
			// D4-D0: RAM/ROM configuration:
			//       $F000-F7FF    $F800-FFFF Address range that banks map into
			//  000wp     2            ROM
			//  001wp     0            ROM
			//  010wp     2            0      as used in Commie Mutants and many others
			//  011wp     0            2      as used in Suicide Mission
			//  100wp     2            ROM
			//  101wp     1            ROM
			//  110wp     2            1      as used in Killer Satellites
			//  111wp     1            2      as we use for 2k/4k ROM cloning
			// 
			//  w = Write Enable (1 = enabled; accesses to $F000-$F0FF cause writes
			//    to happen.  0 = disabled, and the cart acts like ROM.)
			//  p = ROM Power (0 = enabled, 1 = off.)  Only power the ROM if you're
			//    wanting to access the ROM for multiloads.  Otherwise set to 1.

			//_bank2k = configuration & 0x1F;  // remember for the bank() method
			_powerIndicator = !((configuration & 0x01) > 0);
			if (_powerIndicator)
			{
				_powerRomCycle = (int)_elapsedCycles;
			}

			_writeEnabled = (configuration & 0x02) > 0;

			switch ((configuration >> 2) & 0x07)
			{
				case 0x00:
					_imageOffsets[0] = 2 << 11;
					_imageOffsets[1] = 3 << 11;
					break;
				case 0x01:
					_imageOffsets[0] = 0;
					_imageOffsets[1] = 3 << 11;
					break;
				case 0x02:
					_imageOffsets[0] = 2 << 11;
					_imageOffsets[1] = 0;
					break;
				case 0x03:
					_imageOffsets[0] = 0;
					_imageOffsets[1] = 2 << 11;
					break;
				case 0x04:
					_imageOffsets[0] = 2 << 11;
					_imageOffsets[1] = 3 << 11;
					break;
				case 0x05:
					_imageOffsets[0] = 1 << 11;
					_imageOffsets[1] = 3 << 11;
					break;
				case 0x06:
					_imageOffsets[0] = 2 << 11;
					_imageOffsets[1] = 1 << 11;
					break;
				case 0x07:
					_imageOffsets[0] = 1 << 11;
					_imageOffsets[1] = 2 << 11;
					break;
			}
		}

		private void LoadIntoRam(byte load)
		{
			ushort image;

			for (image = 0; image < _numberOfLoadImages; image++)
			{
				if (_loadedImages[(image * 8448) + 8192 + 5] == load)
				{
					for (int i = 0; i < 256; i++)
					{
						_header[i] = _loadedImages[(image * 8448) + 8192 + i];
					}

					if (Checksum(_header.Arr.Take(8).ToArray()) != 0x55)
					{
						Console.WriteLine("WARNING: The Supercharger header checksum is invalid...");
					}

					// TODO: verify the load's header

					// Load all of the pages from the load
					bool invalidPageChecksumSeen = false;
					for (int j = 0; j < _header[3]; j++)
					{
						int bank = _header[16 + j] & 0x03;
						int page = (_header[16 + j] >> 2) & 0x07;
						var src = _loadedImages.Arr.Skip((image * 8448) + (j * 256)).Take(256).ToArray();
						byte sum = (byte)(Checksum(src) + _header[16 + j] + _header[64 + j]);

						if (!invalidPageChecksumSeen && (sum != 0x55))
						{
							Console.WriteLine("WARNING: Some Supercharger page checksums are invalid...");
							invalidPageChecksumSeen = true;
						}

						if (bank < 3)
						{
							for (int k = 0; k < src.Length; k++)
							{
								_superChargerImage[(bank * 2048) + (page * 256) + k] = src[k];
							}
						}
					}

					// TODO: is this the correct Write to do?
					base.WriteMemory(0xFE, _header[0]);
					base.WriteMemory(0xFF, _header[1]);
					base.WriteMemory(0x80, _header[2]);
				}
			}
		}

		private byte Checksum(byte[] s)
		{
			byte sum = 0;

			for (int i = 0; i < s.Count(); i++)
			{
				sum += s[i];
			}

			return sum;
		}
	}
}
