Documentation
use std::iter::once;
use std::{path::Path, sync::Arc};

use crate::wad::{self, Lump, Lumps, WadFile, WadKind};

/// A stack of WAD files layered on top of each other, with later files overlaying earlier ones.
/// A `Wad` usually consists of an [IWAD] overlaid with zero or more [PWADs], an ordering which is
/// enforced by the [`load`] and [`patch`] constructors. There are a set of unchecked constructors
/// if you want to bypass this constraint.
///
/// [IWAD]: WadKind::Iwad
/// [PWADs]: WadKind::Pwad
/// [`load`]: Self::load
/// [`patch`]: Self::patch
#[derive(Clone, Debug)]
#[must_use]
pub struct Wad {
    initial: Arc<WadFile>,
    patches: Vec<Arc<WadFile>>,
}

impl Wad {
    /// Loads an initial [IWAD].
    ///
    /// [IWAD]: WadKind::Iwad
    pub fn load(path: impl AsRef<Path>) -> wad::Result<Self> {
        let file = WadFile::load(path.as_ref())?;
        file.expect_kind(WadKind::Iwad)?;
        Self::new(file)
    }

    /// Loads an initial WAD without checking if it's an [IWAD].
    ///
    /// [IWAD]: WadKind::Iwad
    pub fn load_unchecked(path: impl AsRef<Path>) -> wad::Result<Self> {
        let file = WadFile::load(path.as_ref())?;
        Self::new(file)
    }

    /// Creates a stack with an initial, already loaded WAD file, which does not need to be an
    /// [IWAD].
    ///
    /// This is a low-level method. It's usually easier to call [`load`] instead and avoid dealing
    /// directly with [`WadFile`].
    ///
    /// `new` does not require the file to be an IWAD. If you want to check you can call
    /// [`expect_kind`] first.
    ///
    /// [IWAD]: WadKind::Iwad
    /// [`load`]: Self::load
    /// [`expect_kind`]: WadFile::expect_kind
    pub fn new(file: Arc<WadFile>) -> wad::Result<Self> {
        Ok(Self {
            initial: file,
            patches: Vec::new(),
        })
    }

    /// Overlays a [PWAD].
    ///
    /// [PWAD]: WadKind::Pwad
    pub fn patch(&self, path: impl AsRef<Path>) -> wad::Result<Self> {
        let file = WadFile::load(path.as_ref())?;
        file.expect_kind(WadKind::Pwad)?;
        self.add(file)
    }

    /// Overlays a WAD without checking if it's a [PWAD].
    ///
    /// [PWAD]: WadKind::Pwad
    pub fn patch_unchecked(&self, path: impl AsRef<Path>) -> wad::Result<Self> {
        let file = WadFile::load(path.as_ref())?;
        self.add(file)
    }

    /// Overlays an already loaded WAD file, which does not need to be a [PWAD].
    ///
    /// This is a low-level method. It's usually easier to call [`patch`] instead and avoid dealing
    /// directly with [`WadFile`].
    ///
    /// `add` does not require the file to be a PWAD. If you want to check you can call
    /// [`expect_kind`] first.
    ///
    /// [PWAD]: WadKind::Pwad
    /// [`patch`]: Self::patch
    /// [`expect_kind`]: WadFile::expect_kind
    pub fn add(&self, file: Arc<WadFile>) -> wad::Result<Self> {
        let mut clone = self.clone();
        clone.patches.push(file);
        Ok(clone)
    }

    /// Returns an iterator over all the files in this `Wad`. The files are in the order they were
    /// added: first the initial [`WadFile`], then each of the patches in turn. You can [reverse]
    /// the iterator if you want to see the files in the order lump lookups occur, from last to
    /// first.
    ///
    /// One should not normally need to call this function. It is mainly useful for debugging, or
    /// just to get a peek under the hood.
    ///
    /// [reverse]: Iterator::rev
    pub fn files(&self) -> impl Iterator<Item = &WadFile> + DoubleEndedIterator {
        let initial = once(&*self.initial);
        let patches = self.patches.iter().map(|p| &**p);
        initial.chain(patches)
    }

