struct MMC5 : Chip {
  MMC5(Board& board) : Chip(board) {
    revision = Revision::MMC5;
  }

  auto main() -> void {
    //scanline() resets this; if no scanlines detected, enter video blanking period
    if(++cpuCycleCounter >= 200) blank();  //113-114 normal; ~2500 across Vblank period

    cpu.irqLine(irqEnable && irqPending);
    tick();
  }

  auto scanline(uint y) -> void {
  //used for testing only, to verify MMC5 scanline detection is accurate:
  //if(y != vcounter && y <= 240) print(y, " vs ", vcounter, "\n");
  }

  auto accessPRG(bool write, uint addr, uint8 data = 0x00) -> uint8 {
    uint bank;

    if((addr & 0xe000) == 0x6000) {
      bank = (ramSelect << 2) | ramBank;
      addr &= 0x1fff;
    } else if(prgMode == 0) {
      bank = prgBank[3] & ~3;
      addr &= 0x7fff;
    } else if(prgMode == 1) {
      if((addr & 0xc000) == 0x8000) bank = (prgBank[1] & ~1);
      if((addr & 0xe000) == 0xc000) bank = (prgBank[3] & ~1);
      addr &= 0x3fff;
    } else if(prgMode == 2) {
      if((addr & 0xe000) == 0x8000) bank = (prgBank[1] & ~1) | 0;
      if((addr & 0xe000) == 0xa000) bank = (prgBank[1] & ~1) | 1;
      if((addr & 0xe000) == 0xc000) bank = (prgBank[2]);
      if((addr & 0xe000) == 0xe000) bank = (prgBank[3]);
      addr &= 0x1fff;
    } else if(prgMode == 3) {
      if((addr & 0xe000) == 0x8000) bank = prgBank[0];
      if((addr & 0xe000) == 0xa000) bank = prgBank[1];
      if((addr & 0xe000) == 0xc000) bank = prgBank[2];
      if((addr & 0xe000) == 0xe000) bank = prgBank[3];
      addr &= 0x1fff;
    }

    bool rom = bank & 0x80;
    bank &= 0x7f;

    if(write == false) {
      if(rom) {
        return board.prgrom.read((bank << 13) | addr);
      } else {
        return board.prgram.read((bank << 13) | addr);
      }
    } else {
      if(rom) {
        board.prgrom.write((bank << 13) | addr, data);
      } else {
        if(prgramWriteProtect[0] == 2 && prgramWriteProtect[1] == 1) {
          board.prgram.write((bank << 13) | addr, data);
        }
      }
      return 0x00;
    }
  }

  auto readPRG(uint addr) -> uint8 {
    if((addr & 0xfc00) == 0x5c00) {
      if(exramMode >= 2) return exram[addr & 0x03ff];
      return cpu.mdr();
    }

    if(addr >= 0x6000) {
      return accessPRG(0, addr);
    }

    switch(addr) {
    case 0x5204: {
      uint8 result = (irqPending << 7) | (inFrame << 6);
      irqPending = false;
      return result;
    }
    case 0x5205: return (multiplier * multiplicand) >> 0;
    case 0x5206: return (multiplier * multiplicand) >> 8;
    }

    return 0x00;
  }

