//! QPDF Embedded File helper (PDF spec section 7.11.4)
//!
//! Embedded File streams store file data within a PDF. They have:
//! - **Params**: optional dict with Size, CreationDate, ModDate, CheckSum
//! - **Subtype**: optional name (MIME type)
//! - Stream data: the actual file bytes

use std::collections::HashMap;

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

use crate::qpdf::name_tree_helper::{NameTreeHelper, NameTreeHelperMut, build_name_tree};

// ============================================================================
// EmbeddedFileHelper
// ============================================================================

/// Helper for reading Embedded File stream dictionaries (PDF spec 7.11.4).
///
/// Wraps a dictionary that represents an Embedded File stream's dictionary.
/// When constructed from a full stream Object, can also provide access to stream data.
#[derive(Debug)]
pub struct EmbeddedFileHelper<'a> {
    dict: &'a Dict,
    data: Option<&'a [u8]>,
}

impl<'a> EmbeddedFileHelper<'a> {
    /// Create a helper for the given Embedded File stream dictionary.
    pub fn new(ef: &'a Dict) -> Self {
        Self {
            dict: ef,
            data: None,
        }
    }

    /// Create from a full PDF Object (Stream or Dict).
    /// When the object is a Stream, `data()` will return the stream bytes.
    pub fn from_object(obj: &'a Object) -> Option<Self> {
        match obj {
            Object::Stream { dict, data } => Some(Self {
                dict,
                data: Some(data),
            }),
            Object::Dict(d) => Some(Self {
                dict: d,
                data: None,
            }),
            _ => None,
        }
    }

    /// File size in bytes from /Params/Size.
    pub fn size(&self) -> Option<i64> {
        let params = self
            .dict
            .get(&Name::new("Params"))
            .and_then(Object::as_dict)?;
        params.get(&Name::new("Size")).and_then(Object::as_int)
    }

    /// Creation date from /Params/CreationDate (D:YYYYMMDDHHmmSS format).
    pub fn creation_date(&self) -> Option<&str> {
        let params = self
            .dict
            .get(&Name::new("Params"))
            .and_then(Object::as_dict)?;
        params
            .get(&Name::new("CreationDate"))
            .and_then(Object::as_string)
            .and_then(|s| s.as_str())
    }

    /// Modification date from /Params/ModDate.
    pub fn modification_date(&self) -> Option<&str> {
        let params = self
            .dict
            .get(&Name::new("Params"))
            .and_then(Object::as_dict)?;
        params
            .get(&Name::new("ModDate"))
            .and_then(Object::as_string)
            .and_then(|s| s.as_str())
    }

    /// MIME type from /Subtype name.
    pub fn mime_type(&self) -> Option<&str> {
        self.dict
            .get(&Name::new("Subtype"))
            .and_then(Object::as_name)
            .map(Name::as_str)
    }

    /// MD5 checksum from /Params/CheckSum (16-byte binary string).
    pub fn checksum(&self) -> Option<&[u8]> {
        let params = self
            .dict
            .get(&Name::new("Params"))
            .and_then(Object::as_dict)?;
        params
            .get(&Name::new("CheckSum"))
            .and_then(Object::as_string)
            .map(|s| s.as_bytes())
    }

    /// Stream data. Returns `Some` only when constructed from a Stream object.
    pub fn data(&self) -> Option<&[u8]> {
        self.data
    }
}

// ============================================================================
// EmbeddedFileDocHelper
// ============================================================================

/// Document-level helper for the /Names/EmbeddedFiles name tree.
pub struct EmbeddedFileDocHelper;

impl EmbeddedFileDocHelper {
    /// Walk the /Names/EmbeddedFiles name tree and return all (name, file_spec_ref) pairs.
    ///
    /// Only includes entries where the value is an indirect reference to a file spec.
    pub fn get_all_embedded_files(
        catalog: &Dict,
        objects: &HashMap<ObjRef, Object>,
    ) -> Vec<(String, ObjRef)> {
        let names_dict = catalog.get(&Name::new("Names")).and_then(Object::as_dict);
        let names_dict = match names_dict {
            Some(d) => d,
            None => return Vec::new(),
        };

        let ef_value = names_dict.get(&Name::new("EmbeddedFiles"));
        let ef_value = match ef_value {
            Some(v) => v,
            None => return Vec::new(),
        };

        let ef_dict = resolve_to_dict(ef_value, objects);
        let ef_dict = match ef_dict {
            Some(d) => d,
            None => return Vec::new(),
        };

        let helper = NameTreeHelper::new(ef_dict);
        let entries = helper.get_all_entries(objects);

        let mut result = Vec::new();
        for (name, value) in entries {
            if let Some(r) = value.as_obj_ref() {
                result.push((name, r));
            }
        }
        result
    }