    /// Retrieves a unique lump by name. Lumps in later files override lumps from earlier ones.
    ///
    /// # Errors
    ///
    /// It is an error if the lump is missing.
    pub fn lump(&self, name: &str) -> wad::Result<Lump> {
        self.lookup(|patch| patch.try_lump(name), |initial| initial.lump(name))
    }

    /// Retrieves a unique lump by name. Lumps in later files override lumps from earlier ones.
    ///
    /// Returns `Ok(None)` if the lump is missing.
    pub fn try_lump(&self, name: &str) -> wad::Result<Option<Lump>> {
        self.try_lookup(|file| file.try_lump(name))
    }

    /// Retrieves a block of `size > 0` lumps following a unique named marker. The marker lump is
    /// included in the result. Blocks in later files override entire blocks from earlier files.
    ///
    /// # Errors
    ///
    /// It is an error if the block is missing.
    ///
    /// # Panics
    ///
    /// Panics if `size == 0`.
    pub fn lumps_following(&self, start: &str, size: usize) -> wad::Result<Lumps> {
        self.lookup(
            |patch| patch.try_lumps_following(start, size),
            |initial| initial.lumps_following(start, size),
        )
    }

    /// Retrieves a block of `size > 0` lumps following a unique named marker. The marker lump is
    /// included in the result. Blocks in later files override entire blocks from earlier files.
    ///
    /// Returns `Ok(None)` if the block is missing.
    ///
    /// # Panics
    ///
    /// Panics if `size == 0`.
    pub fn try_lumps_following(&self, start: &str, size: usize) -> wad::Result<Option<Lumps>> {
        self.try_lookup(|file| file.try_lumps_following(start, size))
    }

    /// Retrieves a block of lumps between start and end markers. The marker lumps are included in
    /// the result. Blocks in later wads override entire blocks from earlier files.
    ///
    /// # Errors
    ///
    /// It is an error if the block is missing.
    pub fn lumps_between(&self, start: &str, end: &str) -> wad::Result<Lumps> {
        self.lookup(
            |patch| patch.try_lumps_between(start, end),
            |initial| initial.lumps_between(start, end),
        )
    }

    /// Retrieves a block of lumps between start and end markers. The marker lumps are included in
    /// the result. Blocks in later wads override entire blocks from earlier files.
    ///
    /// Returns `Ok(None)` if the block is missing.
    pub fn try_lumps_between(&self, start: &str, end: &str) -> wad::Result<Option<Lumps>> {
        self.try_lookup(|file| file.try_lumps_between(start, end))
    }

    fn lookup<T>(
        &self,
        try_lookup: impl Fn(&Arc<WadFile>) -> wad::Result<Option<T>>,
        lookup: impl FnOnce(&Arc<WadFile>) -> wad::Result<T>,
    ) -> wad::Result<T> {
        for patch in self.patches.iter().rev() {
            if let Some(value) = try_lookup(patch)? {
                return Ok(value);
            }
        }

        lookup(&self.initial)
    }

