//! Document-level page operations (QPDFPageDocumentHelper equivalent)
//!
//! Provides page tree manipulation, media box resolution with inheritance,
//! content stream coalescing, and page label management.

use std::collections::HashMap;

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

use super::document::Document;
use super::error::{QpdfError, Result};
use super::object_handle::Rectangle;
use super::page_helper::PageHelper;

// ============================================================================
// PageLabelStyle
// ============================================================================

/// Page label numbering style (PDF spec 12.4.2)
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageLabelStyle {
    /// Decimal: 1, 2, 3, ...
    Decimal,
    /// Uppercase Roman: I, II, III, ...
    UpperRoman,
    /// Lowercase Roman: i, ii, iii, ...
    LowerRoman,
    /// Uppercase alphabetic: A, B, C, ...
    UpperAlpha,
    /// Lowercase alphabetic: a, b, c, ...
    LowerAlpha,
    /// No numbering
    None,
}

impl PageLabelStyle {
    fn to_pdf_name(&self) -> Option<Name> {
        match self {
            Self::Decimal => Some(Name::new("D")),
            Self::UpperRoman => Some(Name::new("R")),
            Self::LowerRoman => Some(Name::new("r")),
            Self::UpperAlpha => Some(Name::new("A")),
            Self::LowerAlpha => Some(Name::new("a")),
            Self::None => None,
        }
    }
}

// ============================================================================
// PageDocHelper
// ============================================================================

/// Document-level page operations.
///
/// Operates on a `Document` reference passed to each method.
/// No lifetime parameter - document is passed per call.
pub struct PageDocHelper;

impl PageDocHelper {
    /// Get page reference by 0-based index.
    pub fn get_page(doc: &mut Document, index: usize) -> Result<ObjRef> {
        let pages = doc.get_all_pages()?;
        pages
            .get(index)
            .copied()
            .ok_or_else(|| QpdfError::NotFound(format!("Page index {} out of range", index)))
    }

    /// Get page dictionary by 0-based index.
    /// Returns a clone of the page dict.
    pub fn get_page_dict(doc: &mut Document, index: usize) -> Result<Dict> {
        let page_ref = Self::get_page(doc, index)?;
        let obj = doc
            .get_object(page_ref)
            .ok_or_else(|| QpdfError::NotFound("Page object not found".to_string()))?;
        obj.as_dict()
            .cloned()
            .ok_or_else(|| QpdfError::Structure("Page is not a dictionary".to_string()))
    }

    /// Create a page from a dictionary and add it to the document.
    pub fn add_page_from_dict(doc: &mut Document, page_dict: Dict, first: bool) -> Result<ObjRef> {
        doc.add_page(Object::Dict(page_dict), first)?;
        let index = if first { 0 } else { doc.page_count()? - 1 };
        Self::get_page(doc, index)
    }

    /// Remove page by 0-based index.
    pub fn remove_page_by_index(doc: &mut Document, index: usize) -> Result<()> {
        let page_ref = Self::get_page(doc, index)?;
        doc.remove_page(page_ref)
    }

    /// Get media box for a page, checking inherited values from parent.
    pub fn get_page_media_box(doc: &Document, page_ref: ObjRef) -> Option<Rectangle> {
        let page_obj = doc.get_object(page_ref)?;
        let page_dict = page_obj.as_dict()?;

        // Direct MediaBox on page
        if let Some(rect) = page_dict
            .get(&Name::new("MediaBox"))
            .and_then(Rectangle::from_object)
        {
            return Some(rect);
        }

        // Walk up Parent chain
        let mut parent_ref = page_dict
            .get(&Name::new("Parent"))
            .and_then(Object::as_obj_ref)?;

        loop {
            let parent_obj = doc.get_object(parent_ref)?;
            let parent_dict = parent_obj.as_dict()?;

            if let Some(rect) = parent_dict
                .get(&Name::new("MediaBox"))
                .and_then(Rectangle::from_object)
            {
                return Some(rect);
            }

            parent_ref = parent_dict
                .get(&Name::new("Parent"))
                .and_then(Object::as_obj_ref)?;
        }
    }

