/// ROM header formats.
#[derive(Debug)]
pub enum Kind {
    /// Historical header used since the iNES emulator.
    INes,

    /// Modern header which is backwards-compatible in regards to iNES. Using
    /// this header is helpful to disambiguate certain mappers or to be more
    /// specific about some of them (e.g. submappers inside of MMC3).
    Nes20,
}

/// Nametable arrangement as defined by the header. Typically it's either
/// Vertical or Horizontal, but this library supports "alternative"
/// types in some well-known scenarios. That being said, an "Unknown" value is
/// also defined for ROM files which might have a defect.
#[derive(Debug)]
pub enum NameTableArrangement {
    Vertical,
    Horizontal,
    OneScreen,
    FourScreen,
    Unknown,
}

impl NameTableArrangement {
    /// Returns the mirroring identifier for the nametable arrangement. Note
    /// that mirroring is not exactly the same as nametable arrangement.
    pub fn mirroring(&self) -> &str {
        match self {
            NameTableArrangement::Horizontal => "vertical",
            NameTableArrangement::Vertical => "horizontal",
            NameTableArrangement::OneScreen => "1-screen",
            NameTableArrangement::FourScreen => "4-screen",
            NameTableArrangement::Unknown => "unknown",
        }
    }
}

/// Memory mappers. Note that not all of them are listed here, but they will be
/// added with time (and if I even care).
#[derive(Debug)]
pub enum Mapper {
    Axrom,
    BnromCombo,
    BnromOnly,
    Cnrom,
    Mmc1,
    Mmc2,
    Mmc3Acc,
    Mmc3c,
    Mmc3Nec,
    Mmc3Sharp,
    Mmc3T9552,
    Mmc4,
    Mmc5,
    Mmc6,
    Nina001,
    Nrom,
    Unknown,
    Unrom512,
    Uxrom,
}

impl std::fmt::Display for Mapper {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Mapper::Axrom => write!(f, "AxROM"),
            Mapper::BnromCombo => write!(f, "BNROM / NINA-001"),
            Mapper::BnromOnly => write!(f, "BNROM"),
            Mapper::Cnrom => write!(f, "CNROM"),
            Mapper::Mmc1 => write!(f, "MMC1"),
            Mapper::Mmc2 => write!(f, "MMC2"),
            Mapper::Mmc3Acc => write!(f, "MC-ACC"),
            Mapper::Mmc3c => write!(f, "MMC3C"),
            Mapper::Mmc3Nec => write!(f, "MMC3 (NEC)"),
            Mapper::Mmc3Sharp => write!(f, "MMC3 (Sharp)"),
            Mapper::Mmc3T9552 => write!(f, "MMC3 (variant with a T9552 scrambling chip)"),
            Mapper::Mmc4 => write!(f, "MMC4"),
            Mapper::Mmc5 => write!(f, "MMC5"),
            Mapper::Mmc6 => write!(f, "MMC6"),
            Mapper::Nina001 => write!(f, "NINA 001"),
            Mapper::Nrom => write!(f, "NROM"),
            Mapper::Unknown => write!(f, "unknown"),
            Mapper::Unrom512 => write!(f, "UNROM 512"),
            Mapper::Uxrom => write!(
                f,
                "UxROM (NES-UNROM, NES-UOROM, HVC-UN1ROM their HVC counterparts, and clone boards)"
            ),
        }
    }
}

/// The kind of RAM chip available on the cartridge.
#[derive(Debug)]
pub enum RamKind {
    Ram,
    Nvram,
}

impl std::fmt::Display for RamKind {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::Ram => write!(f, "RAM"),
            Self::Nvram => write!(f, "NVRAM"),
        }
    }
}

/// The kind and size of the external RAM chip available on the cartridge.
#[derive(Debug)]
pub struct RamDefinition {
    pub kind: RamKind,
    pub size: usize,
}

/// The CPU/PPU timing architecture.
#[derive(Debug)]
pub enum Timing {
    Ntsc,
    Pal,
    MultipleRegion,
    Dendy,
}

impl std::fmt::Display for Timing {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            Self::Ntsc => write!(f, "NTSC"),
            Self::Pal => write!(f, "PAL"),
            Self::MultipleRegion => write!(f, "multi-region"),
            Self::Dendy => write!(f, "Dendy"),
        }
    }
}

