using System;
using System.Linq;

using BizHawk.Common;
using BizHawk.Emulation.Common;
using BizHawk.Emulation.Cores.Components.M6502;

#pragma warning disable 162

namespace BizHawk.Emulation.Cores.Nintendo.NES
{
	public partial class NES : IEmulator, ICycleTiming
	{
		//hardware/state
		public MOS6502X<CpuLink> cpu;
		public PPU ppu;
		public APU apu;
		public byte[] ram;
		NESWatch[] sysbus_watch = new NESWatch[65536];
		public byte[] CIRAM; //AKA nametables
		string game_name = ""; //friendly name exposed to user and used as filename base
		CartInfo cart; //the current cart prototype. should be moved into the board, perhaps
		internal INESBoard Board; //the board hardware that is currently driving things
		EDetectionOrigin origin = EDetectionOrigin.None;
		int sprdma_countdown;

		public bool _irq_apu; //various irq signals that get merged to the cpu irq pin
		
		/// <summary>clock speed of the main cpu in hz</summary>
		public int cpuclockrate { get; private set; }

		//user configuration 
		int[] palette_compiled = new int[64 * 8];

		//variable set when VS system games are running
		internal bool _isVS = false;
		//some VS games have a ppu that switches 2000 and 2001, so keep trcak of that
		public byte _isVS2c05 = 0;
		//since prg reg for VS System is set in the controller regs, it is convenient to have it here
		//instead of in the board
		public byte VS_chr_reg;
		public byte VS_prg_reg;
		//various VS controls
		public byte[] VS_dips = new byte[8];
		public byte VS_service = 0;
		public byte VS_coin_inserted=0;
		public byte VS_ROM_control;

		// cheat addr index tracker
		// disables all cheats each frame
		public int[] cheat_indexes = new int[0x10000];
		public int num_cheats;

		// new input system
		NESControlSettings ControllerSettings; // this is stored internally so that a new change of settings won't replace
		IControllerDeck ControllerDeck;
		byte latched4016;

		private DisplayType _display_type = DisplayType.NTSC;

		//Sound config
		public void SetVol1(int v) { apu.m_vol = v; }

		/// <summary>
		/// for debugging only!
		/// </summary>
		/// <returns></returns>
		public INESBoard GetBoard()
		{
			return Board;
		}

		public void Dispose()
		{
			if (magicSoundProvider != null)
				magicSoundProvider.Dispose();
			magicSoundProvider = null;
		}

		class MagicSoundProvider : ISoundProvider, IDisposable
		{
			BlipBuffer blip;
			NES nes;

			const int blipbuffsize = 4096;

			public MagicSoundProvider(NES nes, uint infreq)
			{
				this.nes = nes;

				blip = new BlipBuffer(blipbuffsize);
				blip.SetRates(infreq, 44100);

				//var actualMetaspu = new Sound.MetaspuSoundProvider(Sound.ESynchMethod.ESynchMethod_V);
				//1.789773mhz NTSC
				//resampler = new Sound.Utilities.SpeexResampler(2, infreq, 44100 * APU.DECIMATIONFACTOR, infreq, 44100, actualMetaspu.buffer.enqueue_samples);
				//output = new Sound.Utilities.DCFilter(actualMetaspu);
			}

			public bool CanProvideAsync
			{
				get { return false; }
			}

			public SyncSoundMode SyncMode
			{
				get { return SyncSoundMode.Sync; }
			}

			public void SetSyncMode(SyncSoundMode mode)
			{
				if (mode != SyncSoundMode.Sync)
				{
					throw new NotSupportedException("Only sync mode is supported");
				}
			}

			public void GetSamplesAsync(short[] samples)
			{
				throw new NotSupportedException("Async not supported");
			}

