/*****************************************************************************\
  Snes9x - Portable Super Nintendo Entertainment System (TM) emulator.
                This file is licensed under the Snes9x License.
   For further information, consult the LICENSE file in the root directory.
\*****************************************************************************/

#pragma once

inline constexpr int32_t FIRST_VISIBLE_LINE = 1;

enum
{
	TILE_2BIT,
	TILE_4BIT,
	TILE_8BIT,
	TILE_2BIT_EVEN,
	TILE_2BIT_ODD,
	TILE_4BIT_EVEN,
	TILE_4BIT_ODD
};

inline constexpr size_t MAX_2BIT_TILES = 4096;
inline constexpr size_t MAX_4BIT_TILES = 2048;
inline constexpr size_t MAX_8BIT_TILES = 1024;

struct InternalPPU
{
	bool Interlace;
	uint32_t Red[256];
	uint32_t Green[256];
	uint32_t Blue[256];
};

struct SPPU
{
	struct
	{
		bool High;
		uint8_t Increment;
		uint16_t Address;
		uint16_t Mask1;
		uint16_t FullGraphicCount;
		uint16_t Shift;
	} VMA;

	uint32_t WRAM;

	bool CGFLIPRead;
	uint8_t CGADD;
	uint16_t CGDATA[256];

	uint16_t OAMAddr;
	uint16_t SavedOAMAddr;
	bool OAMPriorityRotation;
	uint8_t OAMFlip;
	uint16_t OAMWriteRegister;
	uint8_t OAMData[512 + 32];

	uint8_t FirstSprite;

	bool HTimerEnabled;
	bool VTimerEnabled;
	short HTimerPosition;
	short VTimerPosition;
	uint16_t IRQHBeamPos;
	uint16_t IRQVBeamPos;

	bool HBeamFlip;
	bool VBeamFlip;

	short MatrixA;
	short MatrixB;

	bool ForcedBlanking;

	uint16_t ScreenHeight;

	bool Need16x8Mulitply;
	uint8_t M7byte;

	uint8_t HDMA;
	uint8_t HDMAEnded;

	uint8_t OpenBus1;
	uint8_t OpenBus2;

	uint16_t VRAMReadBuffer;
};

extern uint16_t SignExtend[2];
extern SPPU PPU;
extern InternalPPU IPPU;

void S9xResetPPU();
void S9xSoftResetPPU();
void S9xSetPPU(uint8_t, uint16_t);
uint8_t S9xGetPPU(uint16_t);
void S9xSetCPU(uint8_t, uint16_t);
uint8_t S9xGetCPU(uint16_t);
void S9xUpdateIRQPositions(bool);

#include "memmap.h"

struct SnesModel
{
	uint8_t _5C77;
	uint8_t _5C78;
	uint8_t _5A22;
};

extern SnesModel *Model;
extern SnesModel M1SNES;

inline void S9xUpdateVRAMReadBuffer()
{
	if (PPU.VMA.FullGraphicCount)
	{
		uint32_t addr = PPU.VMA.Address;
		uint32_t rem = addr & PPU.VMA.Mask1;
		uint32_t address = (addr & ~PPU.VMA.Mask1) + (rem >> PPU.VMA.Shift) + ((rem & (PPU.VMA.FullGraphicCount - 1)) << 3);
		PPU.VRAMReadBuffer = READ_WORD(&Memory.VRAM[(address << 1) & 0xffff]);
	}
	else
		PPU.VRAMReadBuffer = READ_WORD(&Memory.VRAM[(PPU.VMA.Address << 1) & 0xffff]);
}

inline void REGISTER_2104(uint8_t Byt)
{
	if (!(PPU.OAMFlip & 1))
	{
		PPU.OAMWriteRegister &= 0xff00;
		PPU.OAMWriteRegister |= Byt;
	}

	if (PPU.OAMAddr & 0x100)
	{
		int addr = ((PPU.OAMAddr & 0x10f) << 1) + (PPU.OAMFlip & 1);
		if (Byt != PPU.OAMData[addr])
			PPU.OAMData[addr] = Byt;
	}
	else if (PPU.OAMFlip & 1)
	{
		PPU.OAMWriteRegister &= 0x00ff;
		uint8_t lowbyte = static_cast<uint8_t>(PPU.OAMWriteRegister);
		uint8_t highbyte = Byt;
		PPU.OAMWriteRegister |= Byt << 8;

		int addr = PPU.OAMAddr << 1;
		if (lowbyte != PPU.OAMData[addr] || highbyte != PPU.OAMData[addr + 1])
		{
			PPU.OAMData[addr] = lowbyte;
			PPU.OAMData[addr + 1] = highbyte;
		}
	}

	PPU.OAMFlip ^= 1;
	if (!(PPU.OAMFlip & 1))
	{
		++PPU.OAMAddr;
		PPU.OAMAddr &= 0x1ff;
		if (PPU.OAMPriorityRotation && PPU.FirstSprite != (PPU.OAMAddr >> 1))
			PPU.FirstSprite = (PPU.OAMAddr & 0xfe) >> 1;
	}
}

