// SPDX-License-Identifier: LGPL-2.1-or-later OR GPL-2.0-or-later OR MPL-2.0
// SPDX-FileCopyrightText: 2026 Gabriel Marcano <gabemarcano@yahoo.com>

use crate::error::Error;

use std::fmt;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str;

use byteorder::LittleEndian;
use byteorder::ReadBytesExt;

/// Represents the ROM access speeds.
///
/// SNES/SFC ROMs were made with two different access speeds. Slow ROMs is clocked at
/// 2.68MHz, while Fast ROMs are clocked at 3.58MHz.
#[derive(Debug)]
pub enum Speed {
    Slow,
    Fast,
}

impl fmt::Display for Speed {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Slow => write!(fmt, "slow")?,
            Self::Fast => write!(fmt, "fast")?,
        }
        Ok(())
    }
}

/// Represents the memory map mode of the ROM.
#[derive(Debug)]
pub enum Mode {
    Lorom,
    SDD1,
    Hirom,
    SA1,
    Exhirom,
}

impl fmt::Display for Mode {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Lorom => write!(fmt, "LoROM")?,
            Self::SDD1 => write!(fmt, "S-DD1")?,
            Self::Hirom => write!(fmt, "HiROM")?,
            Self::SA1 => write!(fmt, "SA-1")?,
            Self::Exhirom => write!(fmt, "ExHiROM")?,
        }
        Ok(())
    }
}

/// Represents the chipset of the game cartridge.
///
/// This represents the types of chips that are on the cartridge.
#[derive(Debug)]
pub enum Chipset {
    RomOnly,
    RomRam,
    RomRamBattery,
    RomCoprocessor,
    RomCoprocessorRam,
    RomCoprocessorRamBattery,
    RomCoprocessorBattery,
}

impl fmt::Display for Chipset {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::RomOnly => write!(fmt, "ROM only")?,
            Self::RomRam => write!(fmt, "ROM + RAM")?,
            Self::RomRamBattery => write!(fmt, "ROM + RAM + battery")?,
            Self::RomCoprocessor => write!(fmt, "ROM + coprocessor")?,
            Self::RomCoprocessorRam => write!(fmt, "ROM + coprocessor + RAM")?,
            Self::RomCoprocessorRamBattery => write!(fmt, "ROM + coprocessor + RAM + battery")?,
            Self::RomCoprocessorBattery => write!(fmt, "ROM + coprocessor + battery")?,
        }
        Ok(())
    }
}

/// Represents game cartridge coprocessors.
#[derive(Debug)]
pub enum Coprocessor {
    None,
    DSP,
    SuperFX,
    OBC1,
    SA1,
    SDD1,
    SRTC,
    Other,
    Custom,
}

impl fmt::Display for Coprocessor {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::None => write!(fmt, "None")?,
            Self::DSP => write!(fmt, "DSP")?,
            Self::SuperFX => write!(fmt, "GSU/SuperFX")?,
            Self::OBC1 => write!(fmt, "OBC1")?,
            Self::SA1 => write!(fmt, "SA-1")?,
            Self::SDD1 => write!(fmt, "S-DD1")?,
            Self::SRTC => write!(fmt, "S-RTC")?,
            Self::Other => write!(fmt, "other")?,
            Self::Custom => write!(fmt, "custom")?,
        }
        Ok(())
    }
}

/// The region the cartridge is meant for.
#[derive(Debug)]
pub enum Region {
    Japan,
    USA,
    Europe,
}

impl fmt::Display for Region {
    fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
        match self {
            Self::Japan => write!(fmt, "Japan")?,
            Self::USA => write!(fmt, "USA")?,
            Self::Europe => write!(fmt, "Europe")?,
        }
        Ok(())
    }
}

/// Interrupt vectors when running in native mode.
#[derive(Debug)]
pub struct NativeVectors {
    pub cop: u16,
    pub brk: u16,
    pub abort: u16,
    pub nmi: u16,
    pub irq: u16,
}