			public void GetSamplesSync(out short[] samples, out int nsamp)
			{
				//Console.WriteLine("ASync: {0}", nes.apu.dlist.Count);
				foreach (var d in nes.apu.dlist)
					blip.AddDelta(d.time, d.value);
				nes.apu.dlist.Clear();
				blip.EndFrame(nes.apu.sampleclock);
				nes.apu.sampleclock = 0;

				nsamp = blip.SamplesAvailable();
				samples = new short[nsamp * 2];

				blip.ReadSamples(samples, nsamp, true);
				// duplicate to stereo
				for (int i = 0; i < nsamp * 2; i += 2)
					samples[i + 1] = samples[i];

				nes.Board.ApplyCustomAudio(samples);
			}

			public void DiscardSamples()
			{
				nes.apu.dlist.Clear();
				nes.apu.sampleclock = 0;
			}

			public void Dispose()
			{
				if (blip != null)
				{
					blip.Dispose();
					blip = null;
				}
			}
		}
		MagicSoundProvider magicSoundProvider;

		public void HardReset()
		{
			cpu = new MOS6502X<CpuLink>(new CpuLink(this))
			{
				BCD_Enabled = false
			};

			ppu = new PPU(this);
			ram = new byte[0x800];
			CIRAM = new byte[0x800];

			// wire controllers
			// todo: allow changing this
			ControllerDeck = ControllerSettings.Instantiate(ppu.LightGunCallback);
			// set controller definition first time only
			if (ControllerDefinition == null)
			{
				ControllerDefinition = new ControllerDefinition(ControllerDeck.GetDefinition());
				ControllerDefinition.Name = "NES Controller";
				// controls other than the deck
				ControllerDefinition.BoolButtons.Add("Power");
				ControllerDefinition.BoolButtons.Add("Reset");
				if (Board is FDS)
				{
					var b = Board as FDS;
					ControllerDefinition.BoolButtons.Add("FDS Eject");
					for (int i = 0; i < b.NumSides; i++)
						ControllerDefinition.BoolButtons.Add("FDS Insert " + i);
				}

				if (_isVS)
				{
					ControllerDefinition.BoolButtons.Add("Insert Coin P1");
					ControllerDefinition.BoolButtons.Add("Insert Coin P2");
					ControllerDefinition.BoolButtons.Add("Service Switch");
				}
			}

			// don't replace the magicSoundProvider on reset, as it's not needed
			// if (magicSoundProvider != null) magicSoundProvider.Dispose();

			// set up region
			switch (_display_type)
			{
				case Common.DisplayType.PAL:
					apu = new APU(this, apu, true);
					ppu.region = PPU.Region.PAL;
					VsyncNum = 50;
					VsyncDen = 1;
					cpuclockrate = 1662607;
					cpu_sequence = cpu_sequence_PAL;
					_display_type = DisplayType.PAL;
					ClockRate = 5320342.5;
					break;
				case Common.DisplayType.NTSC:
					apu = new APU(this, apu, false);
					ppu.region = PPU.Region.NTSC;
					VsyncNum = 39375000;
					VsyncDen = 655171;
					cpuclockrate = 1789773;
					cpu_sequence = cpu_sequence_NTSC;
					ClockRate = 5369318.1818181818181818181818182;
					break;
				// this is in bootgod, but not used at all
				case Common.DisplayType.Dendy:
					apu = new APU(this, apu, false);
					ppu.region = PPU.Region.Dendy;
					VsyncNum = 50;
					VsyncDen = 1;
					cpuclockrate = 1773448;
					cpu_sequence = cpu_sequence_NTSC;
					_display_type = DisplayType.Dendy;
					ClockRate = 5320342.5;
					break;
				default:
					throw new Exception("Unknown displaytype!");
			}
			if (magicSoundProvider == null)
				magicSoundProvider = new MagicSoundProvider(this, (uint)cpuclockrate);

			BoardSystemHardReset();

			// apu has some specific power up bahaviour that we will emulate here
			apu.NESHardReset();

			if (SyncSettings.InitialWRamStatePattern != null && SyncSettings.InitialWRamStatePattern.Any())
			{
				for (int i = 0; i < 0x800; i++)
				{
					ram[i] = SyncSettings.InitialWRamStatePattern[i % SyncSettings.InitialWRamStatePattern.Count];
				}
			}
			else
			{
				// check fceux's PowerNES and FCEU_MemoryRand function for more information:
				// relevant games: Cybernoid; Minna no Taabou no Nakayoshi Daisakusen; Huang Di; and maybe mechanized attack
				for (int i = 0; i < 0x800; i++)
				{
					if ((i & 4) != 0)
					{
						ram[i] = 0xFF;
					}
					else
					{
						ram[i] = 0x00;
					}
				}
			}

			SetupMemoryDomains();

			// some boards cannot have specific values in RAM upon initialization
			// Let's hard code those cases here
			// these will be defined through the gameDB exclusively for now.

			if (cart.DB_GameInfo!=null)
			{
				
				if (cart.DB_GameInfo.Hash == "60FC5FA5B5ACCAF3AEFEBA73FC8BFFD3C4DAE558" // Camerica Golden 5
					|| cart.DB_GameInfo.Hash == "BAD382331C30B22A908DA4BFF2759C25113CC26A" // Camerica Golden 5
					|| cart.DB_GameInfo.Hash == "40409FEC8249EFDB772E6FFB2DCD41860C6CCA23" // Camerica Pegasus 4-in-1
					)
				{
					ram[0x701] = 0xFF;
				}
				
				if (cart.DB_GameInfo.Hash == "68ABE1E49C9E9CCEA978A48232432C252E5912C0") // Dancing Blocks
				{
					ram[0xEC] = 0;
					ram[0xED] = 0;
				}

				if (cart.DB_GameInfo.Hash == "00C50062A2DECE99580063777590F26A253AAB6B") // Silva Saga
				{
					for (int i = 0; i < Board.WRAM.Length; i++)
					{
						Board.WRAM[i] = 0xFF;
					}

				}

				
			}

		}

