//! Mapper 187 – 卡聖 (Kǎshèng) A98402 / MMC3 variant with PRG override and CHR A18
//!
//! Specifications:
//! - Primary source: NESdev Wiki <https://www.nesdev.org/wiki/INES_Mapper_187>
//! - Reference impl: Mesen2 `Core/NES/Mappers/Mmc3Variants/MMC3_187.h`
//!
//! Known Limitations:
//! - No known gameplay-blocking functional limitations are currently documented.
//!
//! ## Overview
//!
//! Mapper 187 is an MMC3-clone-bearing board used by Kǎshèng/Hummer Team games such
//! as *Street Fighter Zero 2* and *The King of Fighters '96*.  It extends the standard
//! MMC3 in two ways:
//!
//! 1. **NROM override register** at `$5000` (mask `$F001`) that can replace the MMC3
//!    PRG-ROM bank selection with a direct 16/32 KiB NROM-style mapping.
//! 2. **CHR A18 control** embedded in the MMC3 bank-select register at `$8000–$9FFF`
//!    (even addresses), which selects which 256 KiB half of the 512 KiB CHR-ROM is
//!    used for the background or sprite tiles.
//!
//! ## Registers
//!
//! ### NROM Override Register – `$5000` (mask `$F001`)
//!
//! ```text
//! D~7654 3210
//!   ---------
//!   M.NB BBb.
//!   | |+-+++-- BBBb (bits 4:1): 16 KiB PRG-ROM bank number
//!   | +------- N (bit 5): 0 = NROM-128 (16 KiB at both halves)
//!   |                     1 = NROM-256 (32 KiB; replace bank bit 1 with CPU A14)
//!   +--------- M (bit 7): 0 = use MMC3 PRG bank; 1 = use this register
//! ```
//!
//! ### Protection Read – `$5000–$5FFF`
//!
//! Reads return a byte with bit 7 set (`0x83`).
//!
//! ### CHR A18 Control – `$8000–$9FFF` (even, mask `$E001`)
//!
//! ```text
//! D~7654 3210
//!   ---------
//!   M... ....
//!   +--------- M (bit 7): CHR A18 mode
//!              0: CHR A18 = inverted PPU A12 (sprites from upper 256 KiB)
//!              1: CHR A18 = PPU A12 (backgrounds from upper 256 KiB)
//! ```
//!
//! Setting bit 7 here also activates the MMC3's CHR A12 inversion.
//!
//! ### MMC3-compatible registers – `$8000–$FFFF`
//!
//! All standard MMC3 register writes are forwarded unchanged.
//!
//! ## PRG Banking
//!
//! When M=0: normal MMC3 PRG banking (pages masked to `0x3F`).
//!
//! When M=1, with `bank_16k = (reg & 0x1E) >> 1`:
//! - N=0 (NROM-128): 16 KiB bank `bank_16k` at both `$8000–$BFFF` and `$C000–$FFFF`.
//! - N=1 (NROM-256): CPU A14 replaces bit 1 of `bank_16k`; effectively a 32 KiB
//!   window where the lower half is `bank_16k & !2` and the upper half is
//!   `(bank_16k & !2) | 2`.
//!
//! ## CHR Banking
//!
//! The CHR A18 bit is OR'd into bit 8 of the 1 KiB CHR page number, allowing
//! access to both 256 KiB halves of the 512 KiB CHR-ROM.  PPU A12 (or its inverse)
//! determines whether A18 is set.

use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::mmc3::MMC3Mapper;
use crate::nes::cartridge::{Mapper, MapperCapabilities};

const MAPPER_NUMBER: u16 = 187;
const PRG_BANK_SIZE: usize = 0x2000; // 8 KiB
const PRG_BANK_MASK: usize = PRG_BANK_SIZE - 1;
const CHR_1K_BANK_SIZE: usize = 0x0400;
const CHR_BANK_MASK: usize = CHR_1K_BANK_SIZE - 1;

