#ifndef _PPU_H_
#define _PPU_H_

#include "../copyright"
#include <stdint.h>
#include <boolean.h>
#include <retro_inline.h>

#define FIRST_VISIBLE_LINE 1

extern uint8_t  GetBank;
extern uint16_t SignExtend [2];

#define TILE_2BIT 0
#define TILE_4BIT 1
#define TILE_8BIT 2

#define MAX_2BIT_TILES 4096
#define MAX_4BIT_TILES 2048
#define MAX_8BIT_TILES 1024

#define PPU_H_BEAM_IRQ_SOURCE (1 << 0)
#define PPU_V_BEAM_IRQ_SOURCE (1 << 1)
#define GSU_IRQ_SOURCE        (1 << 2)
#define SA1_DMA_IRQ_SOURCE    (1 << 5)
#define SA1_IRQ_SOURCE        (1 << 7)

typedef struct
{
   uint32_t Count [6];
   uint32_t Left  [6][6];
   uint32_t Right [6][6];
} ClipData;

typedef struct
{
   bool     ColorsChanged;
   uint8_t  HDMA;
   bool     OBJChanged;
   bool     RenderThisFrame;
   bool     DirectColourMapsNeedRebuild;
   uint32_t FrameCount;
   uint8_t* TileCache  [3];
   uint8_t* TileCached [3];
   bool     FirstVRAMRead;
   bool     DoubleHeightPixels;
   bool     Interlace;
   bool     DoubleWidthPixels;
   bool     HalfWidthPixels;
   int32_t  RenderedScreenHeight;
   int32_t  RenderedScreenWidth;
   uint32_t Red          [256];
   uint32_t Green        [256];
   uint32_t Blue         [256];
   uint8_t* XB;
   uint16_t ScreenColors [256];
   int32_t  PreviousLine;
   int32_t  CurrentLine;
   int32_t  Controller;
   uint32_t Joypads   [5];
   uint32_t SuperScope;
   uint32_t Mouse     [2];
   int32_t  PrevMouseX[2];
   int32_t  PrevMouseY[2];
   ClipData Clip      [2];
} InternalPPU;

typedef struct
{
   int16_t  HPos;
   uint16_t VPos;
   uint16_t Name;
   uint8_t  VFlip;
   uint8_t  HFlip;
   uint8_t  Priority;
   uint8_t  Palette;
   uint8_t  Size;
} SOBJ;

typedef struct
{
   uint8_t BGMode;
   uint8_t BG3Priority;
   uint8_t Brightness;

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

   struct
   {
      uint16_t SCBase;
      uint16_t VOffset;
      uint16_t HOffset;
      uint8_t  BGSize;
      uint16_t NameBase;
      uint16_t SCSize;
   } BG [4];

   bool     CGFLIP;
   uint16_t CGDATA [256];
   uint8_t  FirstSprite;
   uint8_t  UNUSED1;
   SOBJ     OBJ    [128];
   uint8_t  OAMPriorityRotation;
   uint16_t OAMAddr;
   uint8_t  RangeTimeOver;
   uint8_t  OAMFlip;
   uint16_t UNUSED2;
   uint16_t IRQVBeamPos;
   uint16_t IRQHBeamPos;
   uint16_t VBeamPosLatched;
   uint16_t HBeamPosLatched;
   uint8_t  HBeamFlip;
   uint8_t  VBeamFlip;
   uint8_t  UNUSED3;
   int16_t  MatrixA;
   int16_t  MatrixB;
   int16_t  MatrixC;
   int16_t  MatrixD;
   int16_t  CentreX;
   int16_t  CentreY;
   uint8_t  Joypad1ButtonReadPos;
   uint8_t  Joypad2ButtonReadPos;
   uint8_t  CGADD;
   uint8_t  FixedColourRed;
   uint8_t  FixedColourGreen;
   uint8_t  FixedColourBlue;
   uint16_t SavedOAMAddr;
   uint16_t ScreenHeight;
   uint32_t WRAM;
   uint8_t  UNUSED4;
   bool     ForcedBlanking;
   bool     UNUSED5;
   bool     UNUSED6;
   uint8_t  OBJSizeSelect;
   uint16_t OBJNameBase;
   bool     UNUSED7;
   uint8_t  UNUSED8;
   uint8_t  OAMData                [512 + 32];
   bool     VTimerEnabled;
   bool     HTimerEnabled;
   int16_t  HTimerPosition;
   uint8_t  Mosaic;
   bool     BGMosaic               [4];
   bool     Mode7HFlip;
   bool     Mode7VFlip;
   uint8_t  Mode7Repeat;
   uint8_t  Window1Left;
   uint8_t  Window1Right;
   uint8_t  Window2Left;
   uint8_t  Window2Right;
   uint8_t  UNUSED9                [6];
   uint8_t  ClipWindowOverlapLogic [6];
   uint8_t  ClipWindow1Enable      [6];
   uint8_t  ClipWindow2Enable      [6];
   bool     ClipWindow1Inside      [6];
   bool     ClipWindow2Inside      [6];
   bool     RecomputeClipWindows;
   bool     CGFLIPRead;
   uint16_t OBJNameSelect;
   bool     Need16x8Multiply;
   uint8_t  Joypad3ButtonReadPos;
   uint8_t  UNUSED10 [2];
   uint16_t OAMWriteRegister;
   uint8_t  BGnxOFSbyte;
   uint8_t  OpenBus1;
   uint8_t  OpenBus2;
} SPPU;