		public long CycleCount => ppu.TotalCycles;
		public double ClockRate { get; private set; }

		private int VsyncNum { get; set; }
		private int VsyncDen { get; set; }

		private IController _controller = NullController.Instance;

		bool resetSignal;
		bool hardResetSignal;
		public void FrameAdvance(IController controller, bool render, bool rendersound)
		{
			_controller = controller;

			if (Tracer.Enabled)
				cpu.TraceCallback = (s) => Tracer.Put(s);
			else
				cpu.TraceCallback = null;

			lagged = true;
			if (resetSignal)
			{
				Board.NESSoftReset();
				cpu.NESSoftReset();
				apu.NESSoftReset();
				ppu.NESSoftReset();
			}
			else if (hardResetSignal)
			{
				HardReset();
			}

			Frame++;

			//if (resetSignal)
			//Controller.UnpressButton("Reset");   TODO fix this
			resetSignal = controller.IsPressed("Reset");
			hardResetSignal = controller.IsPressed("Power");

			if (Board is FDS)
			{
				var b = Board as FDS;
				if (controller.IsPressed("FDS Eject"))
					b.Eject();
				for (int i = 0; i < b.NumSides; i++)
					if (controller.IsPressed("FDS Insert " + i))
						b.InsertSide(i);
			}

			if (_isVS)
			{
				if (controller.IsPressed("Service Switch"))
					VS_service = 1;
				else
					VS_service = 0;

				if (controller.IsPressed("Insert Coin P1"))
					VS_coin_inserted |= 1;
				else
					VS_coin_inserted &= 2;

				if (controller.IsPressed("Insert Coin P2"))
					VS_coin_inserted |= 2;
				else
					VS_coin_inserted &= 1;
			}

			if (ppu.ppudead > 0)
			{
				while (ppu.ppudead > 0)
				{
					ppu.NewDeadPPU();
				}				
			}
			else
			{
				ppu.ppu_init_frame();

				ppu.do_vbl = true;
				ppu.do_active_sl = true;
				ppu.do_pre_vbl = true;

				// do the vbl ticks seperate, that will save us a few checks that don't happen in active region
				while (ppu.do_vbl)
				{
					ppu.TickPPU_VBL();
				}

				// now do the rest of the frame
				while (ppu.do_active_sl)
				{
					ppu.TickPPU_active();
				}

				// now do the pre-NMI lines
				while (ppu.do_pre_vbl)
				{
					ppu.TickPPU_preVBL();
				}
			}
			
			if (lagged)
			{
				_lagcount++;
				islag = true;
			}
			else
				islag = false;

			videoProvider.FillFrameBuffer();

			//turn off all cheats
			for (int d=0;d<num_cheats;d++)
			{
				RemoveGameGenie(cheat_indexes[d]);
			}
			num_cheats = 0;
		}

