//! QPDF File Specification helper (PDF spec section 7.11.3)
//!
//! File specifications reference external or embedded files. They have:
//! - **F**: file specification string (filename)
//! - **UF**: Unicode filename (UTF-16BE)
//! - **Desc**: description
//! - **EF**: embedded file stream reference
//! - **AFRelationship**: associated file relationship (PDF 2.0)
//! - **V**: volatile flag

use crate::pdf::object::{Dict, Name, ObjRef, Object, PdfString};

// ============================================================================
// UTF-16BE decoding for /UF
// ============================================================================

fn decode_utf16be(bytes: &[u8]) -> Option<String> {
    if bytes.len() < 2 {
        return None;
    }
    let bytes = strip_utf16_bom(bytes);
    if bytes.len() % 2 != 0 {
        return None;
    }
    let u16s: Vec<u16> = bytes
        .chunks_exact(2)
        .map(|c| u16::from_be_bytes([c[0], c[1]]))
        .collect();
    Some(String::from_utf16_lossy(&u16s))
}

fn strip_utf16_bom(bytes: &[u8]) -> &[u8] {
    if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
        &bytes[2..]
    } else {
        bytes
    }
}

// ============================================================================
// FileSpecHelper
// ============================================================================

/// Helper for reading PDF File Specification dictionaries (PDF spec 7.11.3).
#[derive(Debug)]
pub struct FileSpecHelper<'a> {
    file_spec: &'a Dict,
}

impl<'a> FileSpecHelper<'a> {
    /// Create a helper for the given file specification dictionary.
    pub fn new(file_spec: &'a Dict) -> Self {
        Self { file_spec }
    }

    /// Filename from /F (platform-dependent or PDF doc encoding).
    pub fn filename(&self) -> Option<&str> {
        self.file_spec
            .get(&Name::new("F"))
            .and_then(Object::as_string)
            .and_then(|s| s.as_str())
    }

    /// Unicode filename from /UF (UTF-16BE decoded).
    pub fn unicode_filename(&self) -> Option<String> {
        self.file_spec
            .get(&Name::new("UF"))
            .and_then(Object::as_string)
            .map(|s| s.as_bytes())
            .and_then(decode_utf16be)
    }

    /// Description from /Desc.
    pub fn description(&self) -> Option<&str> {
        self.file_spec
            .get(&Name::new("Desc"))
            .and_then(Object::as_string)
            .and_then(|s| s.as_str())
    }

    /// Reference to the embedded file stream from /EF/F or /EF/UF.
    pub fn embedded_file_ref(&self) -> Option<ObjRef> {
        let ef = self
            .file_spec
            .get(&Name::new("EF"))
            .and_then(Object::as_dict)?;
        ef.get(&Name::new("F"))
            .or_else(|| ef.get(&Name::new("UF")))
            .and_then(Object::as_obj_ref)
    }

    /// Associated file relationship from /AFRelationship (PDF 2.0).
    pub fn relationship(&self) -> Option<&str> {
        self.file_spec
            .get(&Name::new("AFRelationship"))
            .and_then(Object::as_name)
            .map(Name::as_str)
    }

    /// Volatile flag from /V. Returns true if the file may change.
    pub fn is_volatile(&self) -> bool {
        self.file_spec
            .get(&Name::new("V"))
            .and_then(Object::as_bool)
            .unwrap_or(false)
    }
}

// ============================================================================
// create_file_spec
// ============================================================================