    /// Add an embedded file to the /Names/EmbeddedFiles name tree.
    ///
    /// Creates the Names and EmbeddedFiles structure if absent. Requires the
    /// EmbeddedFiles name tree to be an inline dict (not an indirect ref).
    pub fn add_embedded_file(catalog: &mut Dict, name: &str, file_spec_ref: ObjRef) {
        let names_obj = catalog
            .entry(Name::new("Names"))
            .or_insert_with(|| Object::Dict(Dict::new()));

        let names_dict = match names_obj.as_dict_mut() {
            Some(d) => d,
            None => return,
        };

        let ef_obj = names_dict
            .entry(Name::new("EmbeddedFiles"))
            .or_insert_with(|| build_name_tree(Vec::new()));

        let ef_dict = match ef_obj.as_dict_mut() {
            Some(d) => d,
            None => return,
        };

        let mut helper = NameTreeHelperMut::new(ef_dict);
        helper.insert(name, Object::Ref(file_spec_ref));
    }
}

fn resolve_to_dict<'a>(obj: &'a Object, objects: &'a HashMap<ObjRef, Object>) -> Option<&'a Dict> {
    match obj {
        Object::Dict(d) => Some(d),
        Object::Ref(r) => {
            let resolved = objects.get(r)?;
            resolved.as_dict()
        }
        _ => None,
    }
}

// ============================================================================
// create_embedded_file_stream
// ============================================================================

