text: import { Tile } from './tile.js';
  import { utils } from './utils.js';
  
  var PPU = function (nes) {
      this.nes = nes;
  
      // Keep Chrome happy
      this.vramMem = null;
      this.spriteMem = null;
      this.vramAddress = null;
      this.vramTmpAddress = null;
      this.vramBufferedReadValue = null;
      this.firstWrite = null;
      this.sramAddress = null;
      this.currentMirroring = null;
      this.requestEndFrame = null;
      this.nmiOk = null;
      this.dummyCycleToggle = null;
      this.validTileData = null;
      this.nmiCounter = null;
      this.scanlineAlreadyRendered = null;
      this.f_nmiOnVblank = null;
      this.f_spriteSize = null;
      this.f_bgPatternTable = null;
      this.f_spPatternTable = null;
      this.f_addrInc = null;
      this.f_nTblAddress = null;
      this.f_color = null;
      this.f_spVisibility = null;
      this.f_bgVisibility = null;
      this.f_spClipping = null;
      this.f_bgClipping = null;
      this.f_dispType = null;
      this.cntFV = null;
      this.cntV = null;
      this.cntH = null;
      this.cntVT = null;
      this.cntHT = null;
      this.regFV = null;
      this.regV = null;
      this.regH = null;
      this.regVT = null;
      this.regHT = null;
      this.regFH = null;
      this.regS = null;
      this.curNt = null;
      this.attrib = null;
      this.buffer = null;
      this.bgbuffer = null;
      this.pixrendered = null;
  
      this.validTileData = null;
      this.scantile = null;
      this.scanline = null;
      this.lastRenderedScanline = null;
      this.curX = null;
      this.sprX = null;
      this.sprY = null;
      this.sprTile = null;
      this.sprCol = null;
      this.vertFlip = null;
      this.horiFlip = null;
      this.bgPriority = null;
      this.spr0HitX = null;
      this.spr0HitY = null;
      this.hitSpr0 = null;
      this.sprPalette = null;
      this.imgPalette = null;
      this.ptTile = null;
      this.ntable1 = null;
      this.currentMirroring = null;
      this.nameTable = null;
      this.vramMirrorTable = null;
      this.palTable = null;
  
      // Rendering Options:
      this.showSpr0Hit = false;
      this.clipToTvSize = true;
  
      this.reset();
  };
  
  PPU.prototype = {
      // Status flags:
      STATUS_VRAMWRITE: 4,
      STATUS_SLSPRITECOUNT: 5,
      STATUS_SPRITE0HIT: 6,
      STATUS_VBLANK: 7,
  
      reset: function () {
          var i;
  
          // Memory
          this.vramMem = new Array(0x8000);
          this.spriteMem = new Array(0x100);
          for (i = 0; i < this.vramMem.length; i++) {
              this.vramMem[i] = 0;
          }
          for (i = 0; i < this.spriteMem.length; i++) {
              this.spriteMem[i] = 0;
          }
  
          // VRAM I/O:
          this.vramAddress = null;
          this.vramTmpAddress = null;
          this.vramBufferedReadValue = 0;
          this.firstWrite = true; // VRAM/Scroll Hi/Lo latch
  
          // SPR-RAM I/O:
          this.sramAddress = 0; // 8-bit only.
  
          this.currentMirroring = -1;
          this.requestEndFrame = false;
          this.nmiOk = false;
          this.dummyCycleToggle = false;
          this.validTileData = false;
          this.nmiCounter = 0;
          this.scanlineAlreadyRendered = null;
  
          // Control Flags Register 1:
          this.f_nmiOnVblank = 0; // NMI on VBlank. 0=disable, 1=enable
          this.f_spriteSize = 0; // Sprite size. 0=8x8, 1=8x16
          this.f_bgPatternTable = 0; // Background Pattern Table address. 0=0x0000,1=0x1000
          this.f_spPatternTable = 0; // Sprite Pattern Table address. 0=0x0000,1=0x1000
          this.f_addrInc = 0; // PPU Address Increment. 0=1,1=32
          this.f_nTblAddress = 0; // Name Table Address. 0=0x2000,1=0x2400,2=0x2800,3=0x2C00
  
          // Control Flags Register 2:
          this.f_color = 0; // Background color. 0=black, 1=blue, 2=green, 4=red
          this.f_spVisibility = 0; // Sprite visibility. 0=not displayed,1=displayed
          this.f_bgVisibility = 0; // Background visibility. 0=Not Displayed,1=displayed
          this.f_spClipping = 0; // Sprite clipping. 0=Sprites invisible in left 8-pixel column,1=No clipping
          this.f_bgClipping = 0; // Background clipping. 0=BG invisible in left 8-pixel column, 1=No clipping
          this.f_dispType = 0; // Display type. 0=color, 1=monochrome
  
          // Counters:
          this.cntFV = 0;
          this.cntV = 0;
          this.cntH = 0;
          this.cntVT = 0;
          this.cntHT = 0;
  
          // Registers:
          this.regFV = 0;
          this.regV = 0;
          this.regH = 0;
          this.regVT = 0;
          this.regHT = 0;
          this.regFH = 0;
          this.regS = 0;
  
          // These are temporary variables used in rendering and sound procedures.
          // Their states outside of those procedures can be ignored.
          // TODO: the use of this is a bit weird, investigate
          this.curNt = null;
  
          // Variables used when rendering:
          this.attrib = new Array(32);
          this.buffer = new Array(256 * 240);
          this.bgbuffer = new Array(256 * 240);
          this.pixrendered = new Array(256 * 240);
  
          this.validTileData = null;
  
          this.scantile = new Array(32);
  
          // Initialize misc vars:
          this.scanline = 0;
          this.lastRenderedScanline = -1;
          this.curX = 0;
  
          // Sprite data:
          this.sprX = new Array(64); // X coordinate
          this.sprY = new Array(64); // Y coordinate
          this.sprTile = new Array(64); // Tile Index (into pattern table)
          this.sprCol = new Array(64); // Upper two bits of color
          this.vertFlip = new Array(64); // Vertical Flip
          this.horiFlip = new Array(64); // Horizontal Flip
          this.bgPriority = new Array(64); // Background priority
          this.spr0HitX = 0; // Sprite #0 hit X coordinate
          this.spr0HitY = 0; // Sprite #0 hit Y coordinate
          this.hitSpr0 = false;
  
          // Palette data:
          this.sprPalette = new Array(16);
          this.imgPalette = new Array(16);
  
          // Create pattern table tile buffers:
          this.ptTile = new Array(512);
          for (i = 0; i < 512; i++) {
              this.ptTile[i] = new Tile();
          }
  
          // Create nametable buffers:
          // Name table data:
          this.ntable1 = new Array(4);
          this.currentMirroring = -1;
          this.nameTable = new Array(4);
          for (i = 0; i < 4; i++) {
              this.nameTable[i] = new NameTable(32, 32, 'Nt' + i);
          }
  
          // Initialize mirroring lookup table:
          this.vramMirrorTable = new Array(0x8000);
          for (i = 0; i < 0x8000; i++) {
              this.vramMirrorTable[i] = i;
          }
  
          this.palTable = new PaletteTable();
          this.palTable.loadNTSCPalette();
          //this.palTable.loadDefaultPalette();
  
          this.updateControlReg1(0);
          this.updateControlReg2(0);
      },
  
      // Sets Nametable mirroring.
      setMirroring: function (mirroring) {
          if (mirroring === this.currentMirroring) {
              return;
          }
  
          this.currentMirroring = mirroring;
          this.triggerRendering();
  
          // Remove mirroring:
          if (this.vramMirrorTable === null) {
              this.vramMirrorTable = new Array(0x8000);
          }
          for (var i = 0; i < 0x8000; i++) {
              this.vramMirrorTable[i] = i;
          }
  
          // Palette mirroring:
          this.defineMirrorRegion(0x3f20, 0x3f00, 0x20);
          this.defineMirrorRegion(0x3f40, 0x3f00, 0x20);
          this.defineMirrorRegion(0x3f80, 0x3f00, 0x20);
          this.defineMirrorRegion(0x3fc0, 0x3f00, 0x20);
  
          // Additional mirroring:
          this.defineMirrorRegion(0x3000, 0x2000, 0xf00);
          this.defineMirrorRegion(0x4000, 0x0000, 0x4000);
  
          if (mirroring === this.nes.rom.HORIZONTAL_MIRRORING) {
              // Horizontal mirroring.
  
              this.ntable1[0] = 0;
              this.ntable1[1] = 0;
              this.ntable1[2] = 1;
              this.ntable1[3] = 1;
  
              this.defineMirrorRegion(0x2400, 0x2000, 0x400);
              this.defineMirrorRegion(0x2c00, 0x2800, 0x400);
          } else if (mirroring === this.nes.rom.VERTICAL_MIRRORING) {
              // Vertical mirroring.
  
              this.ntable1[0] = 0;
              this.ntable1[1] = 1;
              this.ntable1[2] = 0;
              this.ntable1[3] = 1;
  
              this.defineMirrorRegion(0x2800, 0x2000, 0x400);
              this.defineMirrorRegion(0x2c00, 0x2400, 0x400);
          } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING) {
              // Single Screen mirroring
  
              this.ntable1[0] = 0;
              this.ntable1[1] = 0;
              this.ntable1[2] = 0;
              this.ntable1[3] = 0;
  
              this.defineMirrorRegion(0x2400, 0x2000, 0x400);
              this.defineMirrorRegion(0x2800, 0x2000, 0x400);
              this.defineMirrorRegion(0x2c00, 0x2000, 0x400);
          } else if (mirroring === this.nes.rom.SINGLESCREEN_MIRRORING2) {
              this.ntable1[0] = 1;
              this.ntable1[1] = 1;
              this.ntable1[2] = 1;
              this.ntable1[3] = 1;
  
              this.defineMirrorRegion(0x2400, 0x2400, 0x400);
              this.defineMirrorRegion(0x2800, 0x2400, 0x400);
              this.defineMirrorRegion(0x2c00, 0x2400, 0x400);
          } else {
              // Assume Four-screen mirroring.
  
              this.ntable1[0] = 0;
              this.ntable1[1] = 1;
              this.ntable1[2] = 2;
              this.ntable1[3] = 3;
          }
      },
  
      // Define a mirrored area in the address lookup table.
      // Assumes the regions don't overlap.
      // The 'to' region is the region that is physically in memory.
      defineMirrorRegion: function (fromStart, toStart, size) {
          for (var i = 0; i < size; i++) {
              this.vramMirrorTable[fromStart + i] = toStart + i;
          }
      },
  
      startVBlank: function () {
          // Do NMI:
          this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI);
  
          // Make sure everything is rendered:
          if (this.lastRenderedScanline < 239) {
              this.renderFramePartially(
                  this.lastRenderedScanline + 1,
                  240 - this.lastRenderedScanline
              );
          }
  
          // End frame:
          this.endFrame();
  
          // Reset scanline counter:
          this.lastRenderedScanline = -1;
      },
  
      endScanline: function () {
          switch (this.scanline) {
              case 19:
                  // Dummy scanline.
                  // May be variable length:
                  if (this.dummyCycleToggle) {
                      // Remove dead cycle at end of scanline,
                      // for next scanline:
                      this.curX = 1;
                      this.dummyCycleToggle = !this.dummyCycleToggle;
                  }
                  break;
  
              case 20:
                  // Clear VBlank flag:
                  this.setStatusFlag(this.STATUS_VBLANK, false);
  
                  // Clear Sprite #0 hit flag:
                  this.setStatusFlag(this.STATUS_SPRITE0HIT, false);
                  this.hitSpr0 = false;
                  this.spr0HitX = -1;
                  this.spr0HitY = -1;
  
                  if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
                      // Update counters:
                      this.cntFV = this.regFV;
                      this.cntV = this.regV;
                      this.cntH = this.regH;
                      this.cntVT = this.regVT;
                      this.cntHT = this.regHT;
  
                      if (this.f_bgVisibility === 1) {
                          // Render dummy scanline:
                          this.renderBgScanline(false, 0);
                      }
                  }
  
                  if (this.f_bgVisibility === 1 && this.f_spVisibility === 1) {
                      // Check sprite 0 hit for first scanline:
                      this.checkSprite0(0);
                  }
  
                  if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
                      // Clock mapper IRQ Counter:
                      this.nes.mmap.clockIrqCounter();
                  }
                  break;
  
              case 261:
                  // Dead scanline, no rendering.
                  // Set VINT:
                  this.setStatusFlag(this.STATUS_VBLANK, true);
                  this.requestEndFrame = true;
                  this.nmiCounter = 9;
  
                  // Wrap around:
                  this.scanline = -1; // will be incremented to 0
  
                  break;
  
              default:
                  if (this.scanline >= 21 && this.scanline <= 260) {
                      // Render normally:
                      if (this.f_bgVisibility === 1) {
                          if (!this.scanlineAlreadyRendered) {
                              // update scroll:
                              this.cntHT = this.regHT;
                              this.cntH = this.regH;
                              this.renderBgScanline(true, this.scanline + 1 - 21);
                          }
                          this.scanlineAlreadyRendered = false;
  
                          // Check for sprite 0 (next scanline):
                          if (!this.hitSpr0 && this.f_spVisibility === 1) {
                              if (
                                  this.sprX[0] >= -7 &&
                                  this.sprX[0] < 256 &&
                                  this.sprY[0] + 1 <= this.scanline - 20 &&
                                  this.sprY[0] + 1 + (this.f_spriteSize === 0 ? 8 : 16) >=
                                      this.scanline - 20
                              ) {
                                  if (this.checkSprite0(this.scanline - 20)) {
                                      this.hitSpr0 = true;
                                  }
                              }
                          }
                      }
  
                      if (this.f_bgVisibility === 1 || this.f_spVisibility === 1) {
                          // Clock mapper IRQ Counter:
                          this.nes.mmap.clockIrqCounter();
                      }
                  }
          }
  
          this.scanline++;
          this.regsToAddress();
          this.cntsToAddress();
      },
  
      startFrame: function () {
          // Set background color:
          var bgColor = 0;
  
          if (this.f_dispType === 0) {
              // Color display.
              // f_color determines color emphasis.
              // Use first entry of image palette as BG color.
              bgColor = this.imgPalette[0];
          } else {
              // Monochrome display.
              // f_color determines the bg color.
              switch (this.f_color) {
                  case 0:
                      // Black
                      bgColor = 0x00000;
                      break;
                  case 1:
                      // Green
                      bgColor = 0x00ff00;
                      break;
                  case 2:
                      // Blue
                      bgColor = 0xff0000;
                      break;
                  case 3:
                      // Invalid. Use black.
                      bgColor = 0x000000;
                      break;
                  case 4:
                      // Red
                      bgColor = 0x0000ff;
                      break;
                  default:
                      // Invalid. Use black.
                      bgColor = 0x0;
              }
          }
  
          var buffer = this.buffer;
          var i;
          for (i = 0; i < 256 * 240; i++) {
              buffer[i] = bgColor;
          }
          var pixrendered = this.pixrendered;
          for (i = 0; i < pixrendered.length; i++) {
              pixrendered[i] = 65;
          }
      },
  
      endFrame: function () {
          var i, x, y;
          var buffer = this.buffer;
  
          // Draw spr#0 hit coordinates:
          if (this.showSpr0Hit) {
              // Spr 0 position:
              if (
                  this.sprX[0] >= 0 &&
                  this.sprX[0] < 256 &&
                  this.sprY[0] >= 0 &&
                  this.sprY[0] < 240
              ) {
                  for (i = 0; i < 256; i++) {
                      buffer[(this.sprY[0] << 8) + i] = 0xff5555;
                  }
                  for (i = 0; i < 240; i++) {
                      buffer[(i << 8) + this.sprX[0]] = 0xff5555;
                  }
              }
              // Hit position:
              if (
                  this.spr0HitX >= 0 &&
                  this.spr0HitX < 256 &&
                  this.spr0HitY >= 0 &&
                  this.spr0HitY < 240
              ) {
                  for (i = 0; i < 256; i++) {
                      buffer[(this.spr0HitY << 8) + i] = 0x55ff55;
                  }
                  for (i = 0; i < 240; i++) {
                      buffer[(i << 8) + this.spr0HitX] = 0x55ff55;
                  }
              }
          }
  
          // This is a bit lazy..
          // if either the sprites or the background should be clipped,
          // both are clipped after rendering is finished.
          if (this.clipToTvSize || this.f_bgClipping === 0 || this.f_spClipping === 0) {
              // Clip left 8-pixels column:
              for (y = 0; y < 240; y++) {
                  for (x = 0; x < 8; x++) {
                      buffer[(y << 8) + x] = 0;
                  }
              }
          }
  
          if (this.clipToTvSize) {
              // Clip right 8-pixels column too:
              for (y = 0; y < 240; y++) {
                  for (x = 0; x < 8; x++) {
                      buffer[(y << 8) + 255 - x] = 0;
                  }
              }
          }
  
          // Clip top and bottom 8 pixels:
          if (this.clipToTvSize) {
              for (y = 0; y < 8; y++) {
                  for (x = 0; x < 256; x++) {
                      buffer[(y << 8) + x] = 0;
                      buffer[((239 - y) << 8) + x] = 0;
                  }
              }
          }
  
          this.nes.ui.writeFrame(buffer);
      },
  
      updateControlReg1: function (value) {
          this.triggerRendering();
  
          this.f_nmiOnVblank = (value >> 7) & 1;
          this.f_spriteSize = (value >> 5) & 1;
          this.f_bgPatternTable = (value >> 4) & 1;
          this.f_spPatternTable = (value >> 3) & 1;
          this.f_addrInc = (value >> 2) & 1;
          this.f_nTblAddress = value & 3;
  
          this.regV = (value >> 1) & 1;
          this.regH = value & 1;
          this.regS = (value >> 4) & 1;
      },
  
      updateControlReg2: function (value) {
          this.triggerRendering();
  
          this.f_color = (value >> 5) & 7;
          this.f_spVisibility = (value >> 4) & 1;
          this.f_bgVisibility = (value >> 3) & 1;
          this.f_spClipping = (value >> 2) & 1;
          this.f_bgClipping = (value >> 1) & 1;
          this.f_dispType = value & 1;
  
          if (this.f_dispType === 0) {
              this.palTable.setEmphasis(this.f_color);
          }
          this.updatePalettes();
      },
  
      setStatusFlag: function (flag, value) {
          var n = 1 << flag;
          this.nes.cpu.mem[0x2002] = (this.nes.cpu.mem[0x2002] & (255 - n)) | (value ? n : 0);
      },
  
      // CPU Register $2002:
      // Read the Status Register.
      readStatusRegister: function () {
          var tmp = this.nes.cpu.mem[0x2002];
  
          // Reset scroll & VRAM Address toggle:
          this.firstWrite = true;
  
          // Clear VBlank flag:
          this.setStatusFlag(this.STATUS_VBLANK, false);
  
          // Fetch status data:
          return tmp;
      },
  
      // CPU Register $2003:
      // Write the SPR-RAM address that is used for sramWrite (Register 0x2004 in CPU memory map)
      writeSRAMAddress: function (address) {
          this.sramAddress = address;
      },
  
      // CPU Register $2004 (R):
      // Read from SPR-RAM (Sprite RAM).
      // The address should be set first.
      sramLoad: function () {
          /*short tmp = sprMem.load(sramAddress);
          sramAddress++; // Increment address
          sramAddress%=0x100;
          return tmp;*/
          return this.spriteMem[this.sramAddress];
      },
  
      // CPU Register $2004 (W):
      // Write to SPR-RAM (Sprite RAM).
      // The address should be set first.
      sramWrite: function (value) {
          this.spriteMem[this.sramAddress] = value;
          this.spriteRamWriteUpdate(this.sramAddress, value);
          this.sramAddress++; // Increment address
          this.sramAddress %= 0x100;
      },
  
      // CPU Register $2005:
      // Write to scroll registers.
      // The first write is the vertical offset, the second is the
      // horizontal offset:
      scrollWrite: function (value) {
          this.triggerRendering();
  
          if (this.firstWrite) {
              // First write, horizontal scroll:
              this.regHT = (value >> 3) & 31;
              this.regFH = value & 7;
          } else {
              // Second write, vertical scroll:
              this.regFV = value & 7;
              this.regVT = (value >> 3) & 31;
          }
          this.firstWrite = !this.firstWrite;
      },
  
      // CPU Register $2006:
      // Sets the adress used when reading/writing from/to VRAM.
      // The first write sets the high byte, the second the low byte.
      writeVRAMAddress: function (address) {
          if (this.firstWrite) {
              this.regFV = (address >> 4) & 3;
              this.regV = (address >> 3) & 1;
              this.regH = (address >> 2) & 1;
              this.regVT = (this.regVT & 7) | ((address & 3) << 3);
          } else {
              this.triggerRendering();
  
              this.regVT = (this.regVT & 24) | ((address >> 5) & 7);
              this.regHT = address & 31;
  
              this.cntFV = this.regFV;
              this.cntV = this.regV;
              this.cntH = this.regH;
              this.cntVT = this.regVT;
              this.cntHT = this.regHT;
  
              this.checkSprite0(this.scanline - 20);
          }
  
          this.firstWrite = !this.firstWrite;
  
          // Invoke mapper latch:
          this.cntsToAddress();
          if (this.vramAddress < 0x2000) {
              this.nes.mmap.latchAccess(this.vramAddress);
          }
      },
  
      // CPU Register $2007(R):
      // Read from PPU memory. The address should be set first.
      vramLoad: function () {
          var tmp;
  
          this.cntsToAddress();
          this.regsToAddress();
  
          // If address is in range 0x0000-0x3EFF, return buffered values:
          if (this.vramAddress <= 0x3eff) {
              tmp = this.vramBufferedReadValue;
  
              // Update buffered value:
              if (this.vramAddress < 0x2000) {
                  this.vramBufferedReadValue = this.vramMem[this.vramAddress];
              } else {
                  this.vramBufferedReadValue = this.mirroredLoad(this.vramAddress);
              }
  
              // Mapper latch access:
              if (this.vramAddress < 0x2000) {
                  this.nes.mmap.latchAccess(this.vramAddress);
              }
  
              // Increment by either 1 or 32, depending on d2 of Control Register 1:
              this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
  
              this.cntsFromAddress();
              this.regsFromAddress();
  
              return tmp; // Return the previous buffered value.
          }
  
          // No buffering in this mem range. Read normally.
          tmp = this.mirroredLoad(this.vramAddress);
  
          // Increment by either 1 or 32, depending on d2 of Control Register 1:
          this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
  
          this.cntsFromAddress();
          this.regsFromAddress();
  
          return tmp;
      },
  
      // CPU Register $2007(W):
      // Write to PPU memory. The address should be set first.
      vramWrite: function (value) {
          this.triggerRendering();
          this.cntsToAddress();
          this.regsToAddress();
  
          if (this.vramAddress >= 0x2000) {
              // Mirroring is used.
              this.mirroredWrite(this.vramAddress, value);
          } else {
              // Write normally.
              this.writeMem(this.vramAddress, value);
  
              // Invoke mapper latch:
              this.nes.mmap.latchAccess(this.vramAddress);
          }
  
          // Increment by either 1 or 32, depending on d2 of Control Register 1:
          this.vramAddress += this.f_addrInc === 1 ? 32 : 1;
          this.regsFromAddress();
          this.cntsFromAddress();
      },
  
      // CPU Register $4014:
      // Write 256 bytes of main memory
      // into Sprite RAM.
      sramDMA: function (value) {
          var baseAddress = value * 0x100;
          var data;
          for (var i = this.sramAddress; i < 256; i++) {
              data = this.nes.cpu.mem[baseAddress + i];
              this.spriteMem[i] = data;
              this.spriteRamWriteUpdate(i, data);
          }
  
          this.nes.cpu.haltCycles(513);
      },
  
      // Updates the scroll registers from a new VRAM address.
      regsFromAddress: function () {
          var address = (this.vramTmpAddress >> 8) & 0xff;
          this.regFV = (address >> 4) & 7;
          this.regV = (address >> 3) & 1;
          this.regH = (address >> 2) & 1;
          this.regVT = (this.regVT & 7) | ((address & 3) << 3);
  
          address = this.vramTmpAddress & 0xff;
          this.regVT = (this.regVT & 24) | ((address >> 5) & 7);
          this.regHT = address & 31;
      },
  
      // Updates the scroll registers from a new VRAM address.
      cntsFromAddress: function () {
          var address = (this.vramAddress >> 8) & 0xff;
          this.cntFV = (address >> 4) & 3;
          this.cntV = (address >> 3) & 1;
          this.cntH = (address >> 2) & 1;
          this.cntVT = (this.cntVT & 7) | ((address & 3) << 3);
  
          address = this.vramAddress & 0xff;
          this.cntVT = (this.cntVT & 24) | ((address >> 5) & 7);
          this.cntHT = address & 31;
      },
  
      regsToAddress: function () {
          var b1 = (this.regFV & 7) << 4;
          b1 |= (this.regV & 1) << 3;
          b1 |= (this.regH & 1) << 2;
          b1 |= (this.regVT >> 3) & 3;
  
          var b2 = (this.regVT & 7) << 5;
          b2 |= this.regHT & 31;
  
          this.vramTmpAddress = ((b1 << 8) | b2) & 0x7fff;
      },
  
      cntsToAddress: function () {
          var b1 = (this.cntFV & 7) << 4;
          b1 |= (this.cntV & 1) << 3;
          b1 |= (this.cntH & 1) << 2;
          b1 |= (this.cntVT >> 3) & 3;
  
          var b2 = (this.cntVT & 7) << 5;
          b2 |= this.cntHT & 31;
  
          this.vramAddress = ((b1 << 8) | b2) & 0x7fff;
      },
  
      incTileCounter: function (count) {
          for (var i = count; i !== 0; i--) {
              this.cntHT++;
              if (this.cntHT === 32) {
                  this.cntHT = 0;
                  this.cntVT++;
                  if (this.cntVT >= 30) {
                      this.cntH++;
                      if (this.cntH === 2) {
                          this.cntH = 0;
                          this.cntV++;
                          if (this.cntV === 2) {
                              this.cntV = 0;
                              this.cntFV++;
                              this.cntFV &= 0x7;
                          }
                      }
                  }
              }
          }
      },
  
      // Reads from memory, taking into account
      // mirroring/mapping of address ranges.
      mirroredLoad: function (address) {
          return this.vramMem[this.vramMirrorTable[address]];
      },
  
      // Writes to memory, taking into account
      // mirroring/mapping of address ranges.
      mirroredWrite: function (address, value) {
          if (address >= 0x3f00 && address < 0x3f20) {
              // Palette write mirroring.
              if (address === 0x3f00 || address === 0x3f10) {
                  this.writeMem(0x3f00, value);
                  this.writeMem(0x3f10, value);
              } else if (address === 0x3f04 || address === 0x3f14) {
                  this.writeMem(0x3f04, value);
                  this.writeMem(0x3f14, value);
              } else if (address === 0x3f08 || address === 0x3f18) {
                  this.writeMem(0x3f08, value);
                  this.writeMem(0x3f18, value);
              } else if (address === 0x3f0c || address === 0x3f1c) {
                  this.writeMem(0x3f0c, value);
                  this.writeMem(0x3f1c, value);
              } else {
                  this.writeMem(address, value);
              }
          } else {
              // Use lookup table for mirrored address:
              if (address < this.vramMirrorTable.length) {
                  this.writeMem(this.vramMirrorTable[address], value);
              } else {
                  throw new Error('Invalid VRAM address: ' + address.toString(16));
              }
          }
      },
  
      triggerRendering: function () {
          if (this.scanline >= 21 && this.scanline <= 260) {
              // Render sprites, and combine:
              this.renderFramePartially(
                  this.lastRenderedScanline + 1,
                  this.scanline - 21 - this.lastRenderedScanline
              );
  
              // Set last rendered scanline:
              this.lastRenderedScanline = this.scanline - 21;
          }
      },
  
      renderFramePartially: function (startScan, scanCount) {
          if (this.f_spVisibility === 1) {
              this.renderSpritesPartially(startScan, scanCount, true);
          }
  
          if (this.f_bgVisibility === 1) {
              var si = startScan << 8;
              var ei = (startScan + scanCount) << 8;
              if (ei > 0xf000) {
                  ei = 0xf000;
              }
              var buffer = this.buffer;
              var bgbuffer = this.bgbuffer;
              var pixrendered = this.pixrendered;
              for (var destIndex = si; destIndex < ei; destIndex++) {
                  if (pixrendered[destIndex] > 0xff) {
                      buffer[destIndex] = bgbuffer[destIndex];
                  }
              }
          }
  
          if (this.f_spVisibility === 1) {
              this.renderSpritesPartially(startScan, scanCount, false);
          }
  
          this.validTileData = false;
      },
  
      renderBgScanline: function (bgbuffer, scan) {
          var baseTile = this.regS === 0 ? 0 : 256;
          var destIndex = (scan << 8) - this.regFH;
  
          this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH];
  
          this.cntHT = this.regHT;
          this.cntH = this.regH;
          this.curNt = this.ntable1[this.cntV + this.cntV + this.cntH];
  
          if (scan < 240 && scan - this.cntFV >= 0) {
              var tscanoffset = this.cntFV << 3;
              var scantile = this.scantile;
              var attrib = this.attrib;
              var ptTile = this.ptTile;
              var nameTable = this.nameTable;
              var imgPalette = this.imgPalette;
              var pixrendered = this.pixrendered;
              var targetBuffer = bgbuffer ? this.bgbuffer : this.buffer;
  
              var t, tpix, att, col;
  
              for (var tile = 0; tile < 32; tile++) {
                  if (scan >= 0) {
                      // Fetch tile & attrib data:
                      if (this.validTileData) {
                          // Get data from array:
                          t = scantile[tile];
                          if (typeof t === 'undefined') {
                              continue;
                          }
                          tpix = t.pix;
                          att = attrib[tile];
                      } else {
                          // Fetch data:
                          t =
                              ptTile[
                                  baseTile +
                                      nameTable[this.curNt].getTileIndex(this.cntHT, this.cntVT)
                              ];
                          if (typeof t === 'undefined') {
                              continue;
                          }
                          tpix = t.pix;
                          att = nameTable[this.curNt].getAttrib(this.cntHT, this.cntVT);
                          scantile[tile] = t;
                          attrib[tile] = att;
                      }
  
                      // Render tile scanline:
                      var sx = 0;
                      var x = (tile << 3) - this.regFH;
  
                      if (x > -8) {
                          if (x < 0) {
                              destIndex -= x;
                              sx = -x;
                          }
                          if (t.opaque[this.cntFV]) {
                              for (; sx < 8; sx++) {
                                  targetBuffer[destIndex] = imgPalette[tpix[tscanoffset + sx] + att];
                                  pixrendered[destIndex] |= 256;
                                  destIndex++;
                              }
                          } else {
                              for (; sx < 8; sx++) {
                                  col = tpix[tscanoffset + sx];
                                  if (col !== 0) {
                                      targetBuffer[destIndex] = imgPalette[col + att];
                                      pixrendered[destIndex] |= 256;
                                  }
                                  destIndex++;
                              }
                          }
                      }
                  }
  
                  // Increase Horizontal Tile Counter:
                  if (++this.cntHT === 32) {
                      this.cntHT = 0;
                      this.cntH++;
                      this.cntH %= 2;
                      this.curNt = this.ntable1[(this.cntV << 1) + this.cntH];
                  }
              }
  
              // Tile data for one row should now have been fetched,
              // so the data in the array is valid.
              this.validTileData = true;
          }
  
          // update vertical scroll:
          this.cntFV++;
          if (this.cntFV === 8) {
              this.cntFV = 0;
              this.cntVT++;
              if (this.cntVT === 30) {
                  this.cntVT = 0;
                  this.cntV++;
                  this.cntV %= 2;
                  this.curNt = this.ntable1[(this.cntV << 1) + this.cntH];
              } else if (this.cntVT === 32) {
                  this.cntVT = 0;
              }
  
              // Invalidate fetched data:
              this.validTileData = false;
          }
      },
  
      renderSpritesPartially: function (startscan, scancount, bgPri) {
          if (this.f_spVisibility === 1) {
              for (var i = 0; i < 64; i++) {
                  if (
                      this.bgPriority[i] === bgPri &&
                      this.sprX[i] >= 0 &&
                      this.sprX[i] < 256 &&
                      this.sprY[i] + 8 >= startscan &&
                      this.sprY[i] < startscan + scancount
                  ) {
                      // Show sprite.
                      if (this.f_spriteSize === 0) {
                          // 8x8 sprites
  
                          this.srcy1 = 0;
                          this.srcy2 = 8;
  
                          if (this.sprY[i] < startscan) {
                              this.srcy1 = startscan - this.sprY[i] - 1;
                          }
  
                          if (this.sprY[i] + 8 > startscan + scancount) {
                              this.srcy2 = startscan + scancount - this.sprY[i] + 1;
                          }
  
                          if (this.f_spPatternTable === 0) {
                              this.ptTile[this.sprTile[i]].render(
                                  this.buffer,
                                  0,
                                  this.srcy1,
                                  8,
                                  this.srcy2,
                                  this.sprX[i],
                                  this.sprY[i] + 1,
                                  this.sprCol[i],
                                  this.sprPalette,
                                  this.horiFlip[i],
                                  this.vertFlip[i],
                                  i,
                                  this.pixrendered
                              );
                          } else {
                              this.ptTile[this.sprTile[i] + 256].render(
                                  this.buffer,
                                  0,
                                  this.srcy1,
                                  8,
                                  this.srcy2,
                                  this.sprX[i],
                                  this.sprY[i] + 1,
                                  this.sprCol[i],
                                  this.sprPalette,
                                  this.horiFlip[i],
                                  this.vertFlip[i],
                                  i,
                                  this.pixrendered
                              );
                          }
                      } else {
                          // 8x16 sprites
                          var top = this.sprTile[i];
                          if ((top & 1) !== 0) {
                              top = this.sprTile[i] - 1 + 256;
                          }
  
                          var srcy1 = 0;
                          var srcy2 = 8;
  
                          if (this.sprY[i] < startscan) {
                              srcy1 = startscan - this.sprY[i] - 1;
                          }
  
                          if (this.sprY[i] + 8 > startscan + scancount) {
                              srcy2 = startscan + scancount - this.sprY[i];
                          }
  
                          this.ptTile[top + (this.vertFlip[i] ? 1 : 0)].render(
                              this.buffer,
                              0,
                              srcy1,
                              8,
                              srcy2,
                              this.sprX[i],
                              this.sprY[i] + 1,
                              this.sprCol[i],
                              this.sprPalette,
                              this.horiFlip[i],
                              this.vertFlip[i],
                              i,
                              this.pixrendered
                          );
  
                          srcy1 = 0;
                          srcy2 = 8;
  
                          if (this.sprY[i] + 8 < startscan) {
                              srcy1 = startscan - (this.sprY[i] + 8 + 1);
                          }
  
                          if (this.sprY[i] + 16 > startscan + scancount) {
                              srcy2 = startscan + scancount - (this.sprY[i] + 8);
                          }
  
                          this.ptTile[top + (this.vertFlip[i] ? 0 : 1)].render(
                              this.buffer,
                              0,
                              srcy1,
                              8,
                              srcy2,
                              this.sprX[i],
                              this.sprY[i] + 1 + 8,
                              this.sprCol[i],
                              this.sprPalette,
                              this.horiFlip[i],
                              this.vertFlip[i],
                              i,
                              this.pixrendered
                          );
                      }
                  }
              }
          }
      },
  
      checkSprite0: function (scan) {
          this.spr0HitX = -1;
          this.spr0HitY = -1;
  
          var toffset;
          var tIndexAdd = this.f_spPatternTable === 0 ? 0 : 256;
          var x, y, t, i;
          var bufferIndex;
  
          x = this.sprX[0];
          y = this.sprY[0] + 1;
  
          if (this.f_spriteSize === 0) {
              // 8x8 sprites.
  
              // Check range:
              if (y <= scan && y + 8 > scan && x >= -7 && x < 256) {
                  // Sprite is in range.
                  // Draw scanline:
                  t = this.ptTile[this.sprTile[0] + tIndexAdd];
  
                  if (this.vertFlip[0]) {
                      toffset = 7 - (scan - y);
                  } else {
                      toffset = scan - y;
                  }
                  toffset *= 8;
  
                  bufferIndex = scan * 256 + x;
                  if (this.horiFlip[0]) {
                      for (i = 7; i >= 0; i--) {
                          if (x >= 0 && x < 256) {
                              if (
                                  bufferIndex >= 0 &&
                                  bufferIndex < 61440 &&
                                  this.pixrendered[bufferIndex] !== 0
                              ) {
                                  if (t.pix[toffset + i] !== 0) {
                                      this.spr0HitX = bufferIndex % 256;
                                      this.spr0HitY = scan;
                                      return true;
                                  }
                              }
                          }
                          x++;
                          bufferIndex++;
                      }
                  } else {
                      for (i = 0; i < 8; i++) {
                          if (x >= 0 && x < 256) {
                              if (
                                  bufferIndex >= 0 &&
                                  bufferIndex < 61440 &&
                                  this.pixrendered[bufferIndex] !== 0
                              ) {
                                  if (t.pix[toffset + i] !== 0) {
                                      this.spr0HitX = bufferIndex % 256;
                                      this.spr0HitY = scan;
                                      return true;
                                  }
                              }
                          }
                          x++;
                          bufferIndex++;
                      }
                  }
              }
          } else {
              // 8x16 sprites:
  
              // Check range:
              if (y <= scan && y + 16 > scan && x >= -7 && x < 256) {
                  // Sprite is in range.
                  // Draw scanline:
  
                  if (this.vertFlip[0]) {
                      toffset = 15 - (scan - y);
                  } else {
                      toffset = scan - y;
                  }
  
                  if (toffset < 8) {
                      // first half of sprite.
                      t =
                          this.ptTile[
                              this.sprTile[0] +
                                  (this.vertFlip[0] ? 1 : 0) +
                                  ((this.sprTile[0] & 1) !== 0 ? 255 : 0)
                          ];
                  } else {
                      // second half of sprite.
                      t =
                          this.ptTile[
                              this.sprTile[0] +
                                  (this.vertFlip[0] ? 0 : 1) +
                                  ((this.sprTile[0] & 1) !== 0 ? 255 : 0)
                          ];
                      if (this.vertFlip[0]) {
                          toffset = 15 - toffset;
                      } else {
                          toffset -= 8;
                      }
                  }
                  toffset *= 8;
  
                  bufferIndex = scan * 256 + x;
                  if (this.horiFlip[0]) {
                      for (i = 7; i >= 0; i--) {
                          if (x >= 0 && x < 256) {
                              if (
                                  bufferIndex >= 0 &&
                                  bufferIndex < 61440 &&
                                  this.pixrendered[bufferIndex] !== 0
                              ) {
                                  if (t.pix[toffset + i] !== 0) {
                                      this.spr0HitX = bufferIndex % 256;
                                      this.spr0HitY = scan;
                                      return true;
                                  }
                              }
                          }
                          x++;
                          bufferIndex++;
                      }
                  } else {
                      for (i = 0; i < 8; i++) {
                          if (x >= 0 && x < 256) {
                              if (
                                  bufferIndex >= 0 &&
                                  bufferIndex < 61440 &&
                                  this.pixrendered[bufferIndex] !== 0
                              ) {
                                  if (t.pix[toffset + i] !== 0) {
                                      this.spr0HitX = bufferIndex % 256;
                                      this.spr0HitY = scan;
                                      return true;
                                  }
                              }
                          }
                          x++;
                          bufferIndex++;
                      }
                  }
              }
          }
  
          return false;
      },
  
      // This will write to PPU memory, and
      // update internally buffered data
      // appropriately.
      writeMem: function (address, value) {
          this.vramMem[address] = value;
  
          // Update internally buffered data:
          if (address < 0x2000) {
              this.vramMem[address] = value;
              this.patternWrite(address, value);
          } else if (address >= 0x2000 && address < 0x23c0) {
              this.nameTableWrite(this.ntable1[0], address - 0x2000, value);
          } else if (address >= 0x23c0 && address < 0x2400) {
              this.attribTableWrite(this.ntable1[0], address - 0x23c0, value);
          } else if (address >= 0x2400 && address < 0x27c0) {
              this.nameTableWrite(this.ntable1[1], address - 0x2400, value);
          } else if (address >= 0x27c0 && address < 0x2800) {
              this.attribTableWrite(this.ntable1[1], address - 0x27c0, value);
          } else if (address >= 0x2800 && address < 0x2bc0) {
              this.nameTableWrite(this.ntable1[2], address - 0x2800, value);
          } else if (address >= 0x2bc0 && address < 0x2c00) {
              this.attribTableWrite(this.ntable1[2], address - 0x2bc0, value);
          } else if (address >= 0x2c00 && address < 0x2fc0) {
              this.nameTableWrite(this.ntable1[3], address - 0x2c00, value);
          } else if (address >= 0x2fc0 && address < 0x3000) {
              this.attribTableWrite(this.ntable1[3], address - 0x2fc0, value);
          } else if (address >= 0x3f00 && address < 0x3f20) {
              this.updatePalettes();
          }
      },
  
      // Reads data from $3f00 to $f20
      // into the two buffered palettes.
      updatePalettes: function () {
          var i;
  
          for (i = 0; i < 16; i++) {
              if (this.f_dispType === 0) {
                  this.imgPalette[i] = this.palTable.getEntry(this.vramMem[0x3f00 + i] & 63);
              } else {
                  this.imgPalette[i] = this.palTable.getEntry(this.vramMem[0x3f00 + i] & 32);
              }
          }
          for (i = 0; i < 16; i++) {
              if (this.f_dispType === 0) {
                  this.sprPalette[i] = this.palTable.getEntry(this.vramMem[0x3f10 + i] & 63);
              } else {
                  this.sprPalette[i] = this.palTable.getEntry(this.vramMem[0x3f10 + i] & 32);
              }
          }
      },
  
      // Updates the internal pattern
      // table buffers with this new byte.
      // In vNES, there is a version of this with 4 arguments which isn't used.
      patternWrite: function (address, value) {
          var tileIndex = Math.floor(address / 16);
          var leftOver = address % 16;
          if (leftOver < 8) {
              this.ptTile[tileIndex].setScanline(leftOver, value, this.vramMem[address + 8]);
          } else {
              this.ptTile[tileIndex].setScanline(leftOver - 8, this.vramMem[address - 8], value);
          }
      },
  
      // Updates the internal name table buffers
      // with this new byte.
      nameTableWrite: function (index, address, value) {
          this.nameTable[index].tile[address] = value;
  
          // Update Sprite #0 hit:
          //updateSpr0Hit();
          this.checkSprite0(this.scanline - 20);
      },
  
      // Updates the internal pattern
      // table buffers with this new attribute
      // table byte.
      attribTableWrite: function (index, address, value) {
          this.nameTable[index].writeAttrib(address, value);
      },
  
      // Updates the internally buffered sprite
      // data with this new byte of info.
      spriteRamWriteUpdate: function (address, value) {
          var tIndex = Math.floor(address / 4);
  
          if (tIndex === 0) {
              //updateSpr0Hit();
              this.checkSprite0(this.scanline - 20);
          }
  
          if (address % 4 === 0) {
              // Y coordinate
              this.sprY[tIndex] = value;
          } else if (address % 4 === 1) {
              // Tile index
              this.sprTile[tIndex] = value;
          } else if (address % 4 === 2) {
              // Attributes
              this.vertFlip[tIndex] = (value & 0x80) !== 0;
              this.horiFlip[tIndex] = (value & 0x40) !== 0;
              this.bgPriority[tIndex] = (value & 0x20) !== 0;
              this.sprCol[tIndex] = (value & 3) << 2;
          } else if (address % 4 === 3) {
              // X coordinate
              this.sprX[tIndex] = value;
          }
      },
  
      doNMI: function () {
          // Set VBlank flag:
          this.setStatusFlag(this.STATUS_VBLANK, true);
          //nes.getCpu().doNonMaskableInterrupt();
          this.nes.cpu.requestIrq(this.nes.cpu.IRQ_NMI);
      },
  
      isPixelWhite: function (x, y) {
          this.triggerRendering();
          return this.nes.ppu.buffer[(y << 8) + x] === 0xffffff;
      },
  
      JSON_PROPERTIES: [
          // Memory
          'vramMem',
          'spriteMem',
          // Counters
          'cntFV',
          'cntV',
          'cntH',
          'cntVT',
          'cntHT',
          // Registers
          'regFV',
          'regV',
          'regH',
          'regVT',
          'regHT',
          'regFH',
          'regS',
          // VRAM addr
          'vramAddress',
          'vramTmpAddress',
          // Control/Status registers
          'f_nmiOnVblank',
          'f_spriteSize',
          'f_bgPatternTable',
          'f_spPatternTable',
          'f_addrInc',
          'f_nTblAddress',
          'f_color',
          'f_spVisibility',
          'f_bgVisibility',
          'f_spClipping',
          'f_bgClipping',
          'f_dispType',
          // VRAM I/O
          'vramBufferedReadValue',
          'firstWrite',
          // Mirroring
          'currentMirroring',
          'vramMirrorTable',
          'ntable1',
          // SPR-RAM I/O
          'sramAddress',
          // Sprites. Most sprite data is rebuilt from spriteMem
          'hitSpr0',
          // Palettes
          'sprPalette',
          'imgPalette',
          // Rendering progression
          'curX',
          'scanline',
          'lastRenderedScanline',
          'curNt',
          'scantile',
          // Used during rendering
          'attrib',
          'buffer',
          'bgbuffer',
          'pixrendered',
          // Misc
          'requestEndFrame',
          'nmiOk',
          'dummyCycleToggle',
          'nmiCounter',
          'validTileData',
          'scanlineAlreadyRendered',
      ],
  
      toJSON: function () {
          var i;
          var state = utils.toJSON(this);
  
          state.nameTable = [];
          for (i = 0; i < this.nameTable.length; i++) {
              state.nameTable[i] = this.nameTable[i].toJSON();
          }
  
          state.ptTile = [];
          for (i = 0; i < this.ptTile.length; i++) {
              state.ptTile[i] = this.ptTile[i].toJSON();
          }
  
          return state;
      },
  
      fromJSON: function (state) {
          var i;
  
          utils.fromJSON(this, state);
  
          for (i = 0; i < this.nameTable.length; i++) {
              this.nameTable[i].fromJSON(state.nameTable[i]);
          }
  
          for (i = 0; i < this.ptTile.length; i++) {
              this.ptTile[i].fromJSON(state.ptTile[i]);
          }
  
          // Sprite data:
          for (i = 0; i < this.spriteMem.length; i++) {
              this.spriteRamWriteUpdate(i, this.spriteMem[i]);
          }
      },
  };
  
  var NameTable = function (width, height, name) {
      this.width = width;
      this.height = height;
      this.name = name;
  
      this.tile = new Array(width * height);
      this.attrib = new Array(width * height);
      for (var i = 0; i < width * height; i++) {
          this.tile[i] = 0;
          this.attrib[i] = 0;
      }
  };
  
  NameTable.prototype = {
      getTileIndex: function (x, y) {
          return this.tile[y * this.width + x];
      },
  
      getAttrib: function (x, y) {
          return this.attrib[y * this.width + x];
      },
  
      writeAttrib: function (index, value) {
          var basex = (index % 8) * 4;
          var basey = Math.floor(index / 8) * 4;
          var add;
          var tx, ty;
          var attindex;
  
          for (var sqy = 0; sqy < 2; sqy++) {
              for (var sqx = 0; sqx < 2; sqx++) {
                  add = (value >> (2 * (sqy * 2 + sqx))) & 3;
                  for (var y = 0; y < 2; y++) {
                      for (var x = 0; x < 2; x++) {
                          tx = basex + sqx * 2 + x;
                          ty = basey + sqy * 2 + y;
                          attindex = ty * this.width + tx;
                          this.attrib[attindex] = (add << 2) & 12;
                      }
                  }
              }
          }
      },
  
      toJSON: function () {
          return {
              tile: this.tile,
              attrib: this.attrib,
          };
      },
  
      fromJSON: function (s) {
          this.tile = s.tile;
          this.attrib = s.attrib;
      },
  };
  
  var PaletteTable = function () {
      this.curTable = new Array(64);
      this.emphTable = new Array(8);
      this.currentEmph = -1;
  };
  
  PaletteTable.prototype = {
      reset: function () {
          this.setEmphasis(0);
      },
  
      loadNTSCPalette: function () {
          // prettier-ignore
          this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000];
          this.makeTables();
          this.setEmphasis(0);
      },
  
      loadPALPalette: function () {
          // prettier-ignore
          this.curTable = [0x525252, 0xB40000, 0xA00000, 0xB1003D, 0x740069, 0x00005B, 0x00005F, 0x001840, 0x002F10, 0x084A08, 0x006700, 0x124200, 0x6D2800, 0x000000, 0x000000, 0x000000, 0xC4D5E7, 0xFF4000, 0xDC0E22, 0xFF476B, 0xD7009F, 0x680AD7, 0x0019BC, 0x0054B1, 0x006A5B, 0x008C03, 0x00AB00, 0x2C8800, 0xA47200, 0x000000, 0x000000, 0x000000, 0xF8F8F8, 0xFFAB3C, 0xFF7981, 0xFF5BC5, 0xFF48F2, 0xDF49FF, 0x476DFF, 0x00B4F7, 0x00E0FF, 0x00E375, 0x03F42B, 0x78B82E, 0xE5E218, 0x787878, 0x000000, 0x000000, 0xFFFFFF, 0xFFF2BE, 0xF8B8B8, 0xF8B8D8, 0xFFB6FF, 0xFFC3FF, 0xC7D1FF, 0x9ADAFF, 0x88EDF8, 0x83FFDD, 0xB8F8B8, 0xF5F8AC, 0xFFFFB0, 0xF8D8F8, 0x000000, 0x000000];
          this.makeTables();
          this.setEmphasis(0);
      },
  
      makeTables: function () {
          var r, g, b, col, i, rFactor, gFactor, bFactor;
  
          // Calculate a table for each possible emphasis setting:
          for (var emph = 0; emph < 8; emph++) {
              // Determine color component factors:
              rFactor = 1.0;
              gFactor = 1.0;
              bFactor = 1.0;
  
              if ((emph & 1) !== 0) {
                  rFactor = 0.75;
                  bFactor = 0.75;
              }
              if ((emph & 2) !== 0) {
                  rFactor = 0.75;
                  gFactor = 0.75;
              }
              if ((emph & 4) !== 0) {
                  gFactor = 0.75;
                  bFactor = 0.75;
              }
  
              this.emphTable[emph] = new Array(64);
  
              // Calculate table:
              for (i = 0; i < 64; i++) {
                  col = this.curTable[i];
                  r = Math.floor(this.getRed(col) * rFactor);
                  g = Math.floor(this.getGreen(col) * gFactor);
                  b = Math.floor(this.getBlue(col) * bFactor);
                  this.emphTable[emph][i] = this.getRgb(r, g, b);
              }
          }
      },
  
      setEmphasis: function (emph) {
          if (emph !== this.currentEmph) {
              this.currentEmph = emph;
              for (var i = 0; i < 64; i++) {
                  this.curTable[i] = this.emphTable[emph][i];
              }
          }
      },
  
      getEntry: function (yiq) {
          return this.curTable[yiq];
      },
  
      getRed: function (rgb) {
          return (rgb >> 16) & 0xff;
      },
  
      getGreen: function (rgb) {
          return (rgb >> 8) & 0xff;
      },
  
      getBlue: function (rgb) {
          return rgb & 0xff;
      },
  
      getRgb: function (r, g, b) {
          return (r << 16) | (g << 8) | b;
      },
  
      loadDefaultPalette: function () {
          this.curTable[0] = this.getRgb(117, 117, 117);
          this.curTable[1] = this.getRgb(39, 27, 143);
          this.curTable[2] = this.getRgb(0, 0, 171);
          this.curTable[3] = this.getRgb(71, 0, 159);
          this.curTable[4] = this.getRgb(143, 0, 119);
          this.curTable[5] = this.getRgb(171, 0, 19);
          this.curTable[6] = this.getRgb(167, 0, 0);
          this.curTable[7] = this.getRgb(127, 11, 0);
          this.curTable[8] = this.getRgb(67, 47, 0);
          this.curTable[9] = this.getRgb(0, 71, 0);
          this.curTable[10] = this.getRgb(0, 81, 0);
          this.curTable[11] = this.getRgb(0, 63, 23);
          this.curTable[12] = this.getRgb(27, 63, 95);
          this.curTable[13] = this.getRgb(0, 0, 0);
          this.curTable[14] = this.getRgb(0, 0, 0);
          this.curTable[15] = this.getRgb(0, 0, 0);
          this.curTable[16] = this.getRgb(188, 188, 188);
          this.curTable[17] = this.getRgb(0, 115, 239);
          this.curTable[18] = this.getRgb(35, 59, 239);
          this.curTable[19] = this.getRgb(131, 0, 243);
          this.curTable[20] = this.getRgb(191, 0, 191);
          this.curTable[21] = this.getRgb(231, 0, 91);
          this.curTable[22] = this.getRgb(219, 43, 0);
          this.curTable[23] = this.getRgb(203, 79, 15);
          this.curTable[24] = this.getRgb(139, 115, 0);
          this.curTable[25] = this.getRgb(0, 151, 0);
          this.curTable[26] = this.getRgb(0, 171, 0);
          this.curTable[27] = this.getRgb(0, 147, 59);
          this.curTable[28] = this.getRgb(0, 131, 139);
          this.curTable[29] = this.getRgb(0, 0, 0);
          this.curTable[30] = this.getRgb(0, 0, 0);
          this.curTable[31] = this.getRgb(0, 0, 0);
          this.curTable[32] = this.getRgb(255, 255, 255);
          this.curTable[33] = this.getRgb(63, 191, 255);
          this.curTable[34] = this.getRgb(95, 151, 255);
          this.curTable[35] = this.getRgb(167, 139, 253);
          this.curTable[36] = this.getRgb(247, 123, 255);
          this.curTable[37] = this.getRgb(255, 119, 183);
          this.curTable[38] = this.getRgb(255, 119, 99);
          this.curTable[39] = this.getRgb(255, 155, 59);
          this.curTable[40] = this.getRgb(243, 191, 63);
          this.curTable[41] = this.getRgb(131, 211, 19);
          this.curTable[42] = this.getRgb(79, 223, 75);
          this.curTable[43] = this.getRgb(88, 248, 152);
          this.curTable[44] = this.getRgb(0, 235, 219);
          this.curTable[45] = this.getRgb(0, 0, 0);
          this.curTable[46] = this.getRgb(0, 0, 0);
          this.curTable[47] = this.getRgb(0, 0, 0);
          this.curTable[48] = this.getRgb(255, 255, 255);
          this.curTable[49] = this.getRgb(171, 231, 255);
          this.curTable[50] = this.getRgb(199, 215, 255);
          this.curTable[51] = this.getRgb(215, 203, 255);
          this.curTable[52] = this.getRgb(255, 199, 255);
          this.curTable[53] = this.getRgb(255, 199, 219);
          this.curTable[54] = this.getRgb(255, 191, 179);
          this.curTable[55] = this.getRgb(255, 219, 171);
          this.curTable[56] = this.getRgb(255, 231, 163);
          this.curTable[57] = this.getRgb(227, 255, 163);
          this.curTable[58] = this.getRgb(171, 243, 191);
          this.curTable[59] = this.getRgb(179, 255, 207);
          this.curTable[60] = this.getRgb(159, 255, 243);
          this.curTable[61] = this.getRgb(0, 0, 0);
          this.curTable[62] = this.getRgb(0, 0, 0);
          this.curTable[63] = this.getRgb(0, 0, 0);
  
          this.makeTables();
          this.setEmphasis(0);
      },
  };
  
  export { PPU };
  