/// Interrupt vectors when running in emulation mode.
///
/// The SNES/SFC boots in emulation mode.
#[derive(Debug)]
pub struct EmulationVectors {
    pub cop: u16,
    pub abort: u16,
    pub nmi: u16,
    pub reset: u16,
    pub irq_brk: u16,
}

/// The complete set of interrupt vectors of the SNES/SFC.
#[derive(Debug)]
pub struct Vectors {
    pub native: NativeVectors,
    pub emulation: EmulationVectors,
}

/// Represents the metadata held by the SFC internal header.
#[derive(Debug)]
pub struct Metadata {
    /// The title of the cartridge.
    ///
    /// This is no more than 21 ASCII bytes. On the cartridge any unused bytes are replaced with
    /// spaces, but in this struct we only store the used bytes.
    pub title: String,
    /// The speed setting of the ROM.
    pub speed: Speed,
    /// The memory mapping mode of the cartridge.
    pub mode: Mode,
    /// The chipset of the cartridge.
    pub chipset: Chipset,
    /// The coprocessor on the cartridge, if any.
    pub coprocessor: Coprocessor,
    /// The size of the ROM, in KiB.
    pub rom_size: u32,
    /// The size of the RAM, in KiB.
    pub save_ram_size: u32,
    /// The region the cartridge is released for.
    pub region: Region,
    /// The developer id.
    pub dev_id: u8,
    /// The version of the ROM.
    pub version: String,
    /// The checksum of the ROM.
    pub checksum: u16,
    /// Interrupt vectors.
    pub vectors: Vectors,
    /// Additional metadata, only available if the developer ID is equal to 0x33.
    pub expanded_header: Option<ExpandedHeader>,
}

/// Represents additional metadata held by the SFC internal header.
///
/// This metadata is available if the developer ID is equal to 0x33.
#[derive(Debug)]
pub struct ExpandedHeader {
    /// 2 bytes ASCII maker code.
    pub maker_code: String,
    /// 4 byte ASCII game code.
    pub game_code: String,
    /// Expansion flash size, in KiB.
    pub expansion_flash_size: u32,
    /// Expansion RAM size, in KiB.
    pub expansion_ram_size: u32,
    /// Special version.
    pub special_version: u8,
    /// Chipset subtype.
    pub chipset_subtype: u8,
}

/// Compute the SFC checksum
///
/// SFC checksum is computed by summing all of the bytes of the file into an unsigned 16-bit
/// counter. If the data isn't a power of two, the data is sub-divided into two parts: the first
/// large portion is equal in size to the largest power of two that fits into the current size, and
/// then there's the rest. The rest is repeated as many times until the total new size is equal to
/// the next power of two.
fn compute_sfc_checksum(data: &[u8]) -> u16 {
    let mut checksum = 0u16;
    for byte in data {
        checksum = checksum.wrapping_add(u16::from(*byte));
    }
    let size = data.len();
    // If it's already power of two size, we can exit
    if size == 0 || size.is_power_of_two() {
        return checksum;
    }

    //Otherwise, we need to pad until we're at a power of two size.
    let rom1_size = 1 << size.ilog2();
    let rom2_size = size - rom1_size;
    let full_size = rom1_size << 1;
    let mirror_size = full_size - size;
    let times = mirror_size / rom2_size;
    for _ in 0..times {
        for byte in &data[rom1_size..] {
            checksum = checksum.wrapping_add(u16::from(*byte));
        }
    }
    checksum
}

impl Metadata {
    /// Returns a new Metadata object, after parsing the header.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
    pub fn try_from<T: Read + Seek>(io: &mut T) -> Result<Self, Error> {
        io.read_sfc_metadata()
    }

    /// Computes the checksum of the SFC file.
    ///
    /// SFC checksum is computed by summing all of the bytes of the file into an unsigned 16-bit
    /// counter.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
    pub fn compute_checksum<T: Read + Seek>(&self, io: &mut T) -> Result<u16, Error> {
        io.seek(SeekFrom::Start(0))?;
        let mut data = Vec::new();
        // It's faster to read in the entire SFC... on modern systems, the max size fits in memory
        // just fine, so just read it all
        io.read_to_end(&mut data)?;
        Ok(compute_sfc_checksum(&data))
    }
}

