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

use crate::tar::{TarHeader, TarOctal, TarString};

pub struct TarWriter<W: Write> {
    output: W,
}

pub struct TarFileWriter<'a, W: Write> {
    writer: &'a mut TarWriter<W>,
    written: u64,
    size: u64,
}

pub struct TarNewFileMetadata {
    pub uid: u32,
    pub gid: u32,
    pub mode: u32,
}

impl<W: Write> TarWriter<W> {
    pub fn new(output: W) -> Self {
        Self { output }
    }

    pub fn finish(&mut self) -> io::Result<()> {
        // Two empty blocks
        let empty = [0; 512];
        self.output.write_all(&empty)?;
        self.output.write_all(&empty)?;
        self.output.flush()
    }

    pub fn begin_file(
        &mut self,
        path: &str,
        size: u64,
        metadata: TarNewFileMetadata,
    ) -> io::Result<TarFileWriter<'_, W>> {
        // TODO error reporting
        // TODO long filenames
        let header = TarHeader {
            name: TarString::new(path).unwrap(),
            mode: TarOctal::from_u32(metadata.mode).unwrap(),
            uid: TarOctal::from_u32(metadata.uid).unwrap(),
            gid: TarOctal::from_u32(metadata.gid).unwrap(),
            size: TarOctal::from_u64(size).unwrap(),
            mtime: TarOctal::zero(),
            checksum: TarOctal::zero(),
            file_type: b'0',
            link: TarString::EMPTY,
            ustar_magic: *b"ustar\0",
            ustar_version: *b"00",
            uid_name: TarString::EMPTY,
            gid_name: TarString::EMPTY,
            dev_major: TarOctal::zero(),
            dev_minor: TarOctal::zero(),
            filename_prefix: TarString::EMPTY,
            _0: [0; 12],
        };

        self.output.write_all(bytemuck::bytes_of(&header))?;
        Ok(TarFileWriter {
            writer: self,
            written: 0,
            size,
        })
    }

    pub fn append_file<P: AsRef<Path>>(
        &mut self,
        path: P,
        path_in_archive: &str,
    ) -> io::Result<()> {
        let file = File::open(path)?;
        let metadata = file.metadata()?;
        let mut reader = BufReader::new(file);
        // TODO
        let tar_metadata = TarNewFileMetadata {
            uid: 0,
            gid: 0,
            mode: 0o644,
        };
        let mut writer = self.begin_file(path_in_archive, metadata.len(), tar_metadata)?;

        let mut buffer = [0; 8192];
        loop {
            let len = reader.read(&mut buffer)?;
            if len == 0 {
                break;
            }
            writer.write_all(&buffer[..len])?;
        }
        writer.finish()?;

        Ok(())
    }
}

impl<W: Write> TarFileWriter<'_, W> {
    pub fn finish(mut self) -> io::Result<()> {
        let aligned_size = (self.size + 511) & !511;
        if self.written < aligned_size {
            let zeroes = aligned_size - self.written;
            // TODO suboptimal, but don't want Seek on writer?
            for _ in 0..zeroes {
                self.writer.output.write_all(&[0])?;
            }
            self.writer.output.flush()?;
            self.written = self.size;
        }
        Ok(())
    }
}

impl<W: Write> Write for TarFileWriter<'_, W> {
    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
        let can_write = (self.size - self.written).min(buf.len() as u64);
        if can_write != 0 {
            let len = self.writer.output.write(&buf[..can_write as usize])?;
            self.written += len as u64;
            Ok(len)
        } else {
            Ok(0)
        }
    }

    fn flush(&mut self) -> io::Result<()> {
        self.writer.output.flush()
    }
}