/// Create an Embedded File stream object with the given data and MIME type.
pub fn create_embedded_file_stream(data: Vec<u8>, mime_type: &str) -> Object {
    let mut dict = Dict::new();
    dict.insert(Name::new("Type"), Object::Name(Name::new("EmbeddedFile")));
    dict.insert(Name::new("Subtype"), Object::Name(Name::new(mime_type)));
    dict.insert(Name::new("Length"), Object::Int(data.len() as i64));

    let mut params = Dict::new();
    params.insert(Name::new("Size"), Object::Int(data.len() as i64));
    dict.insert(Name::new("Params"), Object::Dict(params));

    Object::Stream { dict, data }
}

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

    #[test]
    fn test_embedded_file_helper_new() {
        let dict = Dict::new();
        let helper = EmbeddedFileHelper::new(&dict);
        assert!(helper.size().is_none());
        assert!(helper.data().is_none());
    }

    #[test]
    fn test_embedded_file_helper_size() {
        let mut params = Dict::new();
        params.insert(Name::new("Size"), Object::Int(1024));
        let mut dict = Dict::new();
        dict.insert(Name::new("Params"), Object::Dict(params));
        let helper = EmbeddedFileHelper::new(&dict);
        assert_eq!(helper.size(), Some(1024));
    }

    #[test]
    fn test_embedded_file_helper_creation_date() {
        let mut params = Dict::new();
        params.insert(
            Name::new("CreationDate"),
            Object::String(PdfString::new(b"D:20240101120000".to_vec())),
        );
        let mut dict = Dict::new();
        dict.insert(Name::new("Params"), Object::Dict(params));
        let helper = EmbeddedFileHelper::new(&dict);
        assert_eq!(helper.creation_date(), Some("D:20240101120000"));
    }

    #[test]
    fn test_embedded_file_helper_modification_date() {
        let mut params = Dict::new();
        params.insert(
            Name::new("ModDate"),
            Object::String(PdfString::new(b"D:20240301120000".to_vec())),
        );
        let mut dict = Dict::new();
        dict.insert(Name::new("Params"), Object::Dict(params));
        let helper = EmbeddedFileHelper::new(&dict);
        assert_eq!(helper.modification_date(), Some("D:20240301120000"));
    }

    #[test]
    fn test_embedded_file_helper_mime_type() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("Subtype"),
            Object::Name(Name::new("application/pdf")),
        );
        let helper = EmbeddedFileHelper::new(&dict);
        assert_eq!(helper.mime_type(), Some("application/pdf"));
    }

    #[test]
    fn test_embedded_file_helper_checksum() {
        let checksum_bytes = [0u8; 16];
        let mut params = Dict::new();
        params.insert(
            Name::new("CheckSum"),
            Object::String(PdfString::new(checksum_bytes.to_vec())),
        );
        let mut dict = Dict::new();
        dict.insert(Name::new("Params"), Object::Dict(params));
        let helper = EmbeddedFileHelper::new(&dict);
        assert_eq!(helper.checksum(), Some(checksum_bytes.as_slice()));
    }

    #[test]
    fn test_embedded_file_helper_from_object_stream() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Type"), Object::Name(Name::new("EmbeddedFile")));
        let data = b"file content here";
        let obj = Object::Stream {
            dict,
            data: data.to_vec(),
        };
        let helper = EmbeddedFileHelper::from_object(&obj).unwrap();
        assert_eq!(helper.data(), Some(data.as_slice()));
        assert_eq!(helper.mime_type(), None);
    }

    #[test]
    fn test_embedded_file_helper_from_object_dict() {
        let mut dict = Dict::new();
        dict.insert(Name::new("Subtype"), Object::Name(Name::new("text/plain")));
        let obj = Object::Dict(dict);
        let helper = EmbeddedFileHelper::from_object(&obj).unwrap();
        assert!(helper.data().is_none());
        assert_eq!(helper.mime_type(), Some("text/plain"));
    }

    #[test]
    fn test_embedded_file_helper_from_object_ref() {
        let obj = Object::Ref(ObjRef::new(1, 0));
        assert!(EmbeddedFileHelper::from_object(&obj).is_none());
    }

    #[test]
    fn test_get_all_embedded_files_empty() {
        let catalog = Dict::new();
        let objects = HashMap::new();
        let result = EmbeddedFileDocHelper::get_all_embedded_files(&catalog, &objects);
        assert!(result.is_empty());
    }

    #[test]
    fn test_get_all_embedded_files() {
        let file_spec_ref = ObjRef::new(10, 0);
        let mut ef_tree = Dict::new();
        ef_tree.insert(
            Name::new("Names"),
            Object::Array(vec![
                Object::String(PdfString::new(b"doc.xml".to_vec())),
                Object::Ref(file_spec_ref),
            ]),
        );
        ef_tree.insert(
            Name::new("Limits"),
            Object::Array(vec![
                Object::String(PdfString::new(b"doc.xml".to_vec())),
                Object::String(PdfString::new(b"doc.xml".to_vec())),
            ]),
        );
        let mut names = Dict::new();
        names.insert(Name::new("EmbeddedFiles"), Object::Dict(ef_tree));
        let mut catalog = Dict::new();
        catalog.insert(Name::new("Names"), Object::Dict(names));

        let objects = HashMap::new();
        let result = EmbeddedFileDocHelper::get_all_embedded_files(&catalog, &objects);
        assert_eq!(result.len(), 1);
        assert_eq!(result[0].0, "doc.xml");
        assert_eq!(result[0].1, file_spec_ref);
    }

    #[test]
    fn test_add_embedded_file() {
        let mut catalog = Dict::new();
        let file_spec_ref = ObjRef::new(5, 0);
        EmbeddedFileDocHelper::add_embedded_file(&mut catalog, "attachment.pdf", file_spec_ref);

        let names = catalog
            .get(&Name::new("Names"))
            .and_then(Object::as_dict)
            .unwrap();
        let ef = names
            .get(&Name::new("EmbeddedFiles"))
            .and_then(Object::as_dict)
            .unwrap();
        let ef_names = ef
            .get(&Name::new("Names"))
            .and_then(Object::as_array)
            .unwrap();
        assert_eq!(ef_names.len(), 2);
        assert_eq!(
            ef_names[0].as_string().and_then(|s| s.as_str()),
            Some("attachment.pdf")
        );
        assert_eq!(ef_names[1].as_obj_ref(), Some(file_spec_ref));
    }

    #[test]
    fn test_create_embedded_file_stream() {
        let data = b"hello world";
        let obj = create_embedded_file_stream(data.to_vec(), "text/plain");
        let helper = EmbeddedFileHelper::from_object(&obj).unwrap();
        assert_eq!(helper.data(), Some(data.as_slice()));
        assert_eq!(helper.mime_type(), Some("text/plain"));
        assert_eq!(helper.size(), Some(11));
    }
}