#define CLIP_OR 0
#define CLIP_AND 1
#define CLIP_XOR 2
#define CLIP_XNOR 3

typedef struct
{
   bool     TransferDirection;
   bool     AAddressFixed;
   bool     AAddressDecrement;
   uint8_t  TransferMode;
   uint8_t  ABank;
   uint16_t AAddress;
   uint16_t Address;
   uint8_t  BAddress;

   /* General DMA only: */
   uint16_t TransferBytes;

   /* H-DMA only: */
   bool     HDMAIndirectAddressing;
   uint16_t IndirectAddress;
   uint8_t  IndirectBank;
   bool     Repeat;
   uint8_t  LineCount;
   bool     FirstLine;
} SDMA;

void S9xUpdateScreen(void);
void S9xResetPPU(void);
void S9xSoftResetPPU(void);
void S9xFixColourBrightness(void);
void S9xUpdateJoypads(void);
void S9xProcessMouse(int32_t which1);
void S9xSuperFXExec(void);

void S9xSetPPU(uint8_t Byte, uint16_t Address);
uint8_t S9xGetPPU(uint16_t Address);
void S9xSetCPU(uint8_t Byte, uint16_t Address);
uint8_t S9xGetCPU(uint16_t Address);

void S9xInitC4(void);
void S9xSetC4(uint8_t Byte, uint16_t Address);
uint8_t S9xGetC4(uint16_t Address);
void S9xSetC4RAM(uint8_t Byte, uint16_t Address);
uint8_t S9xGetC4RAM(uint16_t Address);

extern SPPU PPU;
extern SDMA DMA [8];
extern InternalPPU IPPU;

#include "memmap.h"

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

extern SnesModel* Model;
extern SnesModel M1SNES;
extern SnesModel M2SNES;

#define MAX_5C77_VERSION 0x01
#define MAX_5A22_VERSION 0x02
#define MAX_5C78_VERSION 0x03

/* Platform specific input functions used by PPU.C */
void JustifierButtons(uint32_t*);
bool JustifierOffscreen(void);

static INLINE void FLUSH_REDRAW(void)
{
   if (IPPU.PreviousLine != IPPU.CurrentLine)
      S9xUpdateScreen();
}

static INLINE void REGISTER_2104(uint8_t byte)
{
   if (PPU.OAMAddr & 0x100)
   {
      int32_t addr = ((PPU.OAMAddr & 0x10f) << 1) + (PPU.OAMFlip & 1);
      if (byte != PPU.OAMData [addr])
      {
         SOBJ* pObj;
         FLUSH_REDRAW();
         PPU.OAMData [addr] = byte;
         IPPU.OBJChanged = true;

         /* X position high bit, and sprite size (x4) */
         pObj = &PPU.OBJ [(addr & 0x1f) * 4];

         pObj->HPos = (pObj->HPos & 0xFF) | SignExtend[(byte >> 0) & 1];
         pObj++->Size = byte & 2;
         pObj->HPos = (pObj->HPos & 0xFF) | SignExtend[(byte >> 2) & 1];
         pObj++->Size = byte & 8;
         pObj->HPos = (pObj->HPos & 0xFF) | SignExtend[(byte >> 4) & 1];
         pObj++->Size = byte & 32;
         pObj->HPos = (pObj->HPos & 0xFF) | SignExtend[(byte >> 6) & 1];
         pObj->Size = byte & 128;
      }
      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;
            IPPU.OBJChanged = true;
         }
      }
      else if (PPU.OAMPriorityRotation && (PPU.OAMAddr & 1))
         IPPU.OBJChanged = true;
   }
   else if (!(PPU.OAMFlip & 1))
   {
      PPU.OAMWriteRegister &= 0xff00;
      PPU.OAMWriteRegister |= byte;
      PPU.OAMFlip |= 1;
      if (PPU.OAMPriorityRotation && (PPU.OAMAddr & 1))
         IPPU.OBJChanged = true;
   }
   else
   {
      int32_t addr;
      uint8_t lowbyte, highbyte;
      PPU.OAMWriteRegister &= 0x00ff;
      lowbyte = (uint8_t)(PPU.OAMWriteRegister);
      highbyte = byte;
      PPU.OAMWriteRegister |= byte << 8;
      addr = (PPU.OAMAddr << 1);

      if (lowbyte != PPU.OAMData [addr] || highbyte != PPU.OAMData [addr + 1])
      {
         FLUSH_REDRAW();
         PPU.OAMData [addr] = lowbyte;
         PPU.OAMData [addr + 1] = highbyte;
         IPPU.OBJChanged = true;
         if (addr & 2)
         {
            /* Tile */
            PPU.OBJ[addr = PPU.OAMAddr >> 1].Name = PPU.OAMWriteRegister & 0x1ff;
            /* priority, h and v flip. */
            PPU.OBJ[addr].Palette = (highbyte >> 1) & 7;
            PPU.OBJ[addr].Priority = (highbyte >> 4) & 3;
            PPU.OBJ[addr].HFlip = (highbyte >> 6) & 1;
            PPU.OBJ[addr].VFlip = (highbyte >> 7) & 1;
         }
         else
         {
            /* X position (low) */
            PPU.OBJ[addr = PPU.OAMAddr >> 1].HPos &= 0xFF00;
            PPU.OBJ[addr].HPos |= lowbyte;

            /* Sprite Y position */
            PPU.OBJ[addr].VPos = highbyte;
         }
      }
      PPU.OAMFlip &= ~1;
      ++PPU.OAMAddr;
      if (PPU.OAMPriorityRotation && PPU.FirstSprite != (PPU.OAMAddr >> 1))
      {
         PPU.FirstSprite = (PPU.OAMAddr & 0xFE) >> 1;
         IPPU.OBJChanged = true;
      }
   }

   Memory.FillRAM [0x2104] = byte;
}

