use binrw::BinRead;

use crate::scd::format::ScdFormat;

/// On-disk size of the [`AudioBasicDesc`] structure in bytes.
pub const AUDIO_BASIC_DESC_SIZE: usize = 32;

/// On-disk size of the [`SubInfoMarkerChunk`] header in bytes.
pub const SUB_INFO_MARKER_CHUNK_SIZE: usize = 20;

/// Per-track flags from the [`AudioBasicDesc`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct AudioFlags(u32);

impl AudioFlags {
    /// The sub-info region begins with a [`SubInfoMarkerChunk`] if this bit is set.
    pub const MARKER_CHUNK: u32 = 0x01;
    /// Audio data is split across mono channels.
    pub const MONO_SPLIT: u32 = 0x02;
    /// Shift bit used for codec-version encoding in some formats.
    pub const VERSION_SHIFT_BIT: u32 = 0x0100_0000;

    /// Wraps a raw `u32` flags word.
    #[must_use]
    pub fn from_bits(bits: u32) -> Self {
        AudioFlags(bits)
    }

    /// Returns the raw `u32` value.
    #[must_use]
    pub fn bits(self) -> u32 {
        self.0
    }

    /// Returns `true` if any bit in `mask` is set.
    #[must_use]
    pub fn contains(self, mask: u32) -> bool {
        self.0 & mask != 0
    }

    /// Returns `true` if [`MARKER_CHUNK`](Self::MARKER_CHUNK) is set.
    #[must_use]
    pub fn has_marker_chunk(self) -> bool {
        self.contains(Self::MARKER_CHUNK)
    }

    /// Returns `true` if [`MONO_SPLIT`](Self::MONO_SPLIT) is set.
    #[must_use]
    pub fn is_mono_split(self) -> bool {
        self.contains(Self::MONO_SPLIT)
    }
}

/// Fixed-size 32-byte descriptor at the start of each audio entry.
///
/// Contains the codec format, sample parameters, and sizes needed to locate the
/// sub-info and payload regions.
///
/// Obtained from [`Track::descriptor`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AudioBasicDesc {
    /// Byte size of the audio payload (codec-specific; for OGG Vorbis this excludes the OGG
    /// header region — see [`OggVorbisSeekTableHeader::ogg_header_size`]).
    pub payload_size: u32,
    /// Number of audio channels.
    pub channel_count: u32,
    /// Sample rate in Hz.
    pub sample_rate: u32,
    /// Codec format.
    pub format: ScdFormat,
    /// Loop start position in samples.
    pub loop_start: u32,
    /// Loop end position in samples (`0` = no loop / end of track).
    pub loop_end: u32,
    /// Byte size of the sub-info region (marker chunk + codec-specific header).
    pub sub_info_size: u32,
    /// Per-track flags; see [`AudioFlags`].
    pub flags: AudioFlags,
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WireAudioBasicDesc {
    pub payload_size: u32,
    pub channel_count: u32,
    pub sample_rate: u32,
    pub format: i32,
    pub loop_start: u32,
    pub loop_end: u32,
    pub sub_info_size: u32,
    pub flags: u32,
}

impl From<WireAudioBasicDesc> for AudioBasicDesc {
    fn from(w: WireAudioBasicDesc) -> Self {
        AudioBasicDesc {
            payload_size: w.payload_size,
            channel_count: w.channel_count,
            sample_rate: w.sample_rate,
            format: ScdFormat::from_raw(w.format),
            loop_start: w.loop_start,
            loop_end: w.loop_end,
            sub_info_size: w.sub_info_size,
            flags: AudioFlags::from_bits(w.flags),
        }
    }
}

/// Optional loop/marker sub-info chunk, present when [`AudioFlags::MARKER_CHUNK`] is set.
///
/// The chunk is followed by `marker_count × 4` bytes of marker position data, accessible
/// via [`Track::marker_positions_bytes`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SubInfoMarkerChunk {
    /// Chunk type identifier.
    pub chunk_id: u32,
    /// Total byte size of the chunk including this header.
    pub chunk_size: u32,
    /// Loop start position in samples.
    pub sample_loop_start: i32,
    /// Loop end position in samples.
    pub sample_loop_end: i32,
    /// Number of marker positions following this header.
    pub marker_count: i32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WireSubInfoMarkerChunk {
    pub chunk_id: u32,
    pub chunk_size: u32,
    pub sample_loop_start: i32,
    pub sample_loop_end: i32,
    pub marker_count: i32,
}