    /// Merge multiple content streams on a page into a single stream.
    pub fn coalesce_content_streams(doc: &mut Document, page_ref: ObjRef) -> Result<()> {
        let contents = {
            let page_obj = doc
                .get_object(page_ref)
                .ok_or_else(|| QpdfError::NotFound("Page not found".to_string()))?;
            let page_dict = page_obj
                .as_dict()
                .ok_or_else(|| QpdfError::Structure("Page is not a dictionary".to_string()))?;

            let contents_obj = page_dict.get(&Name::new("Contents"));
            match contents_obj {
                Some(Object::Array(arr)) => arr
                    .iter()
                    .filter_map(|o| o.as_obj_ref())
                    .collect::<Vec<_>>(),
                Some(obj) => {
                    if let Some(r) = obj.as_obj_ref() {
                        vec![r]
                    } else {
                        return Err(QpdfError::Structure(
                            "Contents is not a reference or array".to_string(),
                        ));
                    }
                }
                None => return Ok(()),
            }
        };

        if contents.len() <= 1 {
            return Ok(());
        }

        let mut combined = Vec::new();
        for content_ref in &contents {
            let obj = doc
                .get_object(*content_ref)
                .ok_or_else(|| QpdfError::NotFound("Content stream not found".to_string()))?;
            let data = match obj {
                Object::Stream { data, .. } => data.clone(),
                _ => {
                    return Err(QpdfError::Structure(
                        "Content stream reference does not point to stream".to_string(),
                    ));
                }
            };
            combined.extend_from_slice(&data);
            if !data.ends_with(b"\n") {
                combined.push(b'\n');
            }
        }

        let new_stream_ref = doc.new_stream(combined);

        let page_obj = doc
            .get_object_mut(page_ref)
            .ok_or_else(|| QpdfError::NotFound("Page not found".to_string()))?;
        let page_dict = page_obj
            .as_dict_mut()
            .ok_or_else(|| QpdfError::Structure("Page is not a dictionary".to_string()))?;
        page_dict.insert(Name::new("Contents"), Object::Ref(new_stream_ref));

        Ok(())
    }

    /// Push inherited attributes from parent(s) to a single page.
    pub fn push_inherited_to_page(doc: &mut Document, page_ref: ObjRef) -> Result<()> {
        const SKIP: &[&str] = &["Type", "Parent", "Kids", "Count"];

        let parent_attrs: Vec<(Name, Object)> = {
            let page_obj = doc
                .get_object(page_ref)
                .ok_or_else(|| QpdfError::NotFound("Page not found".to_string()))?;
            let page_dict = page_obj
                .as_dict()
                .ok_or_else(|| QpdfError::Structure("Page is not a dictionary".to_string()))?;

            let parent_ref = page_dict
                .get(&Name::new("Parent"))
                .and_then(Object::as_obj_ref)
                .ok_or_else(|| QpdfError::Structure("Page has no Parent".to_string()))?;

            let parent_obj = doc
                .get_object(parent_ref)
                .ok_or_else(|| QpdfError::NotFound("Parent not found".to_string()))?;
            let parent_dict = parent_obj
                .as_dict()
                .ok_or_else(|| QpdfError::Structure("Parent is not a dictionary".to_string()))?;

            parent_dict
                .iter()
                .filter(|(k, _)| !SKIP.contains(&k.as_str()))
                .map(|(k, v)| (k.clone(), v.clone()))
                .collect()
        };

        let page_obj = doc
            .get_object_mut(page_ref)
            .ok_or_else(|| QpdfError::NotFound("Page not found".to_string()))?;
        let page_dict = page_obj
            .as_dict_mut()
            .ok_or_else(|| QpdfError::Structure("Page is not a dictionary".to_string()))?;

        for (k, v) in parent_attrs {
            if !page_dict.contains_key(&k) {
                page_dict.insert(k, v);
            }
        }

        Ok(())
    }