static INLINE void REGISTER_2118(uint8_t Byte)
{
   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] = Byte;
   }
   else
      Memory.VRAM[address = (PPU.VMA.Address << 1) & 0xFFFF] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (!PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2118_tile(uint8_t Byte)
{
   uint32_t address;
   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] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (!PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2118_linear(uint8_t Byte)
{
   uint32_t address = (PPU.VMA.Address << 1) & 0xFFFF;
   Memory.VRAM[address] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (!PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2119(uint8_t Byte)
{
   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] = Byte;
   }
   else
      Memory.VRAM[address = ((PPU.VMA.Address << 1) + 1) & 0xFFFF] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2119_tile(uint8_t Byte)
{
   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] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2119_linear(uint8_t Byte)
{
   uint32_t address;
   Memory.VRAM[address = ((PPU.VMA.Address << 1) + 1) & 0xFFFF] = Byte;
   IPPU.TileCached [TILE_2BIT][address >> 4] = false;
   IPPU.TileCached [TILE_4BIT][address >> 5] = false;
   IPPU.TileCached [TILE_8BIT][address >> 6] = false;
   if (PPU.VMA.High)
      PPU.VMA.Address += PPU.VMA.Increment;
}

static INLINE void REGISTER_2122(uint8_t Byte)
{
   if (PPU.CGFLIP)
   {
      if ((Byte & 0x7f) != (PPU.CGDATA[PPU.CGADD] >> 8))
      {
         FLUSH_REDRAW();
         PPU.CGDATA[PPU.CGADD] &= 0x00FF;
         PPU.CGDATA[PPU.CGADD] |= (Byte & 0x7f) << 8;
         IPPU.ColorsChanged = true;
         IPPU.Blue [PPU.CGADD] = IPPU.XB [(Byte >> 2) & 0x1f];
         IPPU.Green [PPU.CGADD] = IPPU.XB [(PPU.CGDATA[PPU.CGADD] >> 5) & 0x1f];
         IPPU.ScreenColors [PPU.CGADD] = (uint16_t) BUILD_PIXEL(IPPU.Red [PPU.CGADD], IPPU.Green [PPU.CGADD], IPPU.Blue [PPU.CGADD]);
      }
      PPU.CGADD++;
   }
   else if (Byte != (uint8_t)(PPU.CGDATA[PPU.CGADD] & 0xff))
   {
      FLUSH_REDRAW();
      PPU.CGDATA[PPU.CGADD] &= 0x7F00;
      PPU.CGDATA[PPU.CGADD] |= Byte;
      IPPU.ColorsChanged = true;
      IPPU.Red [PPU.CGADD] = IPPU.XB [Byte & 0x1f];
      IPPU.Green [PPU.CGADD] = IPPU.XB [(PPU.CGDATA[PPU.CGADD] >> 5) & 0x1f];
      IPPU.ScreenColors [PPU.CGADD] = (uint16_t) BUILD_PIXEL(IPPU.Red [PPU.CGADD], IPPU.Green [PPU.CGADD], IPPU.Blue [PPU.CGADD]);
   }
   PPU.CGFLIP = !PPU.CGFLIP;
}

static INLINE void REGISTER_2180(uint8_t Byte)
{
   Memory.RAM[PPU.WRAM++] = Byte;
   PPU.WRAM &= 0x1FFFF;
   Memory.FillRAM [0x2180] = Byte;
}

static INLINE uint8_t REGISTER_4212(void)
{
   uint8_t GetBank = 0;
   if (CPU.V_Counter >= PPU.ScreenHeight + FIRST_VISIBLE_LINE && CPU.V_Counter < PPU.ScreenHeight + FIRST_VISIBLE_LINE + 3)
      GetBank = 1;

   GetBank |= CPU.Cycles >= Settings.HBlankStart ? 0x40 : 0;
   if (CPU.V_Counter >= PPU.ScreenHeight + FIRST_VISIBLE_LINE)
      GetBank |= 0x80; /* XXX: 0x80 or 0xc0 ? */

   return GetBank;
}
#endif