impl From<WireSubInfoMarkerChunk> for SubInfoMarkerChunk {
    fn from(w: WireSubInfoMarkerChunk) -> Self {
        SubInfoMarkerChunk {
            chunk_id: w.chunk_id,
            chunk_size: w.chunk_size,
            sample_loop_start: w.sample_loop_start,
            sample_loop_end: w.sample_loop_end,
            marker_count: w.marker_count,
        }
    }
}

/// Codec-specific header data parsed from a track's sub-info region.
///
/// Obtained from [`Track::extra_data`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum ExtraData<'a> {
    /// No codec-specific header (empty tracks or unrecognised codec with empty sub-info).
    None,
    /// MS-ADPCM `WAVEFORMATEX`-style header; see [`AdpcmWaveFormat`].
    MsAdpcm(AdpcmWaveFormat),
    /// OGG Vorbis seek-table header and seek-table bytes.
    OggVorbis {
        /// Parsed seek-table header; contains the XOR byte and OGG header size.
        header: OggVorbisSeekTableHeader,
        /// Raw seek-table bytes immediately following the header.
        seek_table: &'a [u8],
    },
    /// ATRAC9 superframe header.
    Atrac9 {
        /// Parsed ATRAC9 header.
        header: Atrac9Header,
    },
    /// PS3/Xbox MP3 seek table (only when the sub-info starts with `"STBL"`).
    Mp3 {
        /// Parsed STBL seek-table header.
        header: Ps3Mp3SeekTableHeader,
        /// Raw seek-table bytes immediately following the header.
        seek_table: &'a [u8],
    },
    /// Unrecognised codec: the sub-info bytes are returned verbatim.
    Opaque(&'a [u8]),
}

impl<'a> ExtraData<'a> {
    /// Returns the seek-table byte slice for OGG Vorbis and MP3 variants, or `None`.
    #[must_use]
    pub fn seek_table_bytes(&self) -> Option<&'a [u8]> {
        match self {
            ExtraData::OggVorbis { seek_table, .. } | ExtraData::Mp3 { seek_table, .. } => {
                Some(*seek_table)
            }
            ExtraData::None
            | ExtraData::MsAdpcm(_)
            | ExtraData::Atrac9 { .. }
            | ExtraData::Opaque(_) => None,
        }
    }
}

/// MS-ADPCM `WAVEFORMATEX`-style header from an [`ExtraData::MsAdpcm`] track.
///
/// Used by [`crate::scd::decode::msadpcm::decode_to_pcm16`] to validate and decode the
/// payload.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdpcmWaveFormat {
    /// Wave format tag; typically `2` for MS-ADPCM.
    pub format_tag: u16,
    /// Number of audio channels.
    pub channels: i16,
    /// Sample rate in Hz.
    pub samples_per_sec: i32,
    /// Average bytes per second.
    pub avg_bytes_per_sec: i32,
    /// Block alignment in bytes.
    pub block_align: i16,
    /// Bits per sample (for ADPCM, typically `4`).
    pub bits_per_sample: u16,
    /// Size in bytes of codec-specific data that follows.
    pub size: i16,
    /// Number of PCM samples per compressed block.
    pub samples_per_block: u16,
    /// Number of valid coefficient pairs in [`coefficients`](Self::coefficients).
    pub num_coef: u16,
    /// Predictor coefficient pairs (up to 7 pairs = 14 values).
    pub coefficients: [i16; 14],
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WireAdpcmWaveFormat {
    pub format_tag: u16,
    pub channels: i16,
    pub samples_per_sec: i32,
    pub avg_bytes_per_sec: i32,
    pub block_align: i16,
    pub bits_per_sample: u16,
    pub size: i16,
    pub samples_per_block: u16,
    pub num_coef: u16,
    pub coefficients: [i16; 14],
}