		//PAL:
		//sequence of ppu clocks per cpu clock: 3,3,3,3,4
		//at least it should be, but something is off with that (start up time?) so it is 3,3,3,4,3 for now
		//NTSC:
		//sequence of ppu clocks per cpu clock: 3
		public ByteBuffer cpu_sequence;
		static ByteBuffer cpu_sequence_NTSC = new ByteBuffer(new byte[] { 3, 3, 3, 3, 3 });
		static ByteBuffer cpu_sequence_PAL = new ByteBuffer(new byte[] { 3, 3, 3, 4, 3 });
		public int cpu_deadcounter;

		public int oam_dma_index;
		public bool oam_dma_exec = false;
		public ushort oam_dma_addr;
		public byte oam_dma_byte;
		public bool dmc_dma_exec = false;
		public bool dmc_realign;
		public bool IRQ_delay;
		public bool special_case_delay; // very ugly but the only option
		public bool do_the_reread;
		public byte DB; //old data bus values from previous reads

		internal void RunCpuOne()
		{
			///////////////////////////
			// OAM DMA start
			///////////////////////////

			if (sprdma_countdown > 0)
			{
				sprdma_countdown--;
				if (sprdma_countdown == 0)
				{
					if (cpu.TotalExecutedCycles % 2 == 0)
					{
						cpu_deadcounter = 2;
					}
					else
					{
						cpu_deadcounter = 1;
					}
					oam_dma_exec = true;
					cpu.RDY = false;
					oam_dma_index = 0;
					special_case_delay = true;
				}
			}

			if (oam_dma_exec && apu.dmc_dma_countdown != 1 && !dmc_realign)
			{
				if (cpu_deadcounter == 0)
				{

					if (oam_dma_index % 2 == 0)
					{
						oam_dma_byte = ReadMemory(oam_dma_addr);
						oam_dma_addr++;
					}
					else
					{
						WriteMemory(0x2004, oam_dma_byte);
					}
					oam_dma_index++;
					if (oam_dma_index == 512) oam_dma_exec = false;

				}
				else
				{
					cpu_deadcounter--;
				}
			}
			else if (apu.dmc_dma_countdown == 1)
			{
				dmc_realign = true;
			}
			else if (dmc_realign)
			{
				dmc_realign = false;
			}
			/////////////////////////////
			// OAM DMA end
			/////////////////////////////


			/////////////////////////////
			// dmc dma start
			/////////////////////////////

			if (apu.dmc_dma_countdown > 0)
			{
				cpu.RDY = false;
				dmc_dma_exec = true;
				apu.dmc_dma_countdown--;
				if (apu.dmc_dma_countdown == 0)
				{
					apu.RunDMCFetch();
					dmc_dma_exec = false;
					apu.dmc_dma_countdown = -1;
					do_the_reread = true;
				}
			}

			/////////////////////////////
			// dmc dma end
			/////////////////////////////
			apu.RunOne(true);

			if (cpu.RDY && !IRQ_delay)
			{
				cpu.IRQ = _irq_apu || Board.IRQSignal;
			}
			else if (special_case_delay || apu.dmc_dma_countdown == 3)
			{
				cpu.IRQ = _irq_apu || Board.IRQSignal;
				special_case_delay = false;
			}

			cpu.ExecuteOne();
			apu.RunOne(false);

			if (ppu.double_2007_read > 0)
				ppu.double_2007_read--;

			if (do_the_reread && cpu.RDY)
				do_the_reread = false;

			if (IRQ_delay)
				IRQ_delay = false;

			if (!dmc_dma_exec && !oam_dma_exec && !cpu.RDY)
			{
				cpu.RDY = true;
				IRQ_delay = true;
			}
			
			Board.ClockCPU();
		}