/// Information that has been parsed from an NES/Famicom ROM header. The
/// `Header` struct implements the `TryFrom` trait. Hence, use
/// `Header::try_from` in order to parse a header.
#[derive(Debug)]
pub struct Header {
    /// PRG ROM size in units of 16KB. That is, a "1" here actually means
    /// "16KB". This is in accordance to the header itself, but callers that
    /// need to display this information should perform the translation to be
    /// more useful to the human eye.
    pub prg_rom_size: usize,

    /// CHR ROM size in units of 8KB. That is, a "1" here actually means "8KB".
    /// This is in accordance to the header itself, but callers that need to
    /// display this information should perform the translation to be more
    /// useful to the human eye.
    pub chr_rom_size: usize,

    /// Nametable arrangement.
    pub nametable_arrangement: NameTableArrangement,

    /// Whether the memory region $6000-$7FFF is persistent or not (e.g.
    /// battery-backed and handled through a mapper like MMC1). For the exact
    /// kind of RAM definition check the `external_ram_definition` member.
    pub has_persistent_memory: bool,

    /// If `has_persistent_memory` is set to true, then it will contain a Some
    /// value with the kind of PRG-RAM chip available and its size.
    pub prg_ram_definition: Option<RamDefinition>,

    /// If `has_persistent_memory` is set to true, then it will contain a Some
    /// value with the kind of CHR-RAM chip available and its size.
    pub chr_ram_definition: Option<RamDefinition>,

    /// The CPU/PPU timing detected from the header.
    pub timing: Timing,

    /// Whether there is a 512-byte trainer at $7000-$71FF or not.
    pub has_trainer: bool,

    /// The memory mapper being used.
    pub mapper: Mapper,

    /// The kind of the header.
    pub kind: Kind,
}

impl TryFrom<&[u8]> for Header {
    type Error = &'static str;

    fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
        let kind = get_rom_kind(bytes)?;
        let ninth = bytes.get(9).unwrap_or(&0);
        let mapper = parse_mapper(bytes.get(6), bytes.get(7), bytes.get(8));
        let has_persistent_memory = (bytes.get(6).unwrap_or(&0) & 0x2) == 0x2;

        Ok(Self {
            prg_rom_size: if matches!(kind, Kind::Nes20) {
                (((ninth & 0x0F) as usize) << 8) + bytes[4] as usize
            } else {
                bytes[4] as usize
            },
            chr_rom_size: if matches!(kind, Kind::Nes20) {
                (((ninth & 0xF0) as usize) << 4) + bytes[5] as usize
            } else {
                bytes[5] as usize
            },
            nametable_arrangement: parse_nametable(bytes.get(6), &mapper),
            has_persistent_memory,
            has_trainer: (bytes.get(6).unwrap_or(&0) & 0x4) == 0x4,
            mapper,
            prg_ram_definition: parse_ram_definition(&kind, bytes.get(10), has_persistent_memory),
            chr_ram_definition: parse_ram_definition(&kind, bytes.get(11), has_persistent_memory),
            timing: parse_timing(&kind, bytes.get(12)),
            kind,
        })
    }
}

// Parse the given `byte` by assuming it's a RAM definition. It will just return
// `None` if `has_persistent_memory` is false or the `kind` is not NES 2.0.
fn parse_ram_definition(
    kind: &Kind,
    byte: Option<&u8>,
    has_persistent_memory: bool,
) -> Option<RamDefinition> {
    // If the bit for persistent memory was not set, then we don't even
    // bother. If there was something relevant here, then the ROM does not have
    // a proper format.\
    //
    // Moreover, if this is not NES 2.0 format, then we don't even bother as
    // defining this on iNES v1 is not recommended.
    if !has_persistent_memory || !matches!(kind, Kind::Nes20) {
        return None;
    }
    let definition = byte?;

    let mut shift = definition & 0x0F;
    if shift > 0 {
        return Some(RamDefinition {
            kind: RamKind::Ram,
            size: 64 << (shift as usize),
        });
    }

    shift = definition & 0xF0;
    if shift > 0 {
        shift >>= 4;
        return Some(RamDefinition {
            kind: RamKind::Nvram,
            size: 64 << (shift as usize),
        });
    }

    None
}