/// Create a file specification dictionary with filename and embedded file reference.
pub fn create_file_spec(filename: &str, ef_ref: ObjRef) -> Dict {
    let mut dict = Dict::new();
    dict.insert(Name::new("Type"), Object::Name(Name::new("Filespec")));
    dict.insert(
        Name::new("F"),
        Object::String(PdfString::new(filename.as_bytes().to_vec())),
    );
    let mut ef = Dict::new();
    ef.insert(Name::new("F"), Object::Ref(ef_ref));
    ef.insert(Name::new("UF"), Object::Ref(ef_ref));
    dict.insert(Name::new("EF"), Object::Dict(ef));
    dict
}

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

    #[test]
    fn test_file_spec_helper_new() {
        let dict = Dict::new();
        let helper = FileSpecHelper::new(&dict);
        assert!(helper.filename().is_none());
        assert!(!helper.is_volatile());
    }

    #[test]
    fn test_file_spec_helper_filename() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("F"),
            Object::String(PdfString::new(b"document.pdf".to_vec())),
        );
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.filename(), Some("document.pdf"));
    }

    #[test]
    fn test_file_spec_helper_unicode_filename() {
        let mut dict = Dict::new();
        let utf16be = [
            0xFE, 0xFF, 0x00, 0x64, 0x00, 0x6F, 0x00, 0x63, 0x00, 0x2E, 0x00, 0x70, 0x00, 0x64,
            0x00, 0x66,
        ];
        dict.insert(
            Name::new("UF"),
            Object::String(PdfString::new(utf16be.to_vec())),
        );
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.unicode_filename(), Some("doc.pdf".to_string()));
    }

    #[test]
    fn test_file_spec_helper_unicode_filename_no_bom() {
        let mut dict = Dict::new();
        let utf16be = [0x00, 0x61, 0x00, 0x62, 0x00, 0x63];
        dict.insert(
            Name::new("UF"),
            Object::String(PdfString::new(utf16be.to_vec())),
        );
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.unicode_filename(), Some("abc".to_string()));
    }

    #[test]
    fn test_file_spec_helper_description() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("Desc"),
            Object::String(PdfString::new(b"Source document".to_vec())),
        );
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.description(), Some("Source document"));
    }

    #[test]
    fn test_file_spec_helper_embedded_file_ref() {
        let ef_ref = ObjRef::new(10, 0);
        let mut ef = Dict::new();
        ef.insert(Name::new("F"), Object::Ref(ef_ref));
        let mut dict = Dict::new();
        dict.insert(Name::new("EF"), Object::Dict(ef));
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.embedded_file_ref(), Some(ef_ref));
    }

    #[test]
    fn test_file_spec_helper_embedded_file_ref_prefer_f_over_uf() {
        let f_ref = ObjRef::new(10, 0);
        let uf_ref = ObjRef::new(11, 0);
        let mut ef = Dict::new();
        ef.insert(Name::new("F"), Object::Ref(f_ref));
        ef.insert(Name::new("UF"), Object::Ref(uf_ref));
        let mut dict = Dict::new();
        dict.insert(Name::new("EF"), Object::Dict(ef));
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.embedded_file_ref(), Some(f_ref));
    }

    #[test]
    fn test_file_spec_helper_embedded_file_ref_uf_only() {
        let uf_ref = ObjRef::new(11, 0);
        let mut ef = Dict::new();
        ef.insert(Name::new("UF"), Object::Ref(uf_ref));
        let mut dict = Dict::new();
        dict.insert(Name::new("EF"), Object::Dict(ef));
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.embedded_file_ref(), Some(uf_ref));
    }

    #[test]
    fn test_file_spec_helper_relationship() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("AFRelationship"),
            Object::Name(Name::new("Source")),
        );
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.relationship(), Some("Source"));
    }

    #[test]
    fn test_file_spec_helper_is_volatile_true() {
        let mut dict = Dict::new();
        dict.insert(Name::new("V"), Object::Bool(true));
        let helper = FileSpecHelper::new(&dict);
        assert!(helper.is_volatile());
    }

    #[test]
    fn test_file_spec_helper_is_volatile_false() {
        let mut dict = Dict::new();
        dict.insert(Name::new("V"), Object::Bool(false));
        let helper = FileSpecHelper::new(&dict);
        assert!(!helper.is_volatile());
    }

    #[test]
    fn test_file_spec_helper_is_volatile_absent() {
        let dict = Dict::new();
        let helper = FileSpecHelper::new(&dict);
        assert!(!helper.is_volatile());
    }

    #[test]
    fn test_create_file_spec() {
        let ef_ref = ObjRef::new(5, 0);
        let dict = create_file_spec("data.xml", ef_ref);
        let helper = FileSpecHelper::new(&dict);
        assert_eq!(helper.filename(), Some("data.xml"));
        assert_eq!(helper.embedded_file_ref(), Some(ef_ref));
        assert_eq!(
            dict.get(&Name::new("Type"))
                .and_then(Object::as_name)
                .map(Name::as_str),
            Some("Filespec")
        );
    }
}