  auto writePRG(uint addr, uint8 data) -> void {
    if((addr & 0xfc00) == 0x5c00) {
      //writes 0x00 *during* Vblank (not during screen rendering ...)
      if(exramMode == 0 || exramMode == 1) exram[addr & 0x03ff] = inFrame ? data : (uint8)0x00;
      if(exramMode == 2) exram[addr & 0x03ff] = data;
      return;
    }

    if(addr >= 0x6000) {
      accessPRG(1, addr, data);
      return;
    }

    switch(addr) {
    case 0x2000:
      sprite8x16 = data & 0x20;
      break;

    case 0x2001:
      //if BG+sprites are disabled; enter video blanking period
      if((data & 0x18) == 0) blank();
      break;

    case 0x5100: prgMode = data & 3; break;
    case 0x5101: chrMode = data & 3; break;

    case 0x5102: prgramWriteProtect[0] = data & 3; break;
    case 0x5103: prgramWriteProtect[1] = data & 3; break;

    case 0x5104:
      exramMode = data & 3;
      break;

    case 0x5105:
      nametableMode[0] = (data & 0x03) >> 0;
      nametableMode[1] = (data & 0x0c) >> 2;
      nametableMode[2] = (data & 0x30) >> 4;
      nametableMode[3] = (data & 0xc0) >> 6;
      break;

    case 0x5106:
      fillmodeTile = data;
      break;

    case 0x5107:
      fillmodeColor = data & 3;
      fillmodeColor |= fillmodeColor << 2;
      fillmodeColor |= fillmodeColor << 4;
      break;

    case 0x5113:
      ramSelect = data & 0x04;
      ramBank = data & 0x03;
      break;

    case 0x5114: prgBank[0] = data; break;
    case 0x5115: prgBank[1] = data; break;
    case 0x5116: prgBank[2] = data; break;
    case 0x5117: prgBank[3] = data | 0x80; break;

    case 0x5120: chrSpriteBank[0] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5121: chrSpriteBank[1] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5122: chrSpriteBank[2] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5123: chrSpriteBank[3] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5124: chrSpriteBank[4] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5125: chrSpriteBank[5] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5126: chrSpriteBank[6] = (chrBankHi << 8) | data; chrActive = 0; break;
    case 0x5127: chrSpriteBank[7] = (chrBankHi << 8) | data; chrActive = 0; break;

    case 0x5128: chrBGBank[0] = (chrBankHi << 8) | data; chrActive = 1; break;
    case 0x5129: chrBGBank[1] = (chrBankHi << 8) | data; chrActive = 1; break;
    case 0x512a: chrBGBank[2] = (chrBankHi << 8) | data; chrActive = 1; break;
    case 0x512b: chrBGBank[3] = (chrBankHi << 8) | data; chrActive = 1; break;

    case 0x5130:
      chrBankHi = data & 3;
      break;

    case 0x5200:
      vsEnable = data & 0x80;
      vsSide = data & 0x40;
      vsTile = data & 0x1f;
      break;

    case 0x5201:
      vsScroll = data;
      break;

    case 0x5202:
      vsBank = data;
      break;

    case 0x5203:
      irqLine = data;
      break;

    case 0x5204:
      irqEnable = data & 0x80;
      break;

    case 0x5205:
      multiplicand = data;
      break;

    case 0x5206:
      multiplier = data;
      break;
    }
  }

  auto chrSpriteAddr(uint addr) -> uint {
    if(chrMode == 0) {
      auto bank = chrSpriteBank[7];
      return (bank * 0x2000) + (addr & 0x1fff);
    }

    if(chrMode == 1) {
      auto bank = chrSpriteBank[(addr / 0x1000) * 4 + 3];
      return (bank * 0x1000) + (addr & 0x0fff);
    }

    if(chrMode == 2) {
      auto bank = chrSpriteBank[(addr / 0x0800) * 2 + 1];
      return (bank * 0x0800) + (addr & 0x07ff);
    }

    if(chrMode == 3) {
      auto bank = chrSpriteBank[(addr / 0x0400)];
      return (bank * 0x0400) + (addr & 0x03ff);
    }

    unreachable;
  }

  auto chrBGAddr(uint addr) -> uint {
    addr &= 0x0fff;

    if(chrMode == 0) {
      auto bank = chrBGBank[3];
      return (bank * 0x2000) + (addr & 0x0fff);
    }

    if(chrMode == 1) {
      auto bank = chrBGBank[3];
      return (bank * 0x1000) + (addr & 0x0fff);
    }

    if(chrMode == 2) {
      auto bank = chrBGBank[(addr / 0x0800) * 2 + 1];
      return (bank * 0x0800) + (addr & 0x07ff);
    }

    if(chrMode == 3) {
      auto bank = chrBGBank[(addr / 0x0400)];
      return (bank * 0x0400) + (addr & 0x03ff);
    }

    unreachable;
  }