impl From<WireAdpcmWaveFormat> for AdpcmWaveFormat {
    fn from(w: WireAdpcmWaveFormat) -> Self {
        AdpcmWaveFormat {
            format_tag: w.format_tag,
            channels: w.channels,
            samples_per_sec: w.samples_per_sec,
            avg_bytes_per_sec: w.avg_bytes_per_sec,
            block_align: w.block_align,
            bits_per_sample: w.bits_per_sample,
            size: w.size,
            samples_per_block: w.samples_per_block,
            num_coef: w.num_coef,
            coefficients: w.coefficients,
        }
    }
}

/// On-disk size of the [`AdpcmWaveFormat`] header in bytes.
pub const ADPCM_WAVE_FORMAT_SIZE: usize = 50;

/// Seek-table header for [`ScdFormat::OggVorbis`] tracks.
///
/// The `xor_byte` and `version` fields control deobfuscation:
///
/// - Version 2: if the payload does **not** start with `OggS`, each byte of the OGG header
///   region is XOR'd with `xor_byte`.  Modern game data (7.x) omits obfuscation and passes
///   through unchanged.
/// - Version 3: the entire payload is XOR'd using a position-dependent table derived from
///   the body size.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OggVorbisSeekTableHeader {
    /// Obfuscation version (`2` or `3`; any other value is a no-op).
    pub version: u8,
    /// On-disk size of this structure.
    pub struct_size: u8,
    /// XOR byte used in version-2 deobfuscation.
    pub xor_byte: u8,
    /// Unknown reserved bytes.
    pub unknown: [u8; 9],
    /// Seek-table step size.
    pub step: f32,
    /// Byte size of the seek-table that follows this header.
    pub seek_table_size: u32,
    /// Byte size of the OGG header region that precedes the audio payload.
    pub ogg_header_size: u32,
    /// Padding / reserved.
    pub padding: [u32; 2],
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WireOggVorbisSeekTableHeader {
    pub version: u8,
    pub struct_size: u8,
    pub xor_byte: u8,
    pub unknown: [u8; 9],
    pub step: f32,
    pub seek_table_size: u32,
    pub ogg_header_size: u32,
    pub padding: [u32; 2],
}

impl From<WireOggVorbisSeekTableHeader> for OggVorbisSeekTableHeader {
    fn from(w: WireOggVorbisSeekTableHeader) -> Self {
        OggVorbisSeekTableHeader {
            version: w.version,
            struct_size: w.struct_size,
            xor_byte: w.xor_byte,
            unknown: w.unknown,
            step: w.step,
            seek_table_size: w.seek_table_size,
            ogg_header_size: w.ogg_header_size,
            padding: w.padding,
        }
    }
}

/// On-disk size of the [`OggVorbisSeekTableHeader`] in bytes.
pub const OGG_VORBIS_SEEK_TABLE_HEADER_SIZE: usize = 32;

/// PS3/Xbox MP3 seek-table header, identified by the `"STBL"` magic in the sub-info region.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Ps3Mp3SeekTableHeader {
    /// Magic identifier; always `b"STBL"`.
    pub id: [u8; 4],
    /// Table version.
    pub version: u8,
    /// Reserved bytes.
    pub reserved: [u8; 3],
    /// Step size between seek-table entries.
    pub step: f32,
    /// Number of `u32` seek-table entries that follow.
    pub num_element: i32,
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WirePs3Mp3SeekTableHeader {
    pub id: [u8; 4],
    pub version: u8,
    pub reserved: [u8; 3],
    pub step: f32,
    pub num_element: i32,
}

impl From<WirePs3Mp3SeekTableHeader> for Ps3Mp3SeekTableHeader {
    fn from(w: WirePs3Mp3SeekTableHeader) -> Self {
        Ps3Mp3SeekTableHeader {
            id: w.id,
            version: w.version,
            reserved: w.reserved,
            step: w.step,
            num_element: w.num_element,
        }
    }
}

/// On-disk size of the [`Ps3Mp3SeekTableHeader`] in bytes.
pub const PS3_MP3_SEEK_TABLE_HEADER_SIZE: usize = 16;