		public byte ReadReg(int addr)
		{
			byte ret_spec;
			switch (addr)
			{
				case 0x4000:
				case 0x4001:
				case 0x4002:
				case 0x4003:
				case 0x4004:
				case 0x4005:
				case 0x4006:
				case 0x4007:
				case 0x4008:
				case 0x4009:
				case 0x400A:
				case 0x400B:
				case 0x400C:
				case 0x400D:
				case 0x400E:
				case 0x400F:
				case 0x4010:
				case 0x4011:
				case 0x4012:
				case 0x4013:
					return DB;
				//return apu.ReadReg(addr);
				case 0x4014: /*OAM DMA*/ break;
				case 0x4015: return (byte)((byte)(apu.ReadReg(addr) & 0xDF) + (byte)(DB & 0x20));
				case 0x4016:
					if (_isVS)
					{
						byte ret = 0;
						ret = read_joyport(0x4016);
						ret &= 1;
						ret = (byte)(ret | (VS_service << 2) | (VS_dips[0] << 3) | (VS_dips[1] << 4) | (VS_coin_inserted << 5) | (VS_ROM_control<<7));

						return ret;
					}
					else
					{
						// special hardware glitch case
						ret_spec = read_joyport(addr);
						if (do_the_reread && ppu.region==PPU.Region.NTSC)
						{
							ret_spec = read_joyport(addr);
							do_the_reread = false;
						}
						return ret_spec;

					}
				case 0x4017:
					if (_isVS)
					{
						byte ret = 0;
						ret = read_joyport(0x4017);
						ret &= 1;

						ret = (byte)(ret | (VS_dips[2] << 2) | (VS_dips[3] << 3) | (VS_dips[4] << 4) | (VS_dips[5] << 5) | (VS_dips[6] << 6) | (VS_dips[7] << 7));

						return ret;

					}
					else
					{
						// special hardware glitch case
						ret_spec = read_joyport(addr);
						if (do_the_reread && ppu.region == PPU.Region.NTSC)
						{
							ret_spec = read_joyport(addr);
							do_the_reread = false;
						}
						return ret_spec;
					}
				default:
					//Console.WriteLine("read register: {0:x4}", addr);
					break;

			}
			return DB;
		}

		public byte PeekReg(int addr)
		{
			switch (addr)
			{
				case 0x4000:
				case 0x4001:
				case 0x4002:
				case 0x4003:
				case 0x4004:
				case 0x4005:
				case 0x4006:
				case 0x4007:
				case 0x4008:
				case 0x4009:
				case 0x400A:
				case 0x400B:
				case 0x400C:
				case 0x400D:
				case 0x400E:
				case 0x400F:
				case 0x4010:
				case 0x4011:
				case 0x4012:
				case 0x4013:
					return apu.PeekReg(addr);
				case 0x4014: /*OAM DMA*/ break;
				case 0x4015: return apu.PeekReg(addr);
				case 0x4016:
				case 0x4017:
					return peek_joyport(addr);
				default:
					//Console.WriteLine("read register: {0:x4}", addr);
					break;

			}
			return 0xFF;
		}

		void WriteReg(int addr, byte val)
		{
			switch (addr)
			{
				case 0x4000:
				case 0x4001:
				case 0x4002:
				case 0x4003:
				case 0x4004:
				case 0x4005:
				case 0x4006:
				case 0x4007:
				case 0x4008:
				case 0x4009:
				case 0x400A:
				case 0x400B:
				case 0x400C:
				case 0x400D:
				case 0x400E:
				case 0x400F:
				case 0x4010:
				case 0x4011:
				case 0x4012:
				case 0x4013:
					apu.WriteReg(addr, val);
					break;
				case 0x4014:
					//schedule a sprite dma event for beginning 1 cycle in the future.
					//this receives 2 because thats just the way it works out.
					oam_dma_addr = (ushort)(val << 8);
					sprdma_countdown = 1;
					break;
				case 0x4015: apu.WriteReg(addr, val); break;
				case 0x4016:
					if (_isVS)
					{
						write_joyport(val);
						VS_chr_reg = (byte)((val & 0x4)>>2);

						//TODO: does other stuff for dual system

						//this is actually different then assignment
						VS_prg_reg = (byte)((val & 0x4)>>2);

					}
					else
					{
						write_joyport(val);
					}
					break;
				case 0x4017: apu.WriteReg(addr, val); break;
				default:
					//Console.WriteLine("wrote register: {0:x4} = {1:x2}", addr, val);
					break;
			}
		}

