//! Mapper 303 – Kaiser KS7017
//!
//! Specifications:
//! - Primary: NesDev wiki (unavailable, HTTP 403).
//! - Fallback: Mesen2 `Core/NES/Mappers/Kaiser/Kaiser7017.h`
//!   <https://github.com/SourMesen/Mesen2/blob/master/Core/NES/Mappers/Kaiser/Kaiser7017.h>
//!
//! # Hardware overview
//!
//! Used for a Kaiser KS7017 cartridge.
//!
//! - PRG-ROM: 2 switchable 16 KiB banks.
//!   - `$8000–$BFFF`: switchable 16 KiB bank (selected via address-encoded register)
//!   - `$C000–$FFFF`: fixed 16 KiB bank, always page 2
//! - CHR-ROM: 8 KiB, bank 0 (fixed / unbanked).
//! - Mirroring: Horizontal or Vertical, controlled by write to `$4025`.
//! - IRQ: 16-bit countdown counter, CPU-cycle driven.
//! - PRG-RAM: none.
//! - Bus conflicts: none.
//!
//! # Register map
//!
//! Registers at `$4020–$5FFF` (write); `$4030` (read):
//!
//! | Address      | Direction | Effect                                                       |
//! |--------------|-----------|--------------------------------------------------------------|
//! | `$4020`      | Write     | Load IRQ counter low byte; also clears pending IRQ           |
//! | `$4021`      | Write     | Load IRQ counter high byte; enables IRQ; clears pending IRQ  |
//! | `$4025`      | Write     | Bit 3: mirroring (0 = Vertical, 1 = Horizontal)              |
//! | `$4Axx`      | Write     | Latch PRG bank: `bank = ((addr>>2) & 0x03) \| ((addr>>4) & 0x04)` |
//! | `$51xx`      | Write     | Apply latched PRG bank                                       |
//! | `$4030`      | Read      | Bit 0: IRQ pending; reading clears pending IRQ               |
//!
//! # Memory map
//!
//! ```text
//! CPU $8000–$BFFF  PRG-ROM 16 KiB switchable (PRG bank register)
//! CPU $C000–$FFFF  PRG-ROM 16 KiB fixed (page 2)
//! PPU $0000–$1FFF  CHR 8 KiB bank 0 (fixed)
//! ```
//!
//! # Power-on / reset state
//!
//! - PRG bank = 0: `$8000–$BFFF` = page 0, `$C000–$FFFF` = page 2.
//! - Mirroring: Vertical.
//! - IRQ counter = 0, IRQ disabled.

use std::cell::Cell;

use crate::nes::cartridge::NametableLayout;
use crate::nes::cartridge::base_mapper::BaseMapper;
use crate::nes::cartridge::cpu_cycle_irq::{CpuCycleIrq, CpuCycleIrqMode};
use crate::nes::cartridge::mapper::{Mapper, MapperCapabilities, MapperContext};

const MAPPER_NUMBER: u16 = 303;
const PRG_BANK_SIZE: usize = 0x4000; // 16 KiB
const FIXED_PRG_BANK: i16 = 2;

/// Mapper 303 – Kaiser KS7017
pub struct Mapper303 {
    base: BaseMapper,
    /// Latched PRG bank register (written via $4Axx, applied via $51xx).
    prg_bank_latch: u8,
    /// Currently applied PRG bank for $8000–$BFFF.
    prg_bank: u8,
    irq: CpuCycleIrq,
    /// Staging register for IRQ counter, low byte.
    irq_counter_low: u8,
    /// Interior-mutable pending flag so $4030 read can clear it in &self context.
    irq_pending_latch: Cell<bool>,
}

impl Mapper303 {
    pub fn new(ctx: MapperContext) -> Self {
        let capabilities = MapperCapabilities {
            has_dynamic_mirroring: true,
            has_irq: true,
            prg_bank_size_kb: 16,
            ..Default::default()
        };
        let mut base = BaseMapper::new(&ctx, capabilities);
        base.configure_prg_banking(PRG_BANK_SIZE);

        let mut mapper = Self {
            base,
            prg_bank_latch: 0,
            prg_bank: 0,
            irq: CpuCycleIrq::new(CpuCycleIrqMode::DownToZero),
            irq_counter_low: 0,
            irq_pending_latch: Cell::new(false),
        };
        mapper.apply_state();
        mapper
    }

