using System;
using System.Linq;
using BizHawk.Common;

namespace BizHawk.Emulation.Cores.Atari.Atari2600
{
	/**
	Cartridge class used for DPC+.  There are six 4K program banks, a 4K
	display bank, 1K frequency table and the DPC chip.  For complete details on
	the DPC chip see David P. Crane's United States Patent Number 4,644,495.
	*/
	internal class mDPCPlus : MapperBase
	{
		// TODO: PokeMem, and everything else
		public mDPCPlus()
		{
			throw new NotImplementedException();
		}

		private IntBuffer _counters = new IntBuffer(8);
		private ByteBuffer _tops = new ByteBuffer(8);
		private ByteBuffer _flags = new ByteBuffer(8);
		private ByteBuffer _bottoms = new ByteBuffer(8);
		private bool[] _musicModes = new bool[3];

		private int _bank4K;
		private byte _currentRandomVal;
		private int _elapsedCycles = 85; // 85 compensates for a slight timing issue when ClockCpu is first run, 85 puts BizHawk back on track with Stella on elapsed timing values
		private float _fractionalClocks; // Fractional DPC music OSC clocks unused during the last update

		private byte[] _dspData;
		public byte[] DspData
		{
			get
			{
				return _dspData ?? (_dspData = Core.Rom.Skip(8192).Take(2048).ToArray());
			}
		}

		// Table for computing the input bit of the random number generator's
		// shift register (it's the NOT of the EOR of four bits)
		private readonly byte[] _randomInputBits = { 1, 0, 0, 1, 0, 1, 1, 0, 0, 1, 1, 0, 1, 0, 0, 1 };

		public override void Dispose()
		{
			base.Dispose();
			_counters.Dispose();
			_tops.Dispose();
			_flags.Dispose();
			_bottoms.Dispose();
		}

		public override void SyncState(Serializer ser)
		{
			base.SyncState(ser);

			ser.Sync("counters", ref _counters);
			ser.Sync("tops", ref _tops);
			ser.Sync("flags", ref _flags);
			ser.Sync("bottoms", ref _bottoms);
			ser.Sync("musicMode0", ref _musicModes[0]); // Silly, but I didn't want to support bool[] in Serializer just for this one variable
			ser.Sync("musicMode1", ref _musicModes[1]);
			ser.Sync("musicMode2", ref _musicModes[2]);

			ser.Sync("bank_4k", ref _bank4K);
			ser.Sync("currentRandomVal", ref _currentRandomVal);
			ser.Sync("elapsedCycles", ref _elapsedCycles);
			ser.Sync("fractionalClocks", ref _fractionalClocks);
		}

		public override void HardReset()
		{
			_counters = new IntBuffer(8);
			_tops = new ByteBuffer(8);
			_flags = new ByteBuffer(8);
			_bottoms = new ByteBuffer(8);
			_musicModes = new bool[3];
			_bank4K = 0;
			_currentRandomVal = 0;
			_elapsedCycles = 85;
			_fractionalClocks = 0;

			base.HardReset();
		}

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

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

			if (!peek)
			{
				Address(addr);
				ClockRandomNumberGenerator();
			}

			if (addr < 0x1040)
			{
				byte result;

				// Get the index of the data fetcher that's being accessed
				var index = addr & 0x07;
				var function = (addr >> 3) & 0x07;

				// Update flag register for selected data fetcher
				if ((_counters[index] & 0x00ff) == _tops[index])
				{
					_flags[index] = 0xff;
				}
				else if ((_counters[index] & 0x00ff) == _bottoms[index])
				{
					_flags[index] = 0x00;
				}

				switch (function)
				{
					case 0x00:
						if (index < 4)
						{
							result = _currentRandomVal;
						}
						else // No, it's a music read
						{
							var musicAmplitudes = new byte[] {
							  0x00, 0x04, 0x05, 0x09, 0x06, 0x0a, 0x0b, 0x0f
							};

							// Update the music data fetchers (counter & flag)
							UpdateMusicModeDataFetchers();

							byte i = 0;
							if (_musicModes[0] && _flags[5] > 0)
							{
								i |= 0x01;
							}

							if (_musicModes[1] && _flags[6] > 0)
							{
								i |= 0x02;
							}

							if (_musicModes[2] && _flags[7] > 0)
							{
								i |= 0x04;
							}

							result = musicAmplitudes[i];
						}

						break;

					// DFx display data read
					case 0x01:
						result = DspData[2047 - _counters[index]];
						break;

					// DFx display data read AND'd w/flag
					case 0x02:
						result = (byte)(DspData[2047 - _counters[index]] & _flags[index]);
						break;

					// DFx flag
					case 0x07:
						result = _flags[index];
						break;

					default:
						result = 0;
						break;
				}

				// Clock the selected data fetcher's counter if needed
				if ((index < 5) || ((index >= 5) && (!_musicModes[index - 5])))
				{
					_counters[index] = (_counters[index] - 1) & 0x07ff;
				}

				return result;
			}