    /// Add a page label entry to the document's PageLabels number tree.
    pub fn add_page_label(
        doc: &mut Document,
        start_page: i32,
        style: PageLabelStyle,
        prefix: Option<String>,
        start_num: i32,
    ) -> Result<()> {
        let root_ref = doc
            .get_root_ref()
            .ok_or_else(|| QpdfError::Structure("No root catalog".to_string()))?;

        let mut label_dict = Dict::new();
        if let Some(s) = style.to_pdf_name() {
            label_dict.insert(Name::new("S"), Object::Name(s));
        }
        if let Some(p) = prefix {
            label_dict.insert(
                Name::new("P"),
                Object::String(PdfString::new(p.into_bytes())),
            );
        }
        if start_num != 1 {
            label_dict.insert(Name::new("St"), Object::Int(start_num as i64));
        }

        let page_labels_key = Name::new("PageLabels");
        let pl_ref = {
            let root_obj = doc
                .get_object(root_ref)
                .ok_or_else(|| QpdfError::Structure("Root object not found".to_string()))?;
            let root_dict = root_obj
                .as_dict()
                .ok_or_else(|| QpdfError::Structure("Root is not a dictionary".to_string()))?;

            match root_dict.get(&page_labels_key) {
                Some(Object::Ref(r)) => *r,
                None => {
                    let mut pl_dict = Dict::new();
                    pl_dict.insert(Name::new("Nums"), Object::Array(vec![]));
                    let r = doc.make_indirect_object(Object::Dict(pl_dict));
                    let root_obj = doc
                        .get_object_mut(root_ref)
                        .ok_or_else(|| QpdfError::Structure("Root object not found".to_string()))?;
                    root_obj
                        .as_dict_mut()
                        .ok_or_else(|| {
                            QpdfError::Structure("Root is not a dictionary".to_string())
                        })?
                        .insert(page_labels_key, Object::Ref(r));
                    r
                }
                Some(_) => {
                    return Err(QpdfError::Structure(
                        "PageLabels exists but is not a number tree reference".to_string(),
                    ));
                }
            }
        };

        let nums = doc
            .get_object_mut(pl_ref)
            .ok_or_else(|| QpdfError::Structure("PageLabels object not found".to_string()))?
            .as_dict_mut()
            .ok_or_else(|| QpdfError::Structure("PageLabels is not a dictionary".to_string()))?
            .get_mut(&Name::new("Nums"))
            .and_then(Object::as_array_mut)
            .ok_or_else(|| QpdfError::Structure("PageLabels has no Nums array".to_string()))?;

        let mut i = 0;
        let mut inserted = false;
        while i + 1 < nums.len() {
            if let Some(k) = nums[i].as_int() {
                if k == start_page as i64 {
                    nums[i + 1] = Object::Dict(label_dict.clone());
                    inserted = true;
                    break;
                }
                if k > start_page as i64 {
                    nums.insert(i, Object::Int(start_page as i64));
                    nums.insert(i + 1, Object::Dict(label_dict.clone()));
                    inserted = true;
                    break;
                }
            }
            i += 2;
        }
        if !inserted {
            nums.push(Object::Int(start_page as i64));
            nums.push(Object::Dict(label_dict.clone()));
        }

        Ok(())
    }
}

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

    fn make_page_dict() -> Dict {
        let mut dict = HashMap::new();
        dict.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(612.0),
                Object::Real(792.0),
            ]),
        );
        dict.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
        dict
    }

    #[test]
    fn test_page_label_style_to_pdf_name() {
        assert_eq!(PageLabelStyle::Decimal.to_pdf_name(), Some(Name::new("D")));
        assert_eq!(
            PageLabelStyle::UpperRoman.to_pdf_name(),
            Some(Name::new("R"))
        );
        assert_eq!(
            PageLabelStyle::LowerRoman.to_pdf_name(),
            Some(Name::new("r"))
        );
        assert_eq!(PageLabelStyle::None.to_pdf_name(), None);
    }

    #[test]
    fn test_get_page() {
        let mut doc = Document::empty_pdf();
        let page_dict = make_page_dict();
        doc.add_page(Object::Dict(page_dict), false).unwrap();

        let page_ref = PageDocHelper::get_page(&mut doc, 0).unwrap();
        assert_eq!(page_ref.num, 3);
    }

    #[test]
    fn test_get_page_out_of_range() {
        let mut doc = Document::empty_pdf();
        let result = PageDocHelper::get_page(&mut doc, 0);
        assert!(result.is_err());
    }

    #[test]
    fn test_get_page_dict() {
        let mut doc = Document::empty_pdf();
        let page_dict = make_page_dict();
        doc.add_page(Object::Dict(page_dict.clone()), false)
            .unwrap();

        let retrieved = PageDocHelper::get_page_dict(&mut doc, 0).unwrap();
        assert!(PageHelper::is_page_object(&retrieved));
        assert!(retrieved.contains_key(&Name::new("MediaBox")));
    }

    #[test]
    fn test_add_page_from_dict() {
        let mut doc = Document::empty_pdf();
        let page_dict = make_page_dict();

        let page_ref = PageDocHelper::add_page_from_dict(&mut doc, page_dict, false).unwrap();
        assert!(doc.get_object(page_ref).is_some());
        assert_eq!(doc.page_count().unwrap(), 1);

        let page_dict2 = make_page_dict();
        let page_ref2 = PageDocHelper::add_page_from_dict(&mut doc, page_dict2, true).unwrap();
        assert_eq!(doc.page_count().unwrap(), 2);
        assert_eq!(PageDocHelper::get_page(&mut doc, 0).unwrap(), page_ref2);
    }

    #[test]
    fn test_remove_page_by_index() {
        let mut doc = Document::empty_pdf();
        let page_dict = make_page_dict();
        doc.add_page(Object::Dict(page_dict), false).unwrap();
        assert_eq!(doc.page_count().unwrap(), 1);

        PageDocHelper::remove_page_by_index(&mut doc, 0).unwrap();
        assert_eq!(doc.page_count().unwrap(), 0);
    }

    #[test]
    fn test_get_page_media_box_direct() {
        let mut doc = Document::empty_pdf();
        doc.add_page(Object::Dict(make_page_dict()), false).unwrap();
        let pages = doc.get_all_pages().unwrap();
        let page_ref = pages[0];

        let rect = PageDocHelper::get_page_media_box(&doc, page_ref);
        assert!(rect.is_some());
        let r = rect.unwrap();
        assert_eq!(r.llx, 0.0);
        assert_eq!(r.ury, 792.0);
    }

    #[test]
    fn test_get_page_media_box_inherited() {
        let mut doc = Document::empty_pdf();
        let pages_ref = {
            let root = doc.get_root().unwrap().as_dict().unwrap();
            root.get(&Name::new("Pages")).unwrap().as_obj_ref().unwrap()
        };

        let mut page_dict = make_page_dict();
        page_dict.remove(&Name::new("MediaBox"));
        page_dict.insert(Name::new("Parent"), Object::Ref(pages_ref));

        let old_pages = doc
            .get_object(pages_ref)
            .unwrap()
            .as_dict()
            .unwrap()
            .clone();
        let mut kids = old_pages
            .get(&Name::new("Kids"))
            .unwrap()
            .as_array()
            .unwrap()
            .clone();
        let page_ref = doc.make_indirect_object(Object::Dict(page_dict));
        kids.push(Object::Ref(page_ref));
        let mut new_pages = old_pages.clone();
        new_pages.insert(
            Name::new("MediaBox"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(595.0),
                Object::Real(842.0),
            ]),
        );
        new_pages.insert(Name::new("Kids"), Object::Array(kids));
        new_pages.insert(Name::new("Count"), Object::Int(1));
        doc.replace_object(pages_ref, Object::Dict(new_pages));

        let rect = PageDocHelper::get_page_media_box(&doc, page_ref);
        assert!(rect.is_some());
        let r = rect.unwrap();
        assert_eq!(r.urx, 595.0);
        assert_eq!(r.ury, 842.0);
    }

    #[test]
    fn test_coalesce_content_streams() {
        let mut doc = Document::empty_pdf();
        let stream1_ref = doc.new_stream(b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET".to_vec());
        let stream2_ref = doc.new_stream(b"BT /F1 12 Tf 100 680 Td (World) Tj ET".to_vec());

        let mut page_dict = make_page_dict();
        page_dict.insert(
            Name::new("Contents"),
            Object::Array(vec![Object::Ref(stream1_ref), Object::Ref(stream2_ref)]),
        );
        doc.add_page(Object::Dict(page_dict), false).unwrap();
        let pages = doc.get_all_pages().unwrap();
        let page_ref = pages[0];

        PageDocHelper::coalesce_content_streams(&mut doc, page_ref).unwrap();

        let page_obj = doc.get_object(page_ref).unwrap();
        let contents = page_obj
            .as_dict()
            .unwrap()
            .get(&Name::new("Contents"))
            .unwrap();
        let single_ref = contents.as_obj_ref().unwrap();
        let stream = doc.get_object(single_ref).unwrap();
        if let Object::Stream { data, .. } = stream {
            assert!(data.windows(b"Hello".len()).any(|w| w == b"Hello"));
            assert!(data.windows(b"World".len()).any(|w| w == b"World"));
        } else {
            panic!("Expected stream");
        }
    }

    #[test]
    fn test_push_inherited_to_page() {
        let mut doc = Document::empty_pdf();
        let root = doc.get_root().unwrap().as_dict().unwrap();
        let pages_ref = root.get(&Name::new("Pages")).unwrap().as_obj_ref().unwrap();

        let mut pages_obj = doc
            .get_object(pages_ref)
            .unwrap()
            .as_dict()
            .unwrap()
            .clone();
        pages_obj.insert(
            Name::new("MediaBox"),
            Object::Array(vec![
                Object::Real(0.0),
                Object::Real(0.0),
                Object::Real(595.0),
                Object::Real(842.0),
            ]),
        );
        doc.replace_object(pages_ref, Object::Dict(pages_obj));

        let mut page_dict = make_page_dict();
        page_dict.remove(&Name::new("MediaBox"));
        page_dict.insert(Name::new("Parent"), Object::Ref(pages_ref));
        doc.add_page(Object::Dict(page_dict), false).unwrap();
        let pages = doc.get_all_pages().unwrap();
        let page_ref = pages[0];

        PageDocHelper::push_inherited_to_page(&mut doc, page_ref).unwrap();

        let page_obj = doc.get_object(page_ref).unwrap();
        let page_dict = page_obj.as_dict().unwrap();
        assert!(page_dict.contains_key(&Name::new("MediaBox")));
    }

    #[test]
    fn test_add_page_label() {
        let mut doc = Document::empty_pdf();
        let page_dict = make_page_dict();
        doc.add_page(Object::Dict(page_dict), false).unwrap();

        PageDocHelper::add_page_label(
            &mut doc,
            0,
            PageLabelStyle::LowerRoman,
            Some("i".to_string()),
            1,
        )
        .unwrap();

        let root = doc.get_root().unwrap();
        let pl_ref = root
            .as_dict()
            .unwrap()
            .get(&Name::new("PageLabels"))
            .and_then(Object::as_obj_ref);
        assert!(pl_ref.is_some());

        PageDocHelper::add_page_label(&mut doc, 5, PageLabelStyle::Decimal, None, 1).unwrap();
    }
}