/// Mapper 187 – Kǎshèng A98402 / MMC3 variant.
///
/// See the module-level documentation for hardware details.
pub struct Mapper187 {
    mmc3: MMC3Mapper,
    /// NROM override register written to `$5000` (mask `$F001`).
    prg_reg: u8,
    /// CHR A18 mode bit extracted from `$8000` writes.
    /// - 0: CHR A18 = inverted PPU A12
    /// - 1: CHR A18 = PPU A12
    chr_a18_mode: u8,
}

impl Mapper187 {
    pub fn new(ctx: crate::nes::cartridge::mapper::MapperContext) -> Self {
        let prg_rom = ctx.prg_rom;
        let chr_rom = ctx.chr_rom;
        let mirroring = ctx.mirroring;
        Self {
            mmc3: MMC3Mapper::new_with_irq_mode(prg_rom, chr_rom, mirroring, false),
            prg_reg: 0,
            chr_a18_mode: 0,
        }
    }

    /// Return the effective 8 KiB PRG page number for `addr`.
    ///
    /// When M=0 (bit 7 of `prg_reg`): delegate to MMC3, masking the result to 6 bits.
    /// When M=1: apply NROM-128 or NROM-256 mapping.
    fn mapped_prg_bank(&self, addr: u16) -> usize {
        if (self.prg_reg & 0x80) == 0 {
            return self.mmc3.mapped_prg_bank(addr) & 0x3F;
        }

        // bank_16k: 4-bit value from bits 4:1 of prg_reg.
        let bank_16k = ((self.prg_reg & 0x1E) >> 1) as usize;
        // slot_in_16k: which 8 KiB half (0 or 1) within the 16 KiB window.
        let slot_in_16k = ((addr & 0x2000) >> 13) as usize;

        if (self.prg_reg & 0x20) == 0 {
            // NROM-128: same 16 KiB bank mirrored at both $8000–$BFFF and $C000–$FFFF.
            bank_16k * 2 + slot_in_16k
        } else {
            // NROM-256: replace bit 1 of bank_16k with CPU A14.
            let a14 = usize::from(addr >= 0xC000);
            let bank_eff = (bank_16k & !2) | (a14 << 1);
            bank_eff * 2 + slot_in_16k
        }
    }

    /// Return the effective 1 KiB CHR page number for `ppu_addr`.
    ///
    /// CHR A18 is OR'd into bit 8 of the MMC3 bank, selecting which 256 KiB half
    /// of the 512 KiB CHR-ROM provides the tile data.
    fn mapped_chr_bank(&self, ppu_addr: u16) -> usize {
        let mmc3_bank = self.mmc3.mapped_chr_1k_bank(ppu_addr);
        let ppu_a12 = usize::from((ppu_addr >> 12) & 1 != 0);
        let chr_a18 = if self.chr_a18_mode == 0 {
            1 - ppu_a12 // inverted PPU A12: A18 set when A12=0
        } else {
            ppu_a12 // PPU A12: A18 set when A12=1
        };
        mmc3_bank | (chr_a18 << 8)
    }
}

impl Mapper for Mapper187 {
    fn base(&self) -> &BaseMapper {
        &self.mmc3.base
    }

    fn base_mut(&mut self) -> &mut BaseMapper {
        &mut self.mmc3.base
    }

    fn mmc3_delegate(&self) -> Option<&MMC3Mapper> {
        Some(&self.mmc3)
    }

    fn mmc3_delegate_mut(&mut self) -> Option<&mut MMC3Mapper> {
        Some(&mut self.mmc3)
    }

    fn read_prg(&self, addr: u16) -> u8 {
        match addr {
            0x5000..=0x5FFF => 0x83, // bit 7 set, as required by KOF'96
            0x6000..=0x7FFF => self.mmc3.read_prg(addr), // PRG-RAM
            0x8000..=0xFFFF => {
                let bank = self.mapped_prg_bank(addr);
                let offset = (addr as usize) & PRG_BANK_MASK;
                self.mmc3.read_prg_at_bank(bank, offset)
            }
            _ => 0,
        }
    }

    fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
        match addr {
            0x5000..=0x5FFF => 0x83,
            0x6000..=0x7FFF => self.mmc3.read_prg_open_bus(addr, open_bus),
            0x8000..=0xFFFF => self.read_prg(addr),
            _ => open_bus,
        }
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        match addr {
            0x5000..=0x5FFF if (addr & 0xF001) == 0x5000 => {
                self.prg_reg = value;
            }
            0x8000..=0x9FFF if (addr & 0x0001) == 0 => {
                // Even write to $8000–$9FFF: extract CHR A18 mode from bit 7, then
                // forward to MMC3 so the bank-select register is also updated.
                self.chr_a18_mode = (value >> 7) & 1;
                self.mmc3.write_prg(addr, value);
            }
            0x6000..=0xFFFF => {
                self.mmc3.write_prg(addr, value);
            }
            _ => {}
        }
    }

    fn read_chr(&mut self, ppu_addr: u16) -> u8 {
        let bank = self.mapped_chr_bank(ppu_addr);
        let offset = (ppu_addr as usize) & CHR_BANK_MASK;
        self.mmc3.read_chr_1k_at(bank, offset)
    }

    fn write_chr(&mut self, ppu_addr: u16, value: u8) {
        let bank = self.mapped_chr_bank(ppu_addr);
        let offset = (ppu_addr as usize) & CHR_BANK_MASK;
        self.mmc3.write_chr_1k_at(bank, offset, value);
    }

    fn mapper_number(&self) -> u16 {
        MAPPER_NUMBER
    }

    fn irq_pending(&self) -> bool {
        self.mmc3.irq_pending()
    }

    fn wram_size(&self) -> usize {
        self.mmc3.wram_size()
    }

    fn wram_snapshot(&self) -> Vec<u8> {
        self.mmc3.wram_snapshot()
    }

    fn load_wram_snapshot(&mut self, data: &[u8]) {
        self.mmc3.load_wram_snapshot(data);
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        let mut snap = self.mmc3.registers_snapshot();
        snap.push(self.prg_reg);
        snap.push(self.chr_a18_mode);
        snap
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() >= 18 {
            // Full mapper-187 snapshot: 16 MMC3 bytes + 2 extra.
            let (mmc3_data, tail) = data.split_at(data.len() - 2);
            self.mmc3.restore_registers(mmc3_data);
            self.prg_reg = tail[0];
            self.chr_a18_mode = tail[1] & 1;
        } else {
            // Legacy snapshot without extended registers
            self.mmc3.restore_registers(data);
            self.prg_reg = 0;
            self.chr_a18_mode = 0;
        }
    }

    fn reset(&mut self) {
        self.mmc3.reset();
        self.prg_reg = 0;
        self.chr_a18_mode = 0;
    }

    fn capabilities(&self) -> MapperCapabilities {
        MapperCapabilities {
            has_irq: true,
            has_chr_banking: true,
            has_dynamic_mirroring: true,
            max_prg_ram_kb: 8,
            prg_bank_size_kb: 8,
            chr_bank_size_kb: 1,
            ..Default::default()
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::nes::cartridge::NametableLayout;
    use crate::nes::cartridge::mapper::{MapperContext, create_mapper};
    use crate::nes::cartridge::test_helpers::{banked_data, banked_data_with_upper_marker};

    // 256KB PRG = 32 × 8KB pages (enough to exercise NROM override with bank 15)
    const PRG_BANKS: usize = 32;
    // 512KB CHR = 512 × 1KB pages (exercises CHR A18)
    const CHR_BANKS: usize = 512;

    fn make_mapper() -> Mapper187 {
        Mapper187::new(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE, PRG_BANKS),
            banked_data(CHR_1K_BANK_SIZE, CHR_BANKS),
            NametableLayout::Vertical,
        ))
    }

    #[test]
    fn mapper_187_is_registered_in_factory() {
        let result = create_mapper(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE, PRG_BANKS),
            banked_data(CHR_1K_BANK_SIZE, CHR_BANKS),
            NametableLayout::Vertical,
        ));
        assert!(result.is_ok(), "Mapper 187 must be creatable via factory");
    }

    #[test]
    fn power_on_uses_mmc3_prg_banking() {
        let mapper = make_mapper();
        // prg_reg=0, M=0 → use MMC3 banking; MMC3 default maps $E000 to last bank
        assert_eq!(
            mapper.read_prg(0xE000),
            (PRG_BANKS - 1) as u8,
            "$E000 must map to last bank via MMC3"
        );
    }

    #[test]
    fn protection_read_returns_bit7_set() {
        let mapper = make_mapper();
        let val = mapper.read_prg(0x5000);
        assert_ne!(
            val & 0x80,
            0,
            "Protection read must return byte with bit 7 set"
        );
    }

    #[test]
    fn protection_read_covers_entire_5000_5fff() {
        let mapper = make_mapper();
        for addr in [0x5000u16, 0x5100, 0x5800, 0x5FFF] {
            let val = mapper.read_prg(addr);
            assert_ne!(val & 0x80, 0, "${addr:#06X} must return bit 7 set");
        }
    }

    #[test]
    fn nrom128_override_maps_same_bank_to_both_halves() {
        let mut mapper = make_mapper();
        // M=1, N=0, bank_16k=2 → reg = 0x80 | (2<<1) = 0x84
        // NROM-128: $8000-$BFFF and $C000-$FFFF use bank 2 (pages 4 and 5)
        mapper.write_prg(0x5000, 0x84);
        // Slot 0 ($8000-$9FFF): 8KB page = 2*2+0 = 4
        // Slot 1 ($A000-$BFFF): 8KB page = 2*2+1 = 5
        // Slot 2 ($C000-$DFFF): 8KB page = 2*2+0 = 4 (mirrored)
        // Slot 3 ($E000-$FFFF): 8KB page = 2*2+1 = 5 (mirrored)
        assert_eq!(mapper.read_prg(0x8000), 4, "$8000 must be page 4");
        assert_eq!(mapper.read_prg(0xA000), 5, "$A000 must be page 5");
        assert_eq!(mapper.read_prg(0xC000), 4, "$C000 must mirror page 4");
        assert_eq!(mapper.read_prg(0xE000), 5, "$E000 must mirror page 5");
    }

    #[test]
    fn nrom256_override_uses_cpu_a14_for_bank_bit1() {
        let mut mapper = make_mapper();
        // M=1, N=1, bank_16k=0 → reg = 0x80 | 0x20 = 0xA0
        // bank_16k=0, N=1: low_half=bank&~2=0, high_half=bank|2=2
        // $8000-$BFFF: pages 0,1; $C000-$FFFF: pages 4,5
        mapper.write_prg(0x5000, 0xA0);
        assert_eq!(mapper.read_prg(0x8000), 0, "$8000 must be page 0");
        assert_eq!(mapper.read_prg(0xA000), 1, "$A000 must be page 1");
        assert_eq!(
            mapper.read_prg(0xC000),
            4,
            "$C000 must be page 4 (A14=1, bit1=1 → bank_eff=2)"
        );
        assert_eq!(mapper.read_prg(0xE000), 5, "$E000 must be page 5");
    }

    #[test]
    fn prg_override_disabled_with_m0() {
        let mut mapper = make_mapper();
        // Write bank_16k=3 but M=0 → should use MMC3 banking
        mapper.write_prg(0x5000, 0x06); // M=0, bank_16k=3
        // MMC3 default: $E000 maps to last bank
        assert_eq!(
            mapper.read_prg(0xE000),
            (PRG_BANKS - 1) as u8,
            "M=0 must use MMC3 PRG bank"
        );
    }

    /// Build a mapper whose CHR uses `banked_data_with_upper_marker` so that
    /// banks 0–255 read back 0 and banks 256–511 read back 1. This makes
    /// the CHR A18 effect directly visible in assertions.
    fn make_mapper_upper_chr() -> Mapper187 {
        Mapper187::new(MapperContext::new_for_test(
            MAPPER_NUMBER,
            banked_data(PRG_BANK_SIZE, PRG_BANKS),
            banked_data_with_upper_marker(CHR_1K_BANK_SIZE, CHR_BANKS),
            NametableLayout::Vertical,
        ))
    }

    #[test]
    fn chr_a18_mode0_inverts_ppu_a12_for_sprites() {
        // Mode 0 (default, chr_a18_mode=0): A18 = inverted PPU A12.
        // PPU $0000 (A12=0) → A18=1 → bank | 0x100 (>= 256) → upper marker = 1
        // PPU $1000 (A12=1) → A18=0 → bank as-is (<256) → upper marker = 0
        let mut mapper = make_mapper_upper_chr();
        assert_eq!(
            mapper.read_chr(0x0000),
            1,
            "mode0: PPU $0000 (A12=0) must use A18=1 (upper CHR half)"
        );
        assert_eq!(
            mapper.read_chr(0x1000),
            0,
            "mode0: PPU $1000 (A12=1) must use A18=0 (lower CHR half)"
        );
    }

    #[test]
    fn chr_a18_mode1_follows_ppu_a12() {
        // Mode 1 (chr_a18_mode=1): A18 = PPU A12.
        // PPU $0000 (A12=0) → A18=0 → lower half → upper marker = 0
        // PPU $1000 (A12=1) → A18=1 → upper half → upper marker = 1
        let mut mapper = make_mapper_upper_chr();
        mapper.write_prg(0x8000, 0x80); // bit 7 = 1 → chr_a18_mode = 1
        assert_eq!(
            mapper.read_chr(0x0000),
            0,
            "mode1: PPU $0000 (A12=0) must use A18=0 (lower CHR half)"
        );
        assert_eq!(
            mapper.read_chr(0x1000),
            1,
            "mode1: PPU $1000 (A12=1) must use A18=1 (upper CHR half)"
        );
    }

    #[test]
    fn write_8000_sets_chr_a18_mode_and_forwards_to_mmc3() {
        let mut mapper = make_mapper();
        // Write $80 to $8000: bit 7 = 1 → chr_a18_mode = 1
        mapper.write_prg(0x8000, 0x80);
        assert_eq!(
            mapper.chr_a18_mode, 1,
            "bit 7 of $8000 write must set chr_a18_mode"
        );
        // Write $00 to $8000: bit 7 = 0 → chr_a18_mode = 0
        mapper.write_prg(0x8000, 0x00);
        assert_eq!(
            mapper.chr_a18_mode, 0,
            "bit 7 clear must reset chr_a18_mode"
        );
    }

    #[test]
    fn reset_clears_extra_regs() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x5000, 0x84);
        mapper.write_prg(0x8000, 0x80);
        mapper.reset();
        assert_eq!(mapper.prg_reg, 0, "reset must clear prg_reg");
        assert_eq!(mapper.chr_a18_mode, 0, "reset must clear chr_a18_mode");
    }

    #[test]
    fn snapshot_restore_round_trips_extra_regs() {
        let mut mapper = make_mapper();
        mapper.write_prg(0x5000, 0x84);
        mapper.write_prg(0x8000, 0x80);
        let snap = mapper.registers_snapshot();

        let mut restored = make_mapper();
        restored.restore_registers(&snap);
        assert_eq!(restored.prg_reg, 0x84);
        assert_eq!(restored.chr_a18_mode, 1);
    }

    #[test]
    fn irq_not_pending_at_power_on() {
        let mapper = make_mapper();
        assert!(!mapper.irq_pending());
    }
}