		void write_joyport(byte value)
		{
			var si = new StrobeInfo(latched4016, value);
			ControllerDeck.Strobe(si, _controller);
			latched4016 = value;
		}

		byte read_joyport(int addr)
		{
			InputCallbacks.Call();
			lagged = false;
			byte ret = 0;
			if (_isVS)
			{
				// for whatever reason, in VS left and right controller have swapped regs
				ret = addr == 0x4017 ? ControllerDeck.ReadA(_controller) : ControllerDeck.ReadB(_controller);
			}
			else
			{
				ret = addr == 0x4016 ? ControllerDeck.ReadA(_controller) : ControllerDeck.ReadB(_controller);
			}
			
			ret &= 0x1f;
			ret |= (byte)(0xe0 & DB);
			return ret;
		}

		byte peek_joyport(int addr)
		{
			// at the moment, the new system doesn't support peeks
			return 0;
		}

		/// <summary>
		/// Sets the provided palette as current.
		/// Applies the current deemph settings if needed to expand a 64-entry palette to 512
		/// </summary>
		public void SetPalette(byte[,] pal)
		{
			int nColors = pal.GetLength(0);
			int nElems = pal.GetLength(1);

			if (nColors == 512)
			{
				//just copy the palette directly
				for (int c = 0; c < 64 * 8; c++)
				{
					int r = pal[c, 0];
					int g = pal[c, 1];
					int b = pal[c, 2];
					palette_compiled[c] = (int)unchecked((int)0xFF000000 | (r << 16) | (g << 8) | b);
				}
			}
			else
			{
				//expand using deemph
				for (int i = 0; i < 64 * 8; i++)
				{
					int d = i >> 6;
					int c = i & 63;
					int r = pal[c, 0];
					int g = pal[c, 1];
					int b = pal[c, 2];
					Palettes.ApplyDeemphasis(ref r, ref g, ref b, d);
					palette_compiled[i] = (int)unchecked((int)0xFF000000 | (r << 16) | (g << 8) | b);
				}
			}
		}

		/// <summary>
		/// looks up an internal NES pixel value to an rgb int (applying the core's current palette and assuming no deemph)
		/// </summary>
		public int LookupColor(int pixel)
		{
			return palette_compiled[pixel];
		}

		public byte DummyReadMemory(ushort addr) { return 0; }

		private void ApplySystemBusPoke(int addr, byte value)
		{
			if (addr < 0x2000)
			{
				ram[(addr & 0x7FF)] = value;
			}
			else if (addr < 0x4000)
			{
				ppu.WriteReg(addr, value);
			}
			else if (addr < 0x4020)
			{
				WriteReg(addr, value);
			}
			else
			{
				ApplyGameGenie(addr, value, null); //Apply a cheat to the remaining regions since they have no direct access, this may not be the best way to handle this situation
			}
		}

		public byte PeekMemory(ushort addr)
		{
			byte ret;

			if (addr >= 0x4020)
			{
				ret = Board.PeekCart(addr); //easy optimization, since rom reads are so common, move this up (reordering the rest of these elseifs is not easy)
			}
			else if (addr < 0x0800)
			{
				ret = ram[addr];
			}
			else if (addr < 0x2000)
			{
				ret = ram[addr & 0x7FF];
			}
			else if (addr < 0x4000)
			{
				ret = Board.PeekReg2xxx(addr);
			}
			else if (addr < 0x4020)
			{
				ret = PeekReg(addr); //we're not rebasing the register just to keep register names canonical
			}
			else
			{
				throw new Exception("Woopsie-doodle!");
				ret = 0xFF;
			}

			return ret;
		}