// Parse the CPU/PPU timing in case it's in the NES 2.0 format.
fn parse_timing(kind: &Kind, byte: Option<&u8>) -> Timing {
    // By default assume NTSC.
    if matches!(kind, Kind::INes) {
        return Timing::Ntsc;
    }
    let Some(definition) = byte else {
        return Timing::Ntsc;
    };

    // A 0 value is NTSC, but Rust here is not clever enough to assume that
    // masking out 0x03 only gives 4 possible outcomes.
    match *definition & 0x03 {
        0x01 => Timing::Pal,
        0x02 => Timing::MultipleRegion,
        0x03 => Timing::Dendy,
        _ => Timing::Ntsc,
    }
}

// Detect the nametable arrangement given the relevant `byte` value. In some
// cases the `mapper` might be needed to weed out some ambiguities.
fn parse_nametable(byte: Option<&u8>, mapper: &Mapper) -> NameTableArrangement {
    // In some broken scenarios this might not be given. Return an "unknown"
    // value just to be safe.
    let Some(b) = byte else {
        return NameTableArrangement::Unknown;
    };

    // Handle special per-mapper cases.
    match mapper {
        Mapper::Mmc3Acc
        | Mapper::Mmc3Nec
        | Mapper::Mmc3Sharp
        | Mapper::Mmc3T9552
        | Mapper::Mmc3c
            // MMC3 chips can mean a 4-screen nametable arrangement if
            // the "alternative nametable layout" bit is set.
            if (b & 0x08) == 0x08 => {
                return NameTableArrangement::FourScreen;
            }
        Mapper::Unrom512
            // In UNROM 512 chips, if the "alternative nametable layout"
            // bit is set, then it depends on the "nametable
            // arrangement" bit to decide whether it's a 1-screen or
            // 4-screen layout.
            if (b & 0x08) == 0x08 => {
                if b & 0x1 == 0 {
                    return NameTableArrangement::OneScreen;
                }
                return NameTableArrangement::FourScreen;
            }
        _ => {}
    }

    // If no special per-mapper case was matched, then we fall back to
    // the default behavior of checking on the "nametable arrangement"
    // bit.
    if b & 0x1 == 0 {
        NameTableArrangement::Vertical
    } else {
        NameTableArrangement::Horizontal
    }
}

fn parse_mapper(sixth: Option<&u8>, seventh: Option<&u8>, eighth: Option<&u8>) -> Mapper {
    match sixth {
        Some(l) => {
            let byte = (l & 0xF0) >> 4;
            let s = seventh.unwrap_or(&0);
            let result = (s & 0xF0) | byte;

            // Is this NES 2.0 format? If so, then the eighth byte will contain
            // further information on mapper/submapper.
            if (s & 0x0C) == 0x08 {
                let e = eighth.unwrap_or(&0);
                get_mapper_from_id(
                    ((*e as usize & 0x0F) << 16) + result as usize,
                    (e & 0xF0) >> 4,
                )
            } else {
                get_mapper_from_id(result as usize, 0)
            }
        }
        None => Mapper::Unknown,
    }
}

fn get_mapper_from_id(mapper_id: usize, submapper_id: u8) -> Mapper {
    match mapper_id {
        0 => Mapper::Nrom,
        1 => Mapper::Mmc1,
        2 => Mapper::Uxrom,
        3 => Mapper::Cnrom,
        4 => match submapper_id {
            0 => Mapper::Mmc3Sharp,
            1 => Mapper::Mmc6,
            2 => Mapper::Mmc3c,
            3 => Mapper::Mmc3Acc,
            4 => Mapper::Mmc3Nec,
            5 => Mapper::Mmc3T9552,
            _ => Mapper::Unknown,
        },
        5 => Mapper::Mmc5,
        7 => Mapper::Axrom,
        9 => Mapper::Mmc2,
        10 => Mapper::Mmc4,
        30 => Mapper::Unrom512,
        34 => match submapper_id {
            0 => Mapper::BnromCombo,
            1 => Mapper::BnromOnly,
            2 => Mapper::Nina001,
            _ => Mapper::Unknown,
        },
        _ => Mapper::Unknown,
    }
}

