// Copyright (c) 2025-present, fjall-rs
// This source code is licensed under both the Apache 2.0 and MIT License
// (found in the LICENSE-* files in the repository)

#[cfg(not(feature = "std"))]
use alloc::vec::Vec;

use crate::io::{Seek, Write};
use crate::sfa::{
    toc::{
        entry::{SectionName, TocEntry},
        writer::TocWriter,
    },
    trailer::writer::TrailerWriter,
};

/// Archive writer
#[expect(
    clippy::struct_field_names,
    reason = "upstream sfa struct: `writer` field matches struct name; \
              kept verbatim so the upstream-sync diff stays mechanical"
)]
pub struct Writer<W: Write + Seek> {
    writer: W,
    last_section_pos: u64,
    section_name: SectionName,
    toc: Vec<TocEntry>,
}

impl<W: Write + Seek> Writer<W> {
    /// Returns a mutable reference to the underlying writer.
    pub fn get_mut(&mut self) -> &mut W {
        &mut self.writer
    }

    /// Creates a new writer with the given I/O writer.
    #[must_use]
    pub fn from_writer(writer: W) -> Self {
        Self {
            writer,
            last_section_pos: 0,
            section_name: SectionName::new(),
            toc: Vec::new(),
        }
    }
}

// `crate::io::Write` is the std trait (via the std-mode supertrait blanket)
// under `std` and the native trait under `no_std`; gated impls differ only in
// the trait path and `Result` type.
#[cfg(feature = "std")]
impl<W: Write + Seek> std::io::Write for Writer<W> {
    fn flush(&mut self) -> std::io::Result<()> {
        self.writer.flush()
    }

    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
        self.writer.write(buf)
    }
}

#[cfg(not(feature = "std"))]
impl<W: Write + Seek> crate::io::Write for Writer<W> {
    fn flush(&mut self) -> crate::io::Result<()> {
        self.writer.flush()
    }

    fn write(&mut self, buf: &[u8]) -> crate::io::Result<usize> {
        self.writer.write(buf)
    }
}

impl<W: Write + Seek> Writer<W> {
    /// Starts the first named section.
    ///
    /// # Errors
    ///
    /// Returns error, if an IO error occurred.
    pub fn start(&mut self, name: impl Into<SectionName>) -> crate::io::Result<()> {
        self.append_toc_entry()?;
        self.section_name = name.into();
        Ok(())
    }

    fn append_toc_entry(&mut self) -> crate::io::Result<()> {
        let file_pos = self.writer.stream_position()?;

        if file_pos > 0 {
            let name = core::mem::take(&mut self.section_name);
            self.toc.push(TocEntry {
                name,
                pos: self.last_section_pos,
                len: file_pos - self.last_section_pos,
            });
        }

        self.last_section_pos = file_pos;

        Ok(())
    }

    fn append_trailer(writer: &mut W, toc: &[TocEntry]) -> crate::sfa::Result<()> {
        // Write ToC
        let toc_pos = writer.stream_position()?;
        let toc_checksum = TocWriter::write_into(&mut *writer, toc)?;

        let after_toc_pos = writer.stream_position()?;
        let toc_len = after_toc_pos - toc_pos;

        // Write trailer
        TrailerWriter::write_into(writer, toc_checksum, toc_pos, toc_len)
    }

    /// Finishes the file.
    ///
    /// # Errors
    ///
    /// Returns error, if an IO error occurred.
    pub fn finish(mut self) -> crate::sfa::Result<()> {
        log::trace!("Finishing archive");

        self.append_toc_entry()?;
        Self::append_trailer(&mut self.writer, &self.toc)?;
        self.writer.flush()?;

        Ok(())
    }

    /// Finishes the file.
    ///
    /// Returns the inner writer.
    ///
    /// # Errors
    ///
    /// Returns error, if an IO error occurred.
    pub fn into_inner(mut self) -> crate::sfa::Result<W> {
        log::trace!("Finishing archive");

        self.append_toc_entry()?;
        Self::append_trailer(&mut self.writer, &self.toc)?;
        self.writer.flush()?;

        Ok(self.writer)
    }
}