		public void ExecFetch(ushort addr)
		{
			MemoryCallbacks.CallExecutes(addr, "System Bus");
		}

		public byte ReadMemory(ushort addr)
		{
			byte ret;

			if (addr >= 0x8000)
			{
				ret = Board.ReadPRG(addr - 0x8000); //easy optimization, since rom reads are so common, move this up (reordering the rest of these elseifs is not easy)
			}
			else if (addr < 0x0800)
			{
				ret = ram[addr];
			}
			else if (addr < 0x2000)
			{
				ret = ram[addr & 0x7FF];
			}
			else if (addr < 0x4000)
			{
				ret = Board.ReadReg2xxx(addr);
			}
			else if (addr < 0x4020)
			{
				ret = ReadReg(addr); //we're not rebasing the register just to keep register names canonical			
			}
			else if (addr < 0x6000)
			{
				ret = Board.ReadEXP(addr - 0x4000);
			}
			else
			{
				ret = Board.ReadWRAM(addr - 0x6000);
			}

			//handle breakpoints and stuff.
			//the idea is that each core can implement its own watch class on an address which will track all the different kinds of monitors and breakpoints and etc.
			//but since freeze is a common case, it was implemented through its own mechanisms
			if (sysbus_watch[addr] != null)
			{
				sysbus_watch[addr].Sync();
				ret = sysbus_watch[addr].ApplyGameGenie(ret);
			}

			MemoryCallbacks.CallReads(addr, "System Bus");

			DB = ret;
			return ret;
		}

		public void ApplyGameGenie(int addr, byte value, byte? compare)
		{
			if (addr < sysbus_watch.Length)
			{
				cheat_indexes[num_cheats] = addr;
				num_cheats++;
				GetWatch(NESWatch.EDomain.Sysbus, addr).SetGameGenie(compare, value);
			}
		}

		public void RemoveGameGenie(int addr)
		{
			if (addr < sysbus_watch.Length)
			{
				GetWatch(NESWatch.EDomain.Sysbus, addr).RemoveGameGenie();
			}
		}

		public void WriteMemory(ushort addr, byte value)
		{
			if (addr < 0x0800)
			{
				ram[addr] = value;
			}
			else if (addr < 0x2000)
			{
				ram[addr & 0x7FF] = value;
			}
			else if (addr < 0x4000)
			{
				Board.WriteReg2xxx(addr, value);
			}
			else if (addr < 0x4020)
			{
				WriteReg(addr, value);  //we're not rebasing the register just to keep register names canonical
			}
			else if (addr < 0x6000)
			{
				Board.WriteEXP(addr - 0x4000, value);
			}
			else if (addr < 0x8000)
			{
				Board.WriteWRAM(addr - 0x6000, value);
			}
			else
			{
				Board.WritePRG(addr - 0x8000, value);
			}

			MemoryCallbacks.CallWrites(addr, "System Bus");
		}

		// the palette for each VS game needs to be chosen explicitly since there are 6 different ones.
		public void PickVSPalette(CartInfo cart)
		{
			switch (cart.palette)
			{
				case "2C05": SetPalette(Palettes.palette_2c03_2c05); ppu.CurrentLuma = PPU.PaletteLuma2C03; break;
				case "2C04-1": SetPalette(Palettes.palette_2c04_001); ppu.CurrentLuma = PPU.PaletteLuma2C04_1; break;
				case "2C04-2": SetPalette(Palettes.palette_2c04_002); ppu.CurrentLuma = PPU.PaletteLuma2C04_2; break;
				case "2C04-3": SetPalette(Palettes.palette_2c04_003); ppu.CurrentLuma = PPU.PaletteLuma2C04_3; break;
				case "2C04-4": SetPalette(Palettes.palette_2c04_004); ppu.CurrentLuma = PPU.PaletteLuma2C04_4; break;
			}

			//since this will run for every VS game, let's get security setting too
			//values below 16 are for the 2c05 PPU
			//values 16,32,48 are for Namco games and dealt with in mapper 206
			_isVS2c05 = (byte)(cart.vs_security & 15);
		}

	}
}