			return Core.Rom[(_bank4K << 12) + (addr & 0xFFF)];
		}

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

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

		public override void WriteMemory(ushort addr, byte value)
		{
			if (addr < 0x1000)
			{
				base.WriteMemory(addr, value);
				return;
			}

			Address(addr);
			ClockRandomNumberGenerator();

			if (addr >= 0x1040 && addr < 0x1080)
			{
				var index = addr & 0x07;
				var function = (addr >> 3) & 0x07;

				switch (function)
				{
					// DFx top count
					case 0x00:
						_tops[index] = value;
						_flags[index] = 0x00;
						break;

					// DFx bottom count
					case 0x01:
						_bottoms[index] = value;
						break;

					// DFx counter low
					case 0x02:
						if ((index >= 5) && _musicModes[index - 5])
						{
							// Data fetcher is in music mode so its low counter value
							// should be loaded from the top register not the poked value
							_counters[index] = (_counters[index] & 0x0700) |
								_tops[index];
						}
						else
						{
							// Data fetcher is either not a music mode data fetcher or it
							// isn't in music mode so it's low counter value should be loaded
							// with the poked value
							_counters[index] = (_counters[index] & 0x0700) | value;
						}

						break;

					// DFx counter high
					case 0x03:
						_counters[index] = (ushort)(((value & 0x07) << 8) |
							(_counters[index] & 0x00ff));

						// Execute special code for music mode data fetchers
						if (index >= 5)
						{
							_musicModes[index - 5] = (value & 0x10) > 0;

							// NOTE: We are not handling the clock source input for
							// the music mode data fetchers.  We're going to assume
							// they always use the OSC input.
						}

						break;

					// Random Number Generator Reset
					case 0x06:
						_currentRandomVal = 1;
						break;
				}
			}
		}

		private void Address(ushort addr)
		{
			if (addr == 0x1FF6)
			{
				_bank4K = 0;
			}
			else if (addr == 0x1FF7)
			{
				_bank4K = 1;
			}
			else if (addr == 0x1FF8)
			{
				_bank4K = 2;
			}
			else if (addr == 0x1FF9)
			{
				_bank4K = 3;
			}
			else if (addr == 0x1FFA)
			{
				_bank4K = 4;
			}
			else if (addr == 0x1FFB)
			{
				_bank4K = 5;
			}
		}

		private void ClockRandomNumberGenerator()
		{
			// Using bits 7, 5, 4, & 3 of the shift register compute the input
			// bit for the shift register
			var bit = _randomInputBits[((_currentRandomVal >> 3) & 0x07) |
				(((_currentRandomVal & 0x80) > 0) ? 0x08 : 0x00)];

			// Update the shift register 
			_currentRandomVal = (byte)((_currentRandomVal << 1) | bit);
		}

		private void UpdateMusicModeDataFetchers()
		{
			// Calculate the number of cycles since the last update
			var cycles = _elapsedCycles;
			_elapsedCycles = 0;

			// Calculate the number of DPC OSC clocks since the last update
			var clocks = ((20000.0 * cycles) / 1193191.66666667) + _fractionalClocks;
			var wholeClocks = (int)clocks;
			_fractionalClocks = (float)(clocks - wholeClocks);

			if (wholeClocks <= 0)
			{
				return;
			}

			// Let's update counters and flags of the music mode data fetchers
			for (var x = 5; x <= 7; ++x)
			{
				// Update only if the data fetcher is in music mode
				if (_musicModes[x - 5])
				{
					var top = _tops[x] + 1;
					var newLow = _counters[x] & 0x00ff;

					if (_tops[x] != 0)
					{
						newLow -= wholeClocks % top;
						if (newLow < 0)
						{
							newLow += top;
						}
					}
					else
					{
						newLow = 0;
					}

					// Update flag register for this data fetcher
					if (newLow <= _bottoms[x])
					{
						_flags[x] = 0x00;
					}
					else if (newLow <= _tops[x])
					{
						_flags[x] = 0xff;
					}

					_counters[x] = (_counters[x] & 0x0700) | (ushort)newLow;
				}
			}
		}
	}
}