    fn apply_state(&mut self) {
        self.base.select_prg_page(0, self.prg_bank as i16);
        self.base.select_prg_page(1, FIXED_PRG_BANK);
        self.base.set_mirroring(NametableLayout::Vertical);
    }
}

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

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

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

    fn read_prg(&self, addr: u16) -> u8 {
        self.base.read_prg_banked(addr)
    }

    fn read_prg_open_bus(&self, addr: u16, open_bus: u8) -> u8 {
        if addr == 0x4030 {
            let pending = self.irq_pending_latch.get();
            self.irq_pending_latch.set(false);
            return (open_bus & 0xFE) | (pending as u8);
        }
        self.base
            .read_prg_open_bus(addr, open_bus, |a| self.read_prg(a))
    }

    fn write_prg(&mut self, addr: u16, value: u8) {
        if self.base.try_write_prg_ram(addr, value) {
            return;
        }
        match addr {
            0x4020 => {
                self.irq.acknowledge();
                self.irq_pending_latch.set(false);
                self.irq_counter_low = value;
            }
            0x4021 => {
                self.irq.acknowledge();
                self.irq_pending_latch.set(false);
                let counter = (self.irq_counter_low as u16) | ((value as u16) << 8);
                self.irq.set_counter(counter);
                self.irq.set_enabled(true);
                self.irq.set_pending(false);
            }
            0x4025 => {
                let layout = if (value >> 3) & 1 == 1 {
                    NametableLayout::Horizontal
                } else {
                    NametableLayout::Vertical
                };
                self.base.set_mirroring(layout);
            }
            0x4A00..=0x4AFF => {
                self.prg_bank_latch = ((addr >> 2) as u8 & 0x03) | ((addr >> 4) as u8 & 0x04);
            }
            0x5100..=0x51FF => {
                self.prg_bank = self.prg_bank_latch;
                self.base.select_prg_page(0, self.prg_bank as i16);
            }
            _ => {}
        }
    }

    fn cpu_cycle(&mut self) {
        self.irq.tick();
        if self.irq.is_pending() {
            self.irq.set_enabled(false);
            self.irq.set_pending(false);
            self.irq_pending_latch.set(true);
        }
    }

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

    fn reset(&mut self) {
        self.prg_bank_latch = 0;
        self.prg_bank = 0;
        self.irq = CpuCycleIrq::new(CpuCycleIrqMode::DownToZero);
        self.irq_counter_low = 0;
        self.irq_pending_latch.set(false);
        self.apply_state();
    }

    fn registers_snapshot(&self) -> Vec<u8> {
        let flags = (self.irq.enabled() as u8) | ((self.irq_pending_latch.get() as u8) << 1);
        vec![
            self.prg_bank_latch,
            self.prg_bank,
            flags,
            (self.irq.counter() & 0xFF) as u8,
            (self.irq.counter() >> 8) as u8,
            self.irq_counter_low,
            self.base.mirroring().to_snapshot_byte(),
        ]
    }

    fn restore_registers(&mut self, data: &[u8]) {
        if data.len() < 6 {
            return;
        }
        self.prg_bank_latch = data[0];
        self.prg_bank = data[1];
        let flags = data[2];
        self.irq.set_enabled(flags & 0x01 != 0);
        let pending = flags & 0x02 != 0;
        self.irq.set_pending(false);
        self.irq_pending_latch.set(pending);
        self.irq
            .set_counter((data[3] as u16) | ((data[4] as u16) << 8));
        self.irq_counter_low = data[5];
        if data.len() >= 7 {
            self.base
                .set_mirroring(NametableLayout::from_snapshot_byte(data[6]));
        }
        self.base.select_prg_page(0, self.prg_bank as i16);
        self.base.select_prg_page(1, FIXED_PRG_BANK);
    }
}

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

    fn create_mapper303(prg_rom: Vec<u8>) -> Box<dyn Mapper> {
        create_mapper(MapperContext::new_for_test(
            303,
            prg_rom,
            vec![],
            NametableLayout::Vertical,
        ))
        .expect("mapper 303 should be implemented")
    }

    // ── Registration ──────────────────────────────────────────────────────────

    #[test]
    fn mapper_303_is_registered() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mapper = create_mapper303(prg_rom);
        assert_eq!(mapper.mapper_number(), 303);
    }

    // ── Power-on: PRG banking ──────────────────────────────────────────────────

    /// Bank 0 ($8000–$BFFF) defaults to PRG page 0.
    /// Bank 1 ($C000–$FFFF) is fixed at PRG page 2.
    #[test]
    fn power_on_prg_banks_correct() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mapper = create_mapper303(prg_rom);

        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "bank 0 ($8000) should be PRG page 0"
        );
        assert_eq!(
            mapper.read_prg(0xC000),
            2,
            "bank 1 ($C000) should be fixed PRG page 2"
        );
    }

    // ── PRG bank switching via $4Axx / $51xx ──────────────────────────────────

    /// Writing to $4A04 ($4Axx, addr bit A2 set → bank bit 0 set) latches bank 1,
    /// and writing to $5100 applies it.
    #[test]
    fn prg_bank_latch_and_apply_via_4axx_51xx() {
        // Addr $4A04: bit A2 set → ((0x04 >> 2) & 0x03) | ((0x04 >> 4) & 0x04) = 1 | 0 = 1
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        // Latch bank = 1 via addr $4A04 (A2 set)
        mapper.write_prg(0x4A04, 0x00);
        // Before applying, $8000 should still be page 0
        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "latch should not take effect until $51xx write"
        );

        // Apply latch
        mapper.write_prg(0x5100, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 1, "$8000 should now read page 1");
    }

    /// Writing to $4A08 ($4Axx, addr bit A3 set → bank bit 1 set) latches bank 2.
    #[test]
    fn prg_bank_latch_addr_bit_a3() {
        // Addr $4A08: ((0x08 >> 2) & 0x03) | ((0x08 >> 4) & 0x04) = 2 | 0 = 2
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4A08, 0x00);
        mapper.write_prg(0x5100, 0x00);
        // Page 2 = fixed bank, but bank 0 is now also page 2
        assert_eq!(mapper.read_prg(0x8000), 2, "$8000 should read page 2");
    }

    /// Writing to $4A40 (addr bit A6 set → bank bit 2 set) latches bank 4.
    #[test]
    fn prg_bank_latch_addr_bit_a6() {
        // Addr $4A40: ((0x40 >> 2) & 0x03) | ((0x40 >> 4) & 0x04) = 0 | 4 = 4
        let prg_rom = banked_data(PRG_BANK_SIZE, 8);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4A40, 0x00);
        mapper.write_prg(0x5100, 0x00);
        assert_eq!(mapper.read_prg(0x8000), 4, "$8000 should read page 4");
    }

    /// $C000–$FFFF always stays at fixed page 2 regardless of bank switch.
    #[test]
    fn fixed_bank_at_c000_ffff_never_changes() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 8);
        let mut mapper = create_mapper303(prg_rom);

        // Switch bank 0 to page 5
        mapper.write_prg(0x4A14, 0x00); // ((0x14 >> 2) & 0x03) | ((0x14 >> 4) & 0x04) = 5
        mapper.write_prg(0x5100, 0x00);

        assert_eq!(
            mapper.read_prg(0xC000),
            2,
            "$C000 should remain fixed at page 2"
        );
        assert_eq!(
            mapper.read_prg(0xFFFF),
            2,
            "$FFFF should remain fixed at page 2"
        );
    }

    // ── Mirroring ─────────────────────────────────────────────────────────────

    #[test]
    fn power_on_mirroring_is_vertical() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mapper = create_mapper303(prg_rom);
        assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
    }

    #[test]
    fn write_4025_bit3_set_selects_horizontal_mirroring() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4025, 0x08); // bit 3 = 1 → Horizontal
        assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);
    }

    #[test]
    fn write_4025_bit3_clear_selects_vertical_mirroring() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        // First switch to Horizontal
        mapper.write_prg(0x4025, 0x08);
        assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);

        // Now switch back to Vertical
        mapper.write_prg(0x4025, 0x00);
        assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
    }

    // ── IRQ ───────────────────────────────────────────────────────────────────

    #[test]
    fn irq_not_pending_at_power_on() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mapper = create_mapper303(prg_rom);
        assert!(!mapper.irq_pending());
    }

    /// Writing $4021 enables IRQ with the 16-bit counter.
    /// Counter counts down each CPU cycle; IRQ fires when it reaches 0.
    #[test]
    fn irq_fires_when_counter_reaches_zero() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        // Set counter = 3 (low=3, high=0), enable
        mapper.write_prg(0x4020, 3);
        mapper.write_prg(0x4021, 0);

        assert!(
            !mapper.irq_pending(),
            "IRQ should not fire before counter hits 0"
        );
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(mapper.irq_pending(), "IRQ should fire after 3 cycles");
    }

    /// Writing $4021 sets high byte, composing the full 16-bit counter.
    #[test]
    fn irq_counter_high_byte_via_4021() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        // low = 0x00, high = 0x01 → counter = 256
        mapper.write_prg(0x4020, 0x00);
        mapper.write_prg(0x4021, 0x01);

        for _ in 0..255 {
            mapper.cpu_cycle();
            assert!(
                !mapper.irq_pending(),
                "IRQ should not fire before 256 cycles"
            );
        }
        mapper.cpu_cycle(); // 256th cycle
        assert!(mapper.irq_pending(), "IRQ should fire after 256 cycles");
    }

    /// Writing $4020 clears IRQ pending.
    #[test]
    fn write_4020_clears_irq_pending() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4020, 1);
        mapper.write_prg(0x4021, 0);
        mapper.cpu_cycle();
        assert!(mapper.irq_pending());

        mapper.write_prg(0x4020, 5); // writing low byte should clear IRQ
        assert!(
            !mapper.irq_pending(),
            "$4020 write should clear pending IRQ"
        );
    }

    /// Writing $4021 clears IRQ pending.
    #[test]
    fn write_4021_clears_irq_pending() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4020, 1);
        mapper.write_prg(0x4021, 0);
        mapper.cpu_cycle();
        assert!(mapper.irq_pending());

        mapper.write_prg(0x4021, 0); // reloading high byte clears IRQ
        assert!(
            !mapper.irq_pending(),
            "$4021 write should clear pending IRQ"
        );
    }

    /// Reading $4030 returns IRQ pending status and clears it.
    #[test]
    fn read_4030_returns_irq_status_and_clears() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4020, 1);
        mapper.write_prg(0x4021, 0);
        mapper.cpu_cycle();
        assert!(mapper.irq_pending());

        // read_prg_open_bus returns status and clears pending IRQ
        let status = mapper.read_prg_open_bus(0x4030, 0xFF);
        assert_eq!(
            status & 0x01,
            0x01,
            "$4030 bit 0 should be 1 when IRQ pending"
        );
        assert!(!mapper.irq_pending(), "$4030 read should clear pending IRQ");
    }

    /// $4030 returns 0 when IRQ is not pending.
    #[test]
    fn read_4030_returns_zero_when_no_irq() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mapper = create_mapper303(prg_rom);
        let status = mapper.read_prg_open_bus(0x4030, 0xFF);
        assert_eq!(status & 0x01, 0x00, "$4030 bit 0 should be 0 when no IRQ");
    }

    /// IRQ is disabled after it fires (writing $4021 re-enables it).
    #[test]
    fn irq_disables_after_firing() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4020, 1);
        mapper.write_prg(0x4021, 0);
        mapper.cpu_cycle();
        assert!(mapper.irq_pending());

        // Acknowledge via $4020
        mapper.write_prg(0x4020, 0);

        // Additional CPU cycles should not fire IRQ again (counter is 0, enabled cleared)
        for _ in 0..10 {
            mapper.cpu_cycle();
        }
        // No new IRQ since it was disabled
        assert!(
            !mapper.irq_pending(),
            "IRQ should stay clear after acknowledge with no reload"
        );
    }

    // ── Reset ─────────────────────────────────────────────────────────────────

    #[test]
    fn reset_restores_power_on_state() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 8);
        let mut mapper = create_mapper303(prg_rom);

        // Change state
        mapper.write_prg(0x4A40, 0); // latch bank 4
        mapper.write_prg(0x5100, 0); // apply
        mapper.write_prg(0x4025, 0x08); // horizontal mirroring

        assert_eq!(mapper.read_prg(0x8000), 4);
        assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);

        mapper.reset();

        assert_eq!(
            mapper.read_prg(0x8000),
            0,
            "after reset, bank 0 should be page 0"
        );
        assert_eq!(
            mapper.read_prg(0xC000),
            2,
            "after reset, fixed bank should be page 2"
        );
        assert_eq!(mapper.base().mirroring(), NametableLayout::Vertical);
        assert!(!mapper.irq_pending());
    }

    // ── Save-state snapshot / restore ─────────────────────────────────────────

    #[test]
    fn snapshot_and_restore_preserves_prg_bank() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 8);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4A04, 0); // latch bank 1
        mapper.write_prg(0x5100, 0); // apply

        let snap = mapper.registers_snapshot();

        mapper.reset();
        assert_eq!(mapper.read_prg(0x8000), 0, "after reset, should be page 0");

        mapper.restore_registers(&snap);
        assert_eq!(
            mapper.read_prg(0x8000),
            1,
            "after restore, should be page 1"
        );
    }

    #[test]
    fn snapshot_and_restore_preserves_irq_state() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4020, 5);
        mapper.write_prg(0x4021, 0); // counter = 5, enabled

        let snap = mapper.registers_snapshot();

        mapper.reset();

        mapper.restore_registers(&snap);
        // Counter should be 5 again
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(!mapper.irq_pending());
        mapper.cpu_cycle();
        assert!(
            mapper.irq_pending(),
            "IRQ should fire after restoring counter=5 and 5 cycles"
        );
    }

    #[test]
    fn snapshot_and_restore_preserves_mirroring() {
        let prg_rom = banked_data(PRG_BANK_SIZE, 4);
        let mut mapper = create_mapper303(prg_rom);

        mapper.write_prg(0x4025, 0x08); // switch to Horizontal
        assert_eq!(mapper.base().mirroring(), NametableLayout::Horizontal);

        let snap = mapper.registers_snapshot();

        mapper.reset();
        assert_eq!(
            mapper.base().mirroring(),
            NametableLayout::Vertical,
            "after reset, mirroring should be Vertical"
        );

        mapper.restore_registers(&snap);
        assert_eq!(
            mapper.base().mirroring(),
            NametableLayout::Horizontal,
            "after restore, mirroring should be Horizontal"
        );
    }
}