    fn try_lookup<T>(
        &self,
        try_lookup: impl Fn(&Arc<WadFile>) -> wad::Result<Option<T>>,
    ) -> wad::Result<Option<T>> {
        for patch in self.patches.iter().rev() {
            if let Some(value) = try_lookup(patch)? {
                return Ok(Some(value));
            }
        }

        try_lookup(&self.initial)
    }
}

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

    #[test]
    fn not_a_wad() {
        assert_matches!(
            Wad::load("test/killer.txt"),
            Err(wad::Error::Malformed { .. })
        );
    }

    #[test]
    fn lump_data() {
        assert_eq!(DOOM_WAD.lump("DEMO1").unwrap().size(), 20118);
        assert_eq!(DOOM_WAD.lump("E1M1").unwrap().size(), 0);
    }

    #[test]
    fn detect_duplicates() {
        assert_matches!(DOOM_WAD.lump("E1M1"), Ok(_));
        assert_matches!(DOOM_WAD.lump("THINGS"), Err(_));
        assert_matches!(DOOM_WAD.lump("VERTEXES"), Err(_));
        assert_matches!(DOOM_WAD.lump("SECTORS"), Err(_));
    }

    #[test]
    fn lumps_between() {
        let sprites = DOOM_WAD.lumps_between("S_START", "S_END").unwrap();
        assert_eq!(sprites.first().name(), "S_START");
        assert_eq!(sprites.last().name(), "S_END");
        assert_eq!(sprites.len(), 485);
        assert_eq!(sprites[100].name(), "SARGB4B6");

        // Backwards.
        assert_matches!(DOOM_WAD.lumps_between("S_END", "S_START"), Err(_));
    }

    #[test]
    fn lumps_following() {
        let map = DOOM_WAD.lumps_following("E1M8", 11).unwrap();
        assert_eq!(map.len(), 11);
        assert_eq!(
            map.iter().map(Lump::name).collect::<Vec<_>>(),
            [
                "E1M8", "THINGS", "LINEDEFS", "SIDEDEFS", "VERTEXES", "SEGS", "SSECTORS", "NODES",
                "SECTORS", "REJECT", "BLOCKMAP"
            ],
        );

        // Check in and out of bounds sizes.
        assert_matches!(DOOM_WAD.try_lumps_following("E1M1", 1), Ok(Some(_)));
        assert_matches!(DOOM_WAD.try_lumps_following("E1M1", 9999), Err(_));
    }

    #[test]
    fn iwad_then_pwads() {
        // IWAD + PWAD = success.
        let _ = Wad::load(DOOM_WAD_PATH)
            .unwrap()
            .patch(KILLER_WAD_PATH)
            .unwrap();

        // IWAD + IWAD = error.
        let wad = Wad::load(DOOM_WAD_PATH).unwrap();
        assert_matches!(wad.patch(DOOM2_WAD_PATH), Err(_));

        // Can't start with a PWAD.
        assert_matches!(Wad::load(KILLER_WAD_PATH), Err(_));
    }

    #[test]
    fn no_type_checking() -> wad::Result<()> {
        // Nonsensical ordering.
        let silly_wad = Wad::load_unchecked(KILLER_WAD_PATH)?
            .patch_unchecked(DOOM2_WAD_PATH)?
            .patch_unchecked(DOOM_WAD_PATH)?
            .patch_unchecked(BIOTECH_WAD_PATH)?;

        assert_matches!(silly_wad.lump("E1M1"), Ok(_));
        assert_matches!(silly_wad.lump("MAP01"), Ok(_));

        Ok(())
    }

    #[test]
    fn layering() {
        assert_eq!(DOOM2_WAD.lump("DEMO3").unwrap().size(), 17898);
        assert_eq!(
            DOOM2_WAD
                .lumps_following("MAP01", 11)
                .unwrap()
                .iter()
                .map(|lump| (lump.name(), lump.size()))
                .collect::<Vec<_>>(),
            [
                ("MAP01", 0),
                ("THINGS", 690),
                ("LINEDEFS", 5180),
                ("SIDEDEFS", 15870),
                ("VERTEXES", 1532),
                ("SEGS", 7212),
                ("SSECTORS", 776),
                ("NODES", 5404),
                ("SECTORS", 1534),
                ("REJECT", 436),
                ("BLOCKMAP", 6418),
            ],
        );
        assert_eq!(
            DOOM2_WAD.lumps_between("S_START", "S_END").unwrap().len(),
            1383
        );

        let wad = DOOM2_WAD.patch(BIOTECH_WAD_PATH).unwrap();
        assert_eq!(wad.lump("DEMO3").unwrap().size(), 9490);
        assert_eq!(
            wad.lumps_following("MAP01", 11)
                .unwrap()
                .iter()
                .map(|lump| (lump.name(), lump.size()))
                .collect::<Vec<_>>(),
            [
                ("MAP01", 0),
                ("THINGS", 1050),
                ("LINEDEFS", 5040),
                ("SIDEDEFS", 17400),
                ("VERTEXES", 1372),
                ("SEGS", 7536),
                ("SSECTORS", 984),
                ("NODES", 6860),
                ("SECTORS", 2184),
                ("REJECT", 882),
                ("BLOCKMAP", 4362),
            ],
        );
        assert_eq!(wad.lumps_between("S_START", "S_END").unwrap().len(), 1383);
        assert_eq!(wad.lumps_between("SS_START", "SS_END").unwrap().len(), 265);
    }

    // Make sure `Wad` is `Send` and `Sync`.
    trait IsSendAndSync: Send + Sync {}
    impl IsSendAndSync for Wad {}
}