use std::io::{self, Read, Seek};

use crate::tar::TarHeader;

pub struct TarReader<R: Read + Seek> {
    input: R,
    end_of_archive: bool,
    position: u64,
}

pub struct TarReaderEntry<'a, R: Read + Seek> {
    reader: &'a mut TarReader<R>,
    position: u64,
    end: u64,
}

impl<R: Read + Seek> TarReader<R> {
    pub fn new(input: R) -> Self {
        Self {
            input,
            position: 0,
            end_of_archive: false,
        }
    }

    pub fn next(&mut self) -> Result<Option<(TarHeader, TarReaderEntry<'_, R>)>, io::Error> {
        let mut have_empty = false;

        while !self.end_of_archive {
            self.input.seek(io::SeekFrom::Start(self.position))?;

            let mut bytes = [0; 512];
            self.input.read_exact(&mut bytes)?;
            let header = bytemuck::from_bytes::<TarHeader>(&bytes);

            if header.is_empty() {
                if have_empty {
                    self.end_of_archive = true;
                    break;
                } else {
                    have_empty = true;
                    continue;
                }
            }

            let entry_size = header.size.as_usize().unwrap();
            let skip = (entry_size + 1023) & !511;

            let position = self.position + 512;
            let end = self.position + 512 + entry_size as u64;

            self.position += skip as u64;

            return Ok(Some((
                *header,
                TarReaderEntry {
                    reader: self,
                    position,
                    end,
                },
            )));
        }

        Ok(None)
    }
}

impl<R: Read + Seek> Read for TarReaderEntry<'_, R> {
    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
        let amount = buf.len().min((self.end - self.position) as usize);
        if amount != 0 {
            self.reader.input.seek(io::SeekFrom::Start(self.position))?;
            let result = self.reader.input.read(&mut buf[..amount])?;
            self.position += result as u64;
            Ok(result)
        } else {
            Ok(0)
        }
    }
}