  auto chrVSAddr(uint addr) -> uint {
    return (vsBank * 0x1000) + (addr & 0x0ff8) + (vsVpos & 7);
  }

  auto blank() -> void {
    inFrame = false;
  }

  auto scanline() -> void {
    hcounter = 0;

    if(inFrame == false) {
      inFrame = true;
      irqPending = false;
      vcounter = 0;
    } else {
      if(vcounter == irqLine) irqPending = true;
      vcounter++;
    }

    cpuCycleCounter = 0;
  }

  auto readCIRAM(uint addr) -> uint8 {
    if(vsFetch && (hcounter & 2) == 0) return exram[vsVpos / 8 * 32 + vsHpos / 8];
    if(vsFetch && (hcounter & 2) != 0) return exram[vsVpos / 32 * 8 + vsHpos / 32 + 0x03c0];

    switch(nametableMode[(addr >> 10) & 3]) {
    case 0: return ppu.readCIRAM(0x0000 | (addr & 0x03ff));
    case 1: return ppu.readCIRAM(0x0400 | (addr & 0x03ff));
    case 2: return exramMode < 2 ? exram[addr & 0x03ff] : (uint8)0x00;
    case 3: return (hcounter & 2) == 0 ? fillmodeTile : fillmodeColor;
    }
    unreachable;
  }

  auto readCHR(uint addr) -> uint8 {
    chrAccess[0] = chrAccess[1];
    chrAccess[1] = chrAccess[2];
    chrAccess[2] = chrAccess[3];
    chrAccess[3] = addr;

    //detect two unused nametable fetches at end of each scanline
    if((chrAccess[0] & 0x2000) == 0
    && (chrAccess[1] & 0x2000)
    && (chrAccess[2] & 0x2000)
    && (chrAccess[3] & 0x2000)) scanline();

    if(inFrame == false) {
      vsFetch = false;
      if(addr & 0x2000) return readCIRAM(addr);
      return board.chrrom.read(chrActive ? chrBGAddr(addr) : chrSpriteAddr(addr));
    }

    bool bgFetch = (hcounter < 256 || hcounter >= 320);
    uint8 result = 0x00;

    if((hcounter & 7) == 0) {
      vsHpos  = hcounter >= 320 ? hcounter - 320 : hcounter + 16;
      vsVpos  = vcounter + vsScroll;
      vsFetch = vsEnable && bgFetch && exramMode < 2
      && (vsSide ? vsHpos / 8 >= vsTile : vsHpos / 8 < vsTile);
      if(vsVpos >= 240) vsVpos -= 240;

      result = readCIRAM(addr);

      exbank = (chrBankHi << 6) | (exram[addr & 0x03ff] & 0x3f);
      exattr = exram[addr & 0x03ff] >> 6;
      exattr |= exattr << 2;
      exattr |= exattr << 4;
    } else if((hcounter & 7) == 2) {
      result = readCIRAM(addr);
      if(bgFetch && exramMode == 1) result = exattr;
    } else {
      if(vsFetch) result = board.chrrom.read(chrVSAddr(addr));
      else if(sprite8x16 ? bgFetch : chrActive) result = board.chrrom.read(chrBGAddr(addr));
      else result = board.chrrom.read(chrSpriteAddr(addr));
      if(bgFetch && exramMode == 1) result = board.chrrom.read(exbank * 0x1000 + (addr & 0x0fff));
    }

    hcounter += 2;
    return result;
  }

  auto writeCHR(uint addr, uint8 data) -> void {
    if(addr & 0x2000) {
      switch(nametableMode[(addr >> 10) & 3]) {
      case 0: return ppu.writeCIRAM(0x0000 | (addr & 0x03ff), data);
      case 1: return ppu.writeCIRAM(0x0400 | (addr & 0x03ff), data);
      case 2: exram[addr & 0x03ff] = data; break;
      }
    }
  }