/// Trims null and whitespace characters (in that order) from the given string.
fn trim(string: &str) -> &str {
    string.trim_matches('\0').trim()
}

/// Checks that the header has valid characters.
fn check_header_name(io: &mut (impl Read + Seek), location: u32) -> bool {
    if io.seek(SeekFrom::Start((location + 0x10).into())).is_err() {
        return false;
    }
    let mut header = [0u8; 21];
    if io.read_exact(&mut header).is_err() {
        return false;
    }

    if !header.is_ascii() {
        return false;
    }

    let name = str::from_utf8(&header);
    name.is_ok()
}

/// Checks the CRC of the file against the computed CRC of the file.
fn check_crc(io: &mut (impl Read + Seek), data: &[u8], location: u32) -> bool {
    if io.seek(SeekFrom::Start((location + 0x2E).into())).is_err() {
        return false;
    }
    let crc = io.read_u16::<LittleEndian>();
    if crc.is_err() {
        return false;
    }
    let crc = crc.unwrap();
    crc == compute_sfc_checksum(data)
}

/// Finds the SFC header.
///
/// SFC headers are usually in one of three places:
///  - `LoROM`:   $007FB0
///  - `HiROM`:   $00FFB0
///  - `ExHiROM`: $40FFB0
///
/// We try to find the location by checking to see if the CRC passes, and if not, if the title
/// check passes.
///
/// One implementation note, we scan `ExHiROM`, `HiROM`, and `LoROM` in that order. Apparently there are
/// some titles that include some kind of header in both their `LoROM` and `HiROM` locations, but only
/// the `HiROM` one is actually valid.
///
/// # Errors
///
/// Returns [`Error::Io`] if an IO error took place while reading the file. If no header is found,
/// returns [`Error::Parse`].
fn find_header(io: &mut (impl Read + Seek)) -> Result<u32, Error> {
    io.seek(SeekFrom::Start(0))?;
    let mut data = Vec::new();
    // It's faster to read in the entire SFC... on modern systems, the max size fits in memory
    // just fine, so just read it all
    io.read_to_end(&mut data)?;
    let data = data;

    let valid_locations = [0x0040_FFB0_u32, 0x0000_FFB0_u32, 0x0000_7FB0_u32];
    for location in &valid_locations {
        if check_crc(io, &data, *location) {
            return Ok(*location);
        }
    }
    // If we failed to find a good CRC, try finding a good title
    for location in &valid_locations {
        if check_header_name(io, *location) {
            return Ok(*location);
        }
    }
    Err(Error::Parse("failed to find header".into()))
}

pub trait MetadataRead {
    /// Parses the  file metadata from the object provided, returning a Metadata object with the
    ///  file metadata.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Parse`] if the header cannot be found or if a field in the header contains
    /// an unexpected value.
    /// Returns [`Error::Io`] if an IO error took place while reading from the file.
    fn read_sfc_metadata(&mut self) -> Result<Metadata, Error>;
}

impl TryFrom<u8> for Mode {
    type Error = Error;

    fn try_from(data: u8) -> Result<Self, Self::Error> {
        match data & 0xF {
            0 => Ok(Self::Lorom),
            1 => Ok(Self::Hirom),
            2 => Ok(Self::SDD1), // Star Ocean apparently is this
            3 => Ok(Self::SA1),
            5 => Ok(Self::Exhirom),
            _ => Err(Self::Error::Parse("failed to parse cart mode".into())),
        }
    }
}

impl From<u8> for Speed {
    fn from(data: u8) -> Self {
        if ((data >> 4) & 0x1) == 1 {
            Self::Fast
        } else {
            Self::Slow
        }
    }
}

impl TryFrom<u8> for Chipset {
    type Error = Error;

