#![allow(clippy::similar_names)]
use crate::tar::{header::Owner, Error, FileType, Header};
use deku::prelude::*;
use std::{
    fs::{File, Metadata},
    io::{self, BufReader},
    str,
};

#[derive(Clone, Debug, Default)]
pub struct Node {
    pub header: Header,
    pub data: Vec<[u8; 512]>,
}

impl Node {
    pub fn to_vec(self) -> Result<Vec<u8>, DekuError> {
        let mut buf = self.header.to_bytes()?;
        for block in self.data {
            buf.extend(block.to_vec());
        }
        Ok(buf)
    }

    /// Write out a single file within the tar to a file or something with a
    /// ``std::io::Write`` trait.
    pub fn write<T: io::Write>(&self, mut input: T) -> Result<usize, Error> {
        input.write_all(&self.header.to_bytes()?)?;
        let mut written = 512;
        for d in &self.data {
            input.write_all(d)?;
            written += d.len();
        }

        Ok(written)
    }

    /// Read a `Node` in from a file or something with a ``std::io::Read`` trait.
    pub fn read<T: io::Read>(mut input: T) -> Result<Self, Error> {
        let mut h = vec![0u8; 512];
        input.read_exact(&mut h)?;

        let (_, header) = Header::from_bytes((&h, 0))?;
        if header == Header::default() {
            return Err(Error::EndOfTar);
        }
        if !header.validate_magic() {
            return Err(Error::InvalidMagic);
        }
        if !header.validate_checksum()? {
            return Err(Error::InvalidChecksum);
        }

        let chunks = (oct_to_dec(&header.size)? / 512) + 1;
        Ok(Node {
            header,
            data: Node::chunk_file(&mut input, Some(chunks))?,
        })
    }

    /// Open and read a file from the ``filename`` argument to a `Node`.
    pub fn read_file_to_tar(filename: &str) -> Result<Self, Error> {
        let header = Header::new(filename)?;
        if header.link_indicator[0] != FileType::Normal as u8 {
            return Ok(Node {
                header,
                data: Vec::<[u8; 512]>::new(),
            });
        }

        let file = File::open(filename)?;
        let mut reader = BufReader::new(file);
        Ok(Node {
            header,
            data: Node::chunk_file(&mut reader, None)?,
        })
    }

    /// Create a Node from in memory data, given the filename and metadata
    pub fn read_data_to_tar(
        data: &[u8],
        filename: &str,
        meta: &Metadata,
        owner: Option<Owner>,
    ) -> Result<Self, Error> {
        let header = Header::new_from_meta(filename, meta, owner)?;
        if header.link_indicator[0] != FileType::Normal as u8 {
            return Ok(Node {
                header,
                data: Vec::<[u8; 512]>::new(),
            });
        }
        let mut reader = BufReader::new(data);
        Ok(Node {
            header,
            data: Node::chunk_file(&mut reader, None)?,
        })
    }

    /// Read in and split a file into ``512`` byte chunks.
    fn chunk_file<T: std::io::Read>(
        file: &mut T,
        max_chunks: Option<usize>,
    ) -> Result<Vec<[u8; 512]>, Error> {
        /* Extract the file data from the tar file */
        let mut out = Vec::<[u8; 512]>::new();
        let mut n = if let Some(max) = max_chunks {
            max
        } else {
            usize::MAX
        };

        /* Carve out 512 bytes at a time */
        let mut buf: [u8; 512] = [0; 512];
        loop {
            let len = file.read(&mut buf)?;

            n -= 1;

            /* If read len == 0, we've hit the EOF */
            if len == 0 || n == 0 {
                break;
            }

            /* Save this chunk */
            out.push(buf);
        }
        Ok(out)
    }
}

fn oct_to_dec(input: &[u8]) -> Result<usize, Error> {
    /* Convert the &[u8] to string and remove the null byte */
    let mut s = str::from_utf8(input)?.to_string();
    s.pop();

    /* Convert to usize from octal */
    Ok(usize::from_str_radix(&s, 8)?)
}