  auto power() -> void {
    for(auto& n : exram) n = 0xff;

    prgMode = 3;
    chrMode = 0;
    for(auto& n : prgramWriteProtect) n = 0;
    exramMode = 0;
    for(auto& n : nametableMode) n = 0;
    fillmodeTile = 0;
    fillmodeColor = 0;
    ramSelect = 0;
    ramBank = 0;
    prgBank[0] = 0x00;
    prgBank[1] = 0x00;
    prgBank[2] = 0x00;
    prgBank[3] = 0xff;
    for(auto& n : chrSpriteBank) n = 0;
    for(auto& n : chrBGBank) n = 0;
    chrBankHi = 0;
    vsEnable = 0;
    vsSide = 0;
    vsTile = 0;
    vsScroll = 0;
    vsBank = 0;
    irqLine = 0;
    irqEnable = 0;
    multiplicand = 0;
    multiplier = 0;

    cpuCycleCounter = 0;
    irqCounter = 0;
    irqPending = 0;
    inFrame = 0;
    vcounter = 0;
    hcounter = 0;
    for(auto& n : chrAccess) n = 0;
    chrActive = 0;
    sprite8x16 = 0;

    exbank = 0;
    exattr = 0;

    vsFetch = 0;
    vsVpos = 0;
    vsHpos = 0;
  }

  auto serialize(serializer& s) -> void {
    s.array(exram);

    s.integer(prgMode);
    s.integer(chrMode);
    for(auto& n : prgramWriteProtect) s.integer(n);
    s.integer(exramMode);
    for(auto& n : nametableMode) s.integer(n);
    s.integer(fillmodeTile);
    s.integer(fillmodeColor);
    s.integer(ramSelect);
    s.integer(ramBank);
    for(auto& n : prgBank) s.integer(n);
    for(auto& n : chrSpriteBank) s.integer(n);
    for(auto& n : chrBGBank) s.integer(n);
    s.integer(chrBankHi);
    s.integer(vsEnable);
    s.integer(vsSide);
    s.integer(vsTile);
    s.integer(vsScroll);
    s.integer(vsBank);
    s.integer(irqLine);
    s.integer(irqEnable);
    s.integer(multiplicand);
    s.integer(multiplier);

    s.integer(cpuCycleCounter);
    s.integer(irqCounter);
    s.integer(irqPending);
    s.integer(inFrame);

    s.integer(vcounter);
    s.integer(hcounter);
    for(auto& n : chrAccess) s.integer(n);
    s.integer(chrActive);
    s.integer(sprite8x16);

    s.integer(exbank);
    s.integer(exattr);

    s.integer(vsFetch);
    s.integer(vsVpos);
    s.integer(vsHpos);
  }

  enum class Revision : uint {
    MMC5,
    MMC5B,
  } revision;

  uint8 exram[1024];

  //programmable registers

  uint2 prgMode;  //$5100
  uint2 chrMode;  //$5101

  uint2 prgramWriteProtect[2];  //$5102,$5103

  uint2 exramMode;         //$5104
  uint2 nametableMode[4];  //$5105
  uint8 fillmodeTile;      //$5106
  uint8 fillmodeColor;     //$5107

  bool ramSelect;           //$5113
  uint2 ramBank;            //$5113
  uint8 prgBank[4];         //$5114-5117
  uint10 chrSpriteBank[8];  //$5120-5127
  uint10 chrBGBank[4];      //$5128-512b
  uint2 chrBankHi;          //$5130

  bool vsEnable;      //$5200
  bool vsSide;        //$5200
  uint5 vsTile;       //$5200
  uint8 vsScroll;     //$5201
  uint8 vsBank;       //$5202

  uint8 irqLine;      //$5203
  bool irqEnable;     //$5204

  uint8 multiplicand;  //$5205
  uint8 multiplier;    //$5206

  //status registers

  uint cpuCycleCounter;
  uint irqCounter;
  bool irqPending;
  bool inFrame;

  uint vcounter;
  uint hcounter;
  uint16 chrAccess[4];
  bool chrActive;
  bool sprite8x16;

  uint8 exbank;
  uint8 exattr;

  bool vsFetch;
  uint8 vsVpos;
  uint8 vsHpos;
};