    fn try_from(data: u8) -> Result<Self, Self::Error> {
        match data & 0xF {
            0x0 => Ok(Self::RomOnly),
            0x1 => Ok(Self::RomRam),
            0x2 => Ok(Self::RomRamBattery),
            0x3 => Ok(Self::RomCoprocessor),
            0x4 => Ok(Self::RomCoprocessorRam),
            0x5 => Ok(Self::RomCoprocessorRamBattery),
            0x6 => Ok(Self::RomCoprocessorBattery),
            _ => Err(Self::Error::Parse("failed to parse cart chipset".into())),
        }
    }
}

impl TryFrom<u8> for Coprocessor {
    type Error = Error;

    fn try_from(data: u8) -> Result<Self, Self::Error> {
        if data <= 2 {
            return Ok(Self::None);
        }
        match data >> 4 {
            0x0 => Ok(Self::DSP),
            0x1 => Ok(Self::SuperFX),
            0x2 => Ok(Self::OBC1),
            0x3 => Ok(Self::SA1),
            0x4 => Ok(Self::SDD1),
            0x5 => Ok(Self::SRTC),
            0xE => Ok(Self::Other),
            0xF => Ok(Self::Custom),
            _ => Err(Self::Error::Parse(
                "failed to parse cart coprocessor".into(),
            )),
        }
    }
}

impl TryFrom<u8> for Region {
    type Error = Error;

    fn try_from(data: u8) -> Result<Self, Self::Error> {
        match data {
            0x00 => Ok(Self::Japan),
            0x01 => Ok(Self::USA),
            0x02 => Ok(Self::Europe),
            _ => Err(Self::Error::Parse("failed to parse cart region".into())),
        }
    }
}

impl TryFrom<&[u8]> for Vectors {
    type Error = Error;

    fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
        Ok(Self {
            native: NativeVectors {
                cop: (&data[4..6]).read_u16::<LittleEndian>()?,
                brk: (&data[6..8]).read_u16::<LittleEndian>()?,
                abort: (&data[8..0xA]).read_u16::<LittleEndian>()?,
                nmi: (&data[0xA..0xC]).read_u16::<LittleEndian>()?,
                irq: (&data[0xE..0x10]).read_u16::<LittleEndian>()?,
            },
            emulation: EmulationVectors {
                cop: (&data[0x14..0x16]).read_u16::<LittleEndian>()?,
                abort: (&data[0x18..0x1A]).read_u16::<LittleEndian>()?,
                nmi: (&data[0x1A..0x1C]).read_u16::<LittleEndian>()?,
                reset: (&data[0x1C..0x1E]).read_u16::<LittleEndian>()?,
                irq_brk: (&data[0x1E..0x20]).read_u16::<LittleEndian>()?,
            },
        })
    }
}

/// Parse the version of the ROM.
///
/// It's just "1.[whatever the version flag is]".
fn parse_version(data: u8) -> String {
    format!("1.{data}")
}

impl<T: Read + Seek> MetadataRead for T {
    fn read_sfc_metadata(&mut self) -> Result<Metadata, Error> {
        // Check file header, and 2 magic bytes
        let location = find_header(self)?;
        self.seek(SeekFrom::Start(location.into()))?;
        let mut data = [0u8; 80];
        self.read_exact(&mut data)?;

        let dev_id = data[0x2A];
        let expanded_header = if dev_id == 0x33 {
            // Uses extended header
            let maker_code = &data[0..2];
            let game_code = &data[2..6];
            let maker_code = if maker_code.is_ascii() {
                str::from_utf8(maker_code)?.to_string()
            } else {
                return Err(Error::Parse("maker code is not ASCII".into()));
            };

            let game_code = if game_code.is_ascii() {
                str::from_utf8(game_code)?.to_string()
            } else {
                return Err(Error::Parse("game code is not ASCII".into()));
            };

            Some(ExpandedHeader {
                maker_code,
                game_code,
                expansion_flash_size: 1 << data[0xC],
                expansion_ram_size: 1 << data[0xD],
                special_version: data[0xE],
                chipset_subtype: data[0xF],
            })
        } else {
            None
        };

        if data[0x27] > 31 || data[0x28] > 31 {
            return Err(Error::Parse("cart size too large".into()));
        }

        let rom_size: u32 = match data[0x27] {
            0 => 0,
            x => 1 << x,
        };
        let save_ram_size: u32 = match data[0x28] {
            0 => 0,
            x => 1 << x,
        };

        let title = &data[0x10..0x25];
        let title = if title.is_ascii() {
            trim(str::from_utf8(title)?).to_string()
        } else {
            return Err(Error::Parse("title is not ASCII".into()));
        };

        Ok(Metadata {
            title,
            speed: data[0x25].into(),
            mode: data[0x25].try_into()?,
            chipset: data[0x26].try_into()?,
            coprocessor: data[0x26].try_into()?,
            rom_size,
            save_ram_size,
            region: data[0x29].try_into()?,
            dev_id: data[0x2A],
            version: parse_version(data[0x2B]),
            checksum: (&data[0x2E..0x30]).read_u16::<LittleEndian>()?,
            vectors: data[0x30..0x50].try_into()?,
            expanded_header,
        })
    }
}