#[cfg(test)]
#[expect(
    clippy::indexing_slicing,
    reason = "vendored upstream tests use `toc[i]` indexing freely; \
              keep verbatim so the upstream-sync diff stays mechanical"
)]
mod tests {
    use super::*;
    use crate::sfa::toc::reader::TocReader;
    use crate::sfa::trailer::reader::TrailerReader;
    use std::fs::File;
    use std::io::Write;
    use test_log::test;

    #[test]
    fn writer_empty() -> crate::sfa::Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("file.sfa");

        let mut file = File::create(&path)?;
        let writer = Writer::from_writer(&mut file);
        writer.finish()?;
        file.sync_all()?;
        drop(file);

        let mut reader = File::open(&path)?;
        let trailer = TrailerReader::from_reader(&mut reader)?;
        assert_eq!(0, trailer.toc_pos);

        let toc = TocReader::from_reader(
            &mut reader,
            trailer.toc_pos,
            trailer.toc_len,
            trailer.toc_checksum,
        )?;
        assert_eq!(0, toc.len());
        assert!(toc.is_empty());
        assert!(toc.section(b"hello").is_none());

        Ok(())
    }

    #[test]
    fn writer_simple() -> crate::sfa::Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("file.sfa");

        let data = b"hello world";

        let mut file = File::create(&path)?;
        let mut writer = Writer::from_writer(&mut file);
        writer.write_all(data)?;
        writer.finish()?;
        file.sync_all()?;
        drop(file);

        let mut reader = File::open(&path)?;
        let trailer = TrailerReader::from_reader(&mut reader)?;
        assert_eq!(data.len() as u64, trailer.toc_pos);

        let toc = TocReader::from_reader(
            &mut reader,
            trailer.toc_pos,
            trailer.toc_len,
            trailer.toc_checksum,
        )?;
        assert_eq!(1, toc.len());
        assert!(toc.section(b"hello").is_none());
        assert!(toc.section(b"").is_some());

        assert_eq!(0, toc[0].pos);
        assert_eq!(data.len() as u64, toc[0].len);
        assert_eq!(&[] as &[u8], &*toc[0].name);

        Ok(())
    }

    #[test]
    fn writer_multiple_sections() -> crate::sfa::Result<()> {
        let dir = tempfile::tempdir()?;
        let path = dir.path().join("file.sfa");

        let data = b"hello world";
        let data2 = b"hello world2";
        let data3 = b"hello world3";

        let mut file = File::create(&path)?;
        let mut writer = Writer::from_writer(&mut file);
        writer.write_all(data)?;
        writer.start("section1")?;
        writer.write_all(data2)?;
        writer.start("section2")?;
        writer.write_all(data3)?;
        writer.finish()?;
        file.sync_all()?;
        drop(file);

        let mut reader = File::open(&path)?;
        let trailer = TrailerReader::from_reader(&mut reader)?;
        assert_eq!(
            data.len() as u64 + data2.len() as u64 + data3.len() as u64,
            trailer.toc_pos,
        );

        let toc = TocReader::from_reader(
            &mut reader,
            trailer.toc_pos,
            trailer.toc_len,
            trailer.toc_checksum,
        )?;
        assert_eq!(3, toc.len());
        assert!(toc.section(b"hello").is_none());
        assert!(toc.section(b"").is_some());
        assert!(toc.section(b"section1").is_some());
        assert!(toc.section(b"section2").is_some());

        assert_eq!(0, toc[0].pos);
        assert_eq!(data.len() as u64, toc[0].len());

        assert_eq!(&[] as &[u8], &*toc[0].name);
        assert_eq!(b"section1", &*toc[1].name);
        assert_eq!(b"section2", &*toc[2].name);

        assert_eq!(data.len() as u64, toc[0].len);
        assert_eq!(data2.len() as u64, toc[1].len);
        assert_eq!(data3.len() as u64, toc[2].len);

        Ok(())
    }
}
