use std::{
    fs::File,
    io::{self, BufReader, Write},
    path::PathBuf,
};

use rayon::prelude::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator};

mod error;
mod header;
mod node;
pub use {
    error::Error,
    header::{FileType, Header, Owner},
    node::Node,
};

#[derive(Default)]
pub struct Archive {
    pub nodes: Vec<Node>,
}

impl Archive {
    pub fn to_vec(self) -> Result<Vec<u8>, Error> {
        let mut buf = vec![];
        for node in self.nodes {
            buf.extend(node.to_vec()?);
        }
        buf.write_all(&[0; 9216])?;
        Ok(buf)
    }

    /// Write out a vector of `TarNodes` to a file or something that implements
    /// ``std::io::Write`` and ``std::io::Copy``.
    ///
    /// # Example
    ///
    /// ```
    /// use std::fs::File;
    /// use hpk::tar::Archive;
    ///
    /// let data = Archive::new("test/1.txt").unwrap();
    ///
    /// let out = File::create("test/2.tar".to_string()).unwrap();
    /// data.write(&out).unwrap();
    /// ```
    pub fn write<T: io::Write + Copy>(self, mut input: T) -> Result<usize, Error> {
        let mut written = 0;
        for f in self.nodes.clone() {
            written += f.write(input)?;
        }

        /* Complete the write with 18 blocks of 512 ``0x00`` bytes per the specification */
        if !self.nodes.is_empty() {
            input.write_all(&[0; 9216])?;
            written += 9216;
        }

        Ok(written)
    }

    /// Create a new `TarFile` struct and initialize it with a `filename` file.
    /// This will read in the file to the `TarFile` struct as a `TarNode`.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    ///
    /// let data = Archive::new("test/1.txt").unwrap();
    /// ```
    pub fn new(filename: &str) -> Result<Self, Error> {
        Ok(Self {
            nodes: vec![Node::read_file_to_tar(filename)?],
        })
    }

    /// Append another file to the `TarFile.file` vector. This adds a file to the
    /// internal representation of the tar file.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    ///
    /// let mut data = Archive::new("test/1.txt").unwrap();
    /// data.append("test/1.txt").unwrap();
    /// ```
    pub fn append(&mut self, filename: &str) -> Result<(), Error> {
        self.nodes.push(Node::read_file_to_tar(filename)?);

        Ok(())
    }

    /// Open and load an external tar file into the internal `TarFile` struct. This
    /// parses and loads up all the files contained within the external tar file.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    ///
    /// Archive::open("test/1.tar").unwrap();
    /// ```
    pub fn open(filename: &str) -> Result<Self, Error> {
        let file = File::open(filename)?;
        let mut reader = BufReader::new(file);
        Self::read(&mut reader)
    }

    /// Read a tar archive from anything which implements `io::Read`.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    /// use std::{fs::File, io::BufReader};
    ///
    /// let file = File::open("test/1.tar").unwrap();
    /// let mut reader = BufReader::new(file);
    /// Archive::read(&mut reader).unwrap();
    /// ```
    pub fn read<T: io::Read>(mut input: T) -> Result<Self, Error> {
        let mut out = Self {
            nodes: Vec::<Node>::new(),
        };
        while let Ok(t) = Node::read(&mut input) {
            out.nodes.push(t);
        }
        Ok(out)
    }

    /// Remove the first file from the Tar that matches the filename and path.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    ///
    /// let mut data = Archive::new("test/1.tar").unwrap();
    /// data.remove("test/1.tar").unwrap();
    /// ```
    pub fn remove(&mut self, filename: &str) -> Result<bool, Error> {
        let mut name = [0u8; 100];
        name[..filename.len()].copy_from_slice(filename.as_bytes());
        if let Some(i) = &self
            .nodes
            .par_iter()
            .position_any(|x| x.header.fname == name)
        {
            self.nodes.remove(*i);
            return Ok(true);
        }

        Ok(false)
    }

    /// Get the first node from the archive that matches the given filename.
    ///
    /// # Example
    ///
    /// ```
    /// use hpk::tar::Archive;
    /// let archive = Archive::new("test/1.txt").unwrap();
    /// let node = archive.get("test/1.txt");
    /// assert!(node.is_some());
    /// ```
    pub fn get(&self, filename: &str) -> Option<Node> {
        self.nodes
            .par_iter()
            .find_any(|x| x.header.file_path() == Ok(PathBuf::from(filename)))
            .cloned()
    }

    pub fn pop(&mut self, filename: &str) -> Option<Node> {
        if let Some(i) = self
            .nodes
            .par_iter()
            .position_any(|x| x.header.file_path() == Ok(PathBuf::from(filename)))
        {
            let node = self.nodes.get(i).cloned();
            self.nodes.remove(i);
            return node;
        }
        None
    }
}

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

    #[test]
    fn open_get() {
        let archive = Archive::open("test/1.tar").unwrap();
        let node = archive.get("1.txt");
        assert!(node.is_some());
    }
}