/// ATRAC9 codec header from an [`ExtraData::Atrac9`] track.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Atrac9Header {
    /// Header version.
    pub version: u16,
    /// On-disk size of this structure in bytes.
    pub struct_size: u16,
    /// Byte size of a single ATRAC9 superframe.
    pub superframe_bytes: u16,
    /// Number of PCM samples per superframe.
    pub superframe_samples: u16,
    /// Unknown field.
    pub unknown: u32,
    /// ATRAC9 encoder configuration data word.
    pub config_data: u32,
    /// Total number of PCM samples in the track.
    pub sample_count: u32,
    /// Unknown field.
    pub unknown0: u32,
    /// Encoder delay in samples.
    pub sample_delay: u32,
    /// Sample rate in Hz.
    pub sample_rate: u32,
    /// Loop start position in samples.
    pub sample_loop_start: u32,
    /// Loop end position in samples.
    pub sample_loop_end: u32,
    /// Padding / reserved.
    pub padding: [u32; 2],
}

#[derive(Debug, BinRead)]
#[brw(little)]
pub(crate) struct WireAtrac9Header {
    pub version: u16,
    pub struct_size: u16,
    pub superframe_bytes: u16,
    pub superframe_samples: u16,
    pub unknown: u32,
    pub config_data: u32,
    pub sample_count: u32,
    pub unknown0: u32,
    pub sample_delay: u32,
    pub sample_rate: u32,
    pub sample_loop_start: u32,
    pub sample_loop_end: u32,
    pub padding: [u32; 2],
}

impl From<WireAtrac9Header> for Atrac9Header {
    fn from(w: WireAtrac9Header) -> Self {
        Atrac9Header {
            version: w.version,
            struct_size: w.struct_size,
            superframe_bytes: w.superframe_bytes,
            superframe_samples: w.superframe_samples,
            unknown: w.unknown,
            config_data: w.config_data,
            sample_count: w.sample_count,
            unknown0: w.unknown0,
            sample_delay: w.sample_delay,
            sample_rate: w.sample_rate,
            sample_loop_start: w.sample_loop_start,
            sample_loop_end: w.sample_loop_end,
            padding: w.padding,
        }
    }
}

/// On-disk size of the [`Atrac9Header`] in bytes.
pub const ATRAC9_HEADER_SIZE: usize = 48;

/// A single parsed audio track from an [`Scd`](crate::scd::parser::Scd) file.
///
/// Holds references into the original byte slice.  Provides:
///
/// - Codec metadata via [`Track::descriptor`], [`Track::format`], [`Track::sample_rate`], etc.
/// - Raw bytes via [`Track::raw_bytes`] (suitable for passing to codec libraries).
/// - Decoding via [`Track::decode_to_pcm16`] (MS-ADPCM only) and
///   [`Track::to_ogg_bytes`] (OGG Vorbis with deobfuscation).
#[derive(Debug, Clone, Copy)]
pub struct Track<'a> {
    desc: AudioBasicDesc,
    marker_chunk: Option<SubInfoMarkerChunk>,
    marker_positions: &'a [u8],
    extra_data: ExtraData<'a>,
    sub_info_bytes: &'a [u8],
    raw_bytes: &'a [u8],
    descriptor_offset: u32,
}

impl<'a> Track<'a> {
    /// Constructs a `Track` from its constituent parts; called by [`crate::scd::parser`].
    pub(crate) fn new(
        desc: AudioBasicDesc,
        marker_chunk: Option<SubInfoMarkerChunk>,
        marker_positions: &'a [u8],
        extra_data: ExtraData<'a>,
        sub_info_bytes: &'a [u8],
        raw_bytes: &'a [u8],
        descriptor_offset: u32,
    ) -> Self {
        Self {
            desc,
            marker_chunk,
            marker_positions,
            extra_data,
            sub_info_bytes,
            raw_bytes,
            descriptor_offset,
        }
    }

    /// Returns the fixed-size descriptor for this track.
    #[must_use]
    pub fn descriptor(&self) -> &AudioBasicDesc {
        &self.desc
    }

    /// Returns the codec format.
    #[must_use]
    pub fn format(&self) -> ScdFormat {
        self.desc.format
    }

    /// Returns the sample rate in Hz.
    #[must_use]
    pub fn sample_rate(&self) -> u32 {
        self.desc.sample_rate
    }

    /// Returns the number of audio channels.
    #[must_use]
    pub fn channel_count(&self) -> u32 {
        self.desc.channel_count
    }