/// Get the Kind as parsed from the header expressed in `bytes`.
pub fn get_rom_kind(bytes: &[u8]) -> Result<Kind, &'static str> {
    if bytes.len() < 6 {
        return Err("given header is too short");
    }
    if bytes[0] != b'N' || bytes[1] != b'E' || bytes[2] != b'S' || bytes[3] != 0x1A {
        return Err("invalid magic value for header");
    }

    if (bytes.get(7).unwrap_or(&0) & 0x0C) == 0x08 {
        Ok(Kind::Nes20)
    } else {
        Ok(Kind::INes)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn get_rom_kind_test() {
        assert_eq!(
            get_rom_kind(vec![].as_slice()).unwrap_err(),
            "given header is too short"
        );

        assert_eq!(
            get_rom_kind(vec![b'N', b'E', b'S', b'\0', 0x01, 0x00].as_slice()).unwrap_err(),
            "invalid magic value for header"
        );

        // iNES without the seventh byte.
        assert!(matches!(
            get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00].as_slice()).unwrap(),
            Kind::INes
        ));

        // iNES with seventh byte.
        assert!(matches!(
            get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x00].as_slice()).unwrap(),
            Kind::INes
        ));

        assert!(matches!(
            get_rom_kind(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x00, 0x00, 0x08].as_slice()).unwrap(),
            Kind::Nes20
        ));
    }

    #[test]
    fn nrom_test() {
        let mut header =
            Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x00].as_slice())
                .unwrap();
        assert_eq!(header.prg_rom_size, 1);
        assert_eq!(header.chr_rom_size, 1);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::Vertical
        ));
        assert!(!header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Nrom));
        assert!(matches!(header.kind, Kind::INes));

        header = Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x00, 0x08].as_slice())
            .unwrap();
        assert_eq!(header.prg_rom_size, 1);
        assert_eq!(header.chr_rom_size, 1);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::Vertical
        ));
        assert!(!header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Nrom));
        assert!(matches!(header.kind, Kind::Nes20));
    }

    #[test]
    fn mmc3_mmc6_test() {
        // MMC3 Sharp in iNES format.
        let mut header =
            Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x00].as_slice())
                .unwrap();
        assert_eq!(header.prg_rom_size, 1);
        assert_eq!(header.chr_rom_size, 1);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::Vertical
        ));
        assert!(!header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
        assert!(matches!(header.kind, Kind::INes));

        // MMC3 Sharp but with NES 2.0 (notice explicit 0 submapper).
        header =
            Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x00].as_slice())
                .unwrap();
        assert_eq!(header.prg_rom_size, 1);
        assert_eq!(header.chr_rom_size, 1);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::Vertical
        ));
        assert!(!header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
        assert!(matches!(header.kind, Kind::Nes20));

        // Disambiguate to MMC6 thanks to submapper
        header =
            Header::try_from(vec![b'N', b'E', b'S', 0x1A, 0x01, 0x01, 0x40, 0x08, 0x10].as_slice())
                .unwrap();
        assert_eq!(header.prg_rom_size, 1);
        assert_eq!(header.chr_rom_size, 1);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::Vertical
        ));
        assert!(!header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Mmc6));
        assert!(matches!(header.kind, Kind::Nes20));
    }

    #[test]
    fn malasombra() {
        // Header extracted from the 'Malasombra' NES game released in 2025.
        let header = Header::try_from(
            vec![
                b'N', b'E', b'S', 0x1A, 0x20, 0x00, 0x4B, 0x08, 0x00, 0x00, 0x70, 0x07, 0x00, 0x00,
                0x00, 0x00,
            ]
            .as_slice(),
        )
        .unwrap();

        assert_eq!(header.prg_rom_size, 32);
        assert_eq!(header.chr_rom_size, 0);
        assert!(matches!(
            header.nametable_arrangement,
            NameTableArrangement::FourScreen
        ));
        assert!(header.has_persistent_memory);
        assert!(!header.has_trainer);
        assert!(matches!(header.mapper, Mapper::Mmc3Sharp));
        assert!(matches!(header.kind, Kind::Nes20));

        let prg_ram = header.prg_ram_definition.unwrap();
        assert_eq!(prg_ram.size, 0x2000);
        assert!(matches!(prg_ram.kind, RamKind::Nvram));

        let chr_ram = header.chr_ram_definition.unwrap();
        assert_eq!(chr_ram.size, 0x2000);
        assert!(matches!(chr_ram.kind, RamKind::Ram));
    }
}