// This code is correct, however due to Snes9x's inaccurate timings, some games might be broken by this chage. :(
inline bool CHECK_INBLANK()
{
	if (Settings.BlockInvalidVRAMAccess && !PPU.ForcedBlanking && CPU.V_Counter < PPU.ScreenHeight + FIRST_VISIBLE_LINE)
	{
		PPU.VMA.Address += !PPU.VMA.High ? PPU.VMA.Increment : 0;
		return true;
	}
	else
		return false;
}

inline void REGISTER_2118(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t address;

	if (PPU.VMA.FullGraphicCount)
	{
		uint32_t rem = PPU.VMA.Address & PPU.VMA.Mask1;
		address = (((PPU.VMA.Address & ~PPU.VMA.Mask1) + (rem >> PPU.VMA.Shift) + ((rem & (PPU.VMA.FullGraphicCount - 1)) << 3)) << 1) & 0xffff;
		Memory.VRAM[address] = Byt;
	}
	else
		Memory.VRAM[address = (PPU.VMA.Address << 1) & 0xffff] = Byt;

	if (!PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2118_tile(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t rem = PPU.VMA.Address & PPU.VMA.Mask1;
	uint32_t address = (((PPU.VMA.Address & ~PPU.VMA.Mask1) + (rem >> PPU.VMA.Shift) + ((rem & (PPU.VMA.FullGraphicCount - 1)) << 3)) << 1) & 0xffff;

	Memory.VRAM[address] = Byt;

	if (!PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2118_linear(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t address;

	Memory.VRAM[address = (PPU.VMA.Address << 1) & 0xffff] = Byt;

	if (!PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2119(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t address;

	if (PPU.VMA.FullGraphicCount)
	{
		uint32_t rem = PPU.VMA.Address & PPU.VMA.Mask1;
		address = ((((PPU.VMA.Address & ~PPU.VMA.Mask1) + (rem >> PPU.VMA.Shift) + ((rem & (PPU.VMA.FullGraphicCount - 1)) << 3)) << 1) + 1) & 0xffff;
		Memory.VRAM[address] = Byt;
	}
	else
		Memory.VRAM[address = ((PPU.VMA.Address << 1) + 1) & 0xffff] = Byt;

	if (PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2119_tile(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t rem = PPU.VMA.Address & PPU.VMA.Mask1;
	uint32_t address = ((((PPU.VMA.Address & ~PPU.VMA.Mask1) + (rem >> PPU.VMA.Shift) + ((rem & (PPU.VMA.FullGraphicCount - 1)) << 3)) << 1) + 1) & 0xffff;

	Memory.VRAM[address] = Byt;

	if (PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2119_linear(uint8_t Byt)
{
	if (CHECK_INBLANK())
		return;

	uint32_t address;

	Memory.VRAM[address = ((PPU.VMA.Address << 1) + 1) & 0xffff] = Byt;

	if (PPU.VMA.High)
		PPU.VMA.Address += PPU.VMA.Increment;
}

inline void REGISTER_2180(uint8_t Byt)
{
	Memory.RAM[PPU.WRAM++] = Byt;
	PPU.WRAM &= 0x1ffff;
}

inline uint8_t REGISTER_4212()
{
	uint8_t byte = 0;

	if (CPU.V_Counter >= PPU.ScreenHeight + FIRST_VISIBLE_LINE && CPU.V_Counter < PPU.ScreenHeight + FIRST_VISIBLE_LINE + 3)
		byte = 1;
	if (CPU.Cycles < Timings.HBlankEnd || CPU.Cycles >= Timings.HBlankStart)
		byte |= 0x40;
	if (CPU.V_Counter >= PPU.ScreenHeight + FIRST_VISIBLE_LINE)
		byte |= 0x80;

	return byte;
}