#[cfg(test)]
mod tests {
    use crate::metadata::Chipset;
    use crate::metadata::Coprocessor;
    use crate::metadata::Metadata;
    use crate::metadata::MetadataRead;
    use crate::metadata::Mode;
    use crate::metadata::Region;
    use crate::metadata::Speed;

    use std::env;
    use std::fs::File;
    use std::io::Cursor;
    use std::io::Read;
    use std::path::Path;

    fn setup(filename: &str) -> Cursor<Vec<u8>> {
        let root = env::var("CARGO_MANIFEST_DIR").unwrap();
        let test_dir = Path::new(&root).join("resources/test");
        let mut file = File::open(test_dir.join(filename)).unwrap();
        let mut data = Vec::new();
        file.read_to_end(&mut data).unwrap();
        Cursor::new(data)
    }

    fn test_parse(data: &mut Cursor<Vec<u8>>) -> Metadata {
        let cart = data.read_sfc_metadata();
        assert!(cart.is_ok());
        cart.unwrap()
    }

    #[test]
    fn test_lorom() {
        let mut sfc = setup("lorom.sfc");
        let cart = test_parse(&mut sfc);

        assert!(cart.title == "LoROM Test");
        assert!(matches!(cart.speed, Speed::Slow));
        assert!(matches!(cart.mode, Mode::Lorom));
        assert!(matches!(cart.chipset, Chipset::RomRam));
        assert!(matches!(cart.coprocessor, Coprocessor::None));
        assert!(cart.rom_size == 4096);
        assert!(cart.save_ram_size == 32);
        assert!(matches!(cart.region, Region::USA));
        assert!(cart.dev_id == 0xFF);
        assert!(cart.version == "1.3");
        assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
    }

    #[test]
    fn test_hirom() {
        let mut sfc = setup("hirom.sfc");
        let cart = test_parse(&mut sfc);
        assert!(cart.title == "HiROM Test");
        assert!(matches!(cart.speed, Speed::Slow));
        assert!(matches!(cart.mode, Mode::Lorom));
        assert!(matches!(cart.chipset, Chipset::RomRam));
        assert!(matches!(cart.coprocessor, Coprocessor::None));
        assert!(cart.rom_size == 4096);
        assert!(cart.save_ram_size == 32);
        assert!(matches!(cart.region, Region::USA));
        assert!(cart.dev_id == 0xFF);
        assert!(cart.version == "1.3");
        assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
    }

    #[test]
    fn test_exhirom() {
        let mut sfc = setup("exhirom.sfc");
        let cart = test_parse(&mut sfc);
        assert!(cart.title == "ExHiROM Test");
        assert!(matches!(cart.speed, Speed::Slow));
        assert!(matches!(cart.mode, Mode::Lorom));
        assert!(matches!(cart.chipset, Chipset::RomRam));
        assert!(matches!(cart.coprocessor, Coprocessor::None));
        assert!(cart.rom_size == 4096);
        assert!(cart.save_ram_size == 32);
        assert!(matches!(cart.region, Region::USA));
        assert!(cart.dev_id == 0xFF);
        assert!(cart.version == "1.3");
        assert!(cart.checksum == cart.compute_checksum(&mut sfc).unwrap());
    }
}