    /// Returns the loop start sample position.
    #[must_use]
    pub fn loop_start(&self) -> u32 {
        self.desc.loop_start
    }

    /// Returns the loop end sample position (`0` = no loop).
    #[must_use]
    pub fn loop_end(&self) -> u32 {
        self.desc.loop_end
    }

    /// Returns the total sample count, if available.
    ///
    /// Currently only populated for [`ScdFormat::Atrac9`] tracks; returns `None` for all others.
    #[must_use]
    pub fn sample_count(&self) -> Option<u32> {
        match self.extra_data {
            ExtraData::Atrac9 { header: h } => Some(h.sample_count),
            _ => None,
        }
    }

    /// Returns the codec-specific extra data parsed from the sub-info region.
    #[must_use]
    pub fn extra_data(&self) -> &ExtraData<'a> {
        &self.extra_data
    }

    /// Returns the [`SubInfoMarkerChunk`] if present (i.e. [`AudioFlags::MARKER_CHUNK`] was set).
    #[must_use]
    pub fn marker_chunk(&self) -> Option<&SubInfoMarkerChunk> {
        self.marker_chunk.as_ref()
    }

    /// Returns the raw marker position bytes following the [`SubInfoMarkerChunk`] header.
    #[must_use]
    pub fn marker_positions_bytes(&self) -> &'a [u8] {
        self.marker_positions
    }

    /// Returns the full raw sub-info region bytes.
    #[must_use]
    pub fn sub_info_bytes(&self) -> &'a [u8] {
        self.sub_info_bytes
    }

    /// Returns the raw audio payload bytes.
    ///
    /// For OGG Vorbis tracks this includes the OGG header region
    /// (`ogg_header_size` bytes) prepended to the declared payload; call
    /// [`Track::to_ogg_bytes`] to obtain a deobfuscated, self-contained OGG stream.
    #[must_use]
    pub fn raw_bytes(&self) -> &'a [u8] {
        self.raw_bytes
    }

    /// Returns the byte offset of this track's [`AudioBasicDesc`] within the original SCD bytes.
    ///
    /// Equal to the corresponding entry in [`Scd::audio_offsets`](crate::scd::parser::Scd::audio_offsets).
    #[must_use]
    pub fn descriptor_offset(&self) -> u32 {
        self.descriptor_offset
    }
}

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

    #[test]
    fn fixed_sizes_match_wire_format() {
        assert_eq!(AUDIO_BASIC_DESC_SIZE, 32);
        assert_eq!(SUB_INFO_MARKER_CHUNK_SIZE, 20);
        assert_eq!(ADPCM_WAVE_FORMAT_SIZE, 50);
        assert_eq!(OGG_VORBIS_SEEK_TABLE_HEADER_SIZE, 32);
        assert_eq!(PS3_MP3_SEEK_TABLE_HEADER_SIZE, 16);
        assert_eq!(ATRAC9_HEADER_SIZE, 48);
    }

    #[test]
    fn audio_flags_named_bits_recognised() {
        let f = AudioFlags::from_bits(AudioFlags::MARKER_CHUNK | AudioFlags::MONO_SPLIT);
        assert!(f.has_marker_chunk());
        assert!(f.is_mono_split());
        assert!(!f.contains(AudioFlags::VERSION_SHIFT_BIT));
        assert_eq!(f.bits(), AudioFlags::MARKER_CHUNK | AudioFlags::MONO_SPLIT);
    }

    #[test]
    fn extra_data_seek_table_bytes_routes_per_variant() {
        let bytes: &[u8] = &[0u8; 8];
        let ogg = ExtraData::OggVorbis {
            header: OggVorbisSeekTableHeader {
                version: 2,
                struct_size: 32,
                xor_byte: 0,
                unknown: [0; 9],
                step: 1.0,
                seek_table_size: 8,
                ogg_header_size: 0,
                padding: [0; 2],
            },
            seek_table: bytes,
        };
        assert_eq!(ogg.seek_table_bytes().map(<[u8]>::len), Some(8));
        assert!(ExtraData::None.seek_table_bytes().is_none());
        assert!(ExtraData::Opaque(bytes).seek_table_bytes().is_none());
    }
}
