//! PDF outline/bookmark helpers inspired by QPDFOutlineDocumentHelper and QPDFOutlineObjectHelper
//!
//! Implements PDF spec section 12.3.3 - Document Outline (Bookmarks).

use std::collections::HashMap;

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

/// Helper for inspecting a single outline item (bookmark) dictionary.
#[derive(Debug)]
pub struct OutlineHelper<'a> {
    outline: &'a Dict,
}

impl<'a> OutlineHelper<'a> {
    pub fn new(outline: &'a Dict) -> Self {
        Self { outline }
    }

    /// Get the outline title, decoding UTF-16BE or Latin-1 as needed.
    pub fn title(&self) -> Option<String> {
        self.outline
            .get(&Name::new("Title"))
            .and_then(decode_outline_title)
    }

    /// Get the /Dest (destination) object if present.
    pub fn destination(&self) -> Option<&Object> {
        self.outline.get(&Name::new("Dest"))
    }

    /// Get the /A (action) dictionary if present.
    pub fn action(&self) -> Option<&Object> {
        self.outline.get(&Name::new("A"))
    }

    /// Whether children are initially open (positive /Count).
    pub fn is_open(&self) -> bool {
        self.child_count() > 0
    }

    /// The /Count value (negative = closed, positive = open).
    pub fn child_count(&self) -> i64 {
        self.outline
            .get(&Name::new("Count"))
            .and_then(Object::as_int)
            .unwrap_or(0)
    }

    /// Whether this outline item has children.
    pub fn has_children(&self) -> bool {
        self.child_count() != 0
    }

    /// Get the /First child reference.
    pub fn first_child_ref(&self) -> Option<ObjRef> {
        self.outline
            .get(&Name::new("First"))
            .and_then(Object::as_obj_ref)
    }

    /// Get the /Last child reference.
    pub fn last_child_ref(&self) -> Option<ObjRef> {
        self.outline
            .get(&Name::new("Last"))
            .and_then(Object::as_obj_ref)
    }

    /// Get the /Next sibling reference.
    pub fn next_ref(&self) -> Option<ObjRef> {
        self.outline
            .get(&Name::new("Next"))
            .and_then(Object::as_obj_ref)
    }

    /// Get the /Prev sibling reference.
    pub fn prev_ref(&self) -> Option<ObjRef> {
        self.outline
            .get(&Name::new("Prev"))
            .and_then(Object::as_obj_ref)
    }

    /// Get the /Parent reference.
    pub fn parent_ref(&self) -> Option<ObjRef> {
        self.outline
            .get(&Name::new("Parent"))
            .and_then(Object::as_obj_ref)
    }

    /// Get RGB color from /C array [r g b] (0.0–1.0).
    pub fn color(&self) -> Option<(f64, f64, f64)> {
        let arr = self.outline.get(&Name::new("C"))?.as_array()?;
        if arr.len() >= 3 {
            let r = arr.get(0)?.as_real()?;
            let g = arr.get(1)?.as_real()?;
            let b = arr.get(2)?.as_real()?;
            Some((r, g, b))
        } else {
            None
        }
    }

    /// Bit 1 of /F: bold style.
    pub fn is_bold(&self) -> bool {
        let flags = self
            .outline
            .get(&Name::new("F"))
            .and_then(Object::as_int)
            .unwrap_or(0) as u32;
        (flags & (1 << 1)) != 0
    }

    /// Bit 0 of /F: italic style.
    pub fn is_italic(&self) -> bool {
        let flags = self
            .outline
            .get(&Name::new("F"))
            .and_then(Object::as_int)
            .unwrap_or(0) as u32;
        (flags & (1 << 0)) != 0
    }

    /// Extract page reference from /Dest or /A (GoTo action).
    /// Handles: array dest [page_ref ...], direct page ref, or GoTo action with /D.
    pub fn page_destination(&self) -> Option<ObjRef> {
        // Try /Dest first
        if let Some(dest) = self.destination() {
            if let Some(page_ref) = extract_page_ref_from_dest(dest) {
                return Some(page_ref);
            }
        }

        // Try /A with /S /GoTo and /D
        let action = self.action()?.as_dict()?;
        if action
            .get(&Name::new("S"))
            .and_then(Object::as_name)?
            .as_str()
            != "GoTo"
        {
            return None;
        }
        let d = action.get(&Name::new("D"))?;
        extract_page_ref_from_dest(d)
    }
}

/// Extract page ObjRef from a destination object.
fn extract_page_ref_from_dest(dest: &Object) -> Option<ObjRef> {
    match dest {
        Object::Ref(r) => Some(*r),
        Object::Array(arr) => {
            let first = arr.first()?;
            first.as_obj_ref()
        }
        _ => None,
    }
}

/// Helper for document-level outline operations.
pub struct OutlineDocHelper;

impl OutlineDocHelper {
    /// Collect all top-level outline items by following /First -> /Next chain.
    pub fn get_top_level_items(
        outlines_dict: &Dict,
        objects: &HashMap<ObjRef, Object>,
    ) -> Vec<ObjRef> {
        let mut items = Vec::new();
        let mut current = outlines_dict
            .get(&Name::new("First"))
            .and_then(Object::as_obj_ref);

        while let Some(ref_obj) = current {
            items.push(ref_obj);
            let obj = match objects.get(&ref_obj) {
                Some(o) => o,
                None => break,
            };
            let dict = match obj.as_dict() {
                Some(d) => d,
                None => break,
            };
            current = dict.get(&Name::new("Next")).and_then(Object::as_obj_ref);
        }

        items
    }

    /// Flatten the outline tree with depth info. Returns (depth, ObjRef) pairs.
    pub fn flatten_outlines(
        outlines_dict: &Dict,
        objects: &HashMap<ObjRef, Object>,
    ) -> Vec<(i32, ObjRef)> {
        let mut result = Vec::new();
        let mut current = outlines_dict
            .get(&Name::new("First"))
            .and_then(Object::as_obj_ref);

        while let Some(ref_obj) = current {
            Self::flatten_recursive(ref_obj, 0, objects, &mut result);
            current = objects
                .get(&ref_obj)
                .and_then(|o| o.as_dict())
                .and_then(|d| d.get(&Name::new("Next")))
                .and_then(Object::as_obj_ref);
        }

        result
    }

    fn flatten_recursive(
        ref_obj: ObjRef,
        depth: i32,
        objects: &HashMap<ObjRef, Object>,
        result: &mut Vec<(i32, ObjRef)>,
    ) {
        result.push((depth, ref_obj));

        let Some(obj) = objects.get(&ref_obj) else {
            return;
        };
        let Some(dict) = obj.as_dict() else {
            return;
        };

        let helper = OutlineHelper::new(dict);
        if let Some(first_child) = helper.first_child_ref() {
            let child_depth = depth + 1;
            let mut current = Some(first_child);
            while let Some(child_ref) = current {
                Self::flatten_recursive(child_ref, child_depth, objects, result);
                let child_obj = objects.get(&child_ref);
                current = child_obj
                    .and_then(|o| o.as_dict())
                    .and_then(|d| d.get(&Name::new("Next")))
                    .and_then(Object::as_obj_ref);
            }
        }
    }

    /// Total count of outline items in the tree.
    pub fn count_outlines(outlines_dict: &Dict, objects: &HashMap<ObjRef, Object>) -> usize {
        Self::flatten_outlines(outlines_dict, objects).len()
    }
}

/// Create an outline item dictionary with /Title and /Dest.
pub fn build_outline_item(title: &str, dest: Object) -> Dict {
    let mut dict = HashMap::new();
    dict.insert(
        Name::new("Title"),
        Object::String(PdfString::new(title.as_bytes().to_vec())),
    );
    dict.insert(Name::new("Dest"), dest);
    dict
}

/// Decode outline title from PdfString (UTF-16BE with BOM or Latin-1/PDFDocEncoding).
pub fn decode_outline_title(title_obj: &Object) -> Option<String> {
    let pdf_str = title_obj.as_string()?;
    let bytes = pdf_str.as_bytes();

    // UTF-16BE with BOM (0xFE 0xFF) per PDF spec
    if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
        if let Some(s) = decode_utf16be(&bytes[2..]) {
            return Some(s);
        }
    }

    // Latin-1 / PDFDocEncoding fallback
    Some(bytes.iter().map(|&b| char::from(b)).collect::<String>())
}

fn decode_utf16be(bytes: &[u8]) -> Option<String> {
    if bytes.len() % 2 != 0 {
        return None;
    }
    let mut s = String::with_capacity(bytes.len() / 2);
    let mut i = 0;
    while i + 1 < bytes.len() {
        let code_unit = u16::from_be_bytes([bytes[i], bytes[i + 1]]);
        if code_unit == 0 {
            break;
        }
        if let Some(ch) = char::from_u32(code_unit as u32) {
            s.push(ch);
        }
        i += 2;
    }
    Some(s)
}

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

    fn make_outline_item(
        title: &str,
        dest: Option<Object>,
        count: Option<i64>,
        first: Option<ObjRef>,
        last: Option<ObjRef>,
        next: Option<ObjRef>,
        prev: Option<ObjRef>,
        parent: Option<ObjRef>,
        color: Option<(f64, f64, f64)>,
        flags: Option<i64>,
    ) -> Dict {
        let mut dict = HashMap::new();
        dict.insert(
            Name::new("Title"),
            Object::String(PdfString::new(title.as_bytes().to_vec())),
        );
        if let Some(d) = dest {
            dict.insert(Name::new("Dest"), d);
        }
        if let Some(c) = count {
            dict.insert(Name::new("Count"), Object::Int(c));
        }
        if let Some(f) = first {
            dict.insert(Name::new("First"), Object::Ref(f));
        }
        if let Some(l) = last {
            dict.insert(Name::new("Last"), Object::Ref(l));
        }
        if let Some(n) = next {
            dict.insert(Name::new("Next"), Object::Ref(n));
        }
        if let Some(p) = prev {
            dict.insert(Name::new("Prev"), Object::Ref(p));
        }
        if let Some(par) = parent {
            dict.insert(Name::new("Parent"), Object::Ref(par));
        }
        if let Some((r, g, b)) = color {
            dict.insert(
                Name::new("C"),
                Object::Array(vec![Object::Real(r), Object::Real(g), Object::Real(b)]),
            );
        }
        if let Some(f) = flags {
            dict.insert(Name::new("F"), Object::Int(f));
        }
        dict
    }

    #[test]
    fn test_outline_helper_new() {
        let dict = make_outline_item(
            "Chapter 1",
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.title(), Some("Chapter 1".to_string()));
    }

    #[test]
    fn test_outline_helper_title() {
        let dict = make_outline_item(
            "My Bookmark",
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.title(), Some("My Bookmark".to_string()));
    }

    #[test]
    fn test_outline_helper_title_missing() {
        let mut dict = HashMap::new();
        dict.insert(Name::new("Dest"), Object::Ref(ObjRef::new(5, 0)));
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.title(), None);
    }

    #[test]
    fn test_outline_helper_destination() {
        let dest = Object::Ref(ObjRef::new(10, 0));
        let dict = make_outline_item(
            "X",
            Some(dest.clone()),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.destination(), Some(&dest));
    }

    #[test]
    fn test_outline_helper_destination_array() {
        let dest = Object::Array(vec![
            Object::Ref(ObjRef::new(7, 0)),
            Object::Name(Name::new("Fit")),
        ]);
        let dict = make_outline_item(
            "Y",
            Some(dest.clone()),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.destination(), Some(&dest));
    }

    #[test]
    fn test_outline_helper_action() {
        let mut action_dict = HashMap::new();
        action_dict.insert(Name::new("S"), Object::Name(Name::new("GoTo")));
        action_dict.insert(Name::new("D"), Object::Ref(ObjRef::new(99, 0)));
        let action = Object::Dict(action_dict);
        let mut dict = make_outline_item("Z", None, None, None, None, None, None, None, None, None);
        dict.insert(Name::new("A"), action);
        let helper = OutlineHelper::new(&dict);
        assert!(helper.action().is_some());
    }

    #[test]
    fn test_outline_helper_is_open() {
        let dict = make_outline_item(
            "Open",
            None,
            Some(3),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert!(helper.is_open());

        let dict = make_outline_item(
            "Closed",
            None,
            Some(-2),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert!(!helper.is_open());

        let dict = make_outline_item(
            "NoCount", None, None, None, None, None, None, None, None, None,
        );
        let helper = OutlineHelper::new(&dict);
        assert!(!helper.is_open());
    }

    #[test]
    fn test_outline_helper_child_count() {
        let dict = make_outline_item("C", None, Some(5), None, None, None, None, None, None, None);
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.child_count(), 5);

        let dict = make_outline_item(
            "C2",
            None,
            Some(-2),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.child_count(), -2);
    }

    #[test]
    fn test_outline_helper_has_children() {
        let dict = make_outline_item(
            "Yes",
            None,
            Some(1),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert!(helper.has_children());

        let dict = make_outline_item("No", None, None, None, None, None, None, None, None, None);
        let helper = OutlineHelper::new(&dict);
        assert!(!helper.has_children());
    }

    #[test]
    fn test_outline_helper_refs() {
        let first = ObjRef::new(20, 0);
        let last = ObjRef::new(22, 0);
        let next = ObjRef::new(2, 0);
        let prev = ObjRef::new(0, 0);
        let parent = ObjRef::new(1, 0);
        let dict = make_outline_item(
            "R",
            None,
            Some(2),
            Some(first),
            Some(last),
            Some(next),
            Some(prev),
            Some(parent),
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.first_child_ref(), Some(first));
        assert_eq!(helper.last_child_ref(), Some(last));
        assert_eq!(helper.next_ref(), Some(next));
        assert_eq!(helper.prev_ref(), Some(prev));
        assert_eq!(helper.parent_ref(), Some(parent));
    }

    #[test]
    fn test_outline_helper_color() {
        let dict = make_outline_item(
            "Red",
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some((1.0, 0.0, 0.0)),
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.color(), Some((1.0, 0.0, 0.0)));
    }

    #[test]
    fn test_outline_helper_color_missing() {
        let dict = make_outline_item(
            "NoColor", None, None, None, None, None, None, None, None, None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.color(), None);
    }

    #[test]
    fn test_outline_helper_is_bold_italic() {
        // Flags: bit 0 = italic, bit 1 = bold. So 3 = both.
        let dict = make_outline_item(
            "BI",
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(3),
        );
        let helper = OutlineHelper::new(&dict);
        assert!(helper.is_bold());
        assert!(helper.is_italic());

        let dict = make_outline_item(
            "Plain",
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            Some(0),
        );
        let helper = OutlineHelper::new(&dict);
        assert!(!helper.is_bold());
        assert!(!helper.is_italic());
    }

    #[test]
    fn test_outline_helper_page_destination_from_ref() {
        let page_ref = ObjRef::new(42, 0);
        let dest = Object::Ref(page_ref);
        let dict = make_outline_item(
            "P",
            Some(dest),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.page_destination(), Some(page_ref));
    }

    #[test]
    fn test_outline_helper_page_destination_from_array() {
        let page_ref = ObjRef::new(100, 0);
        let dest = Object::Array(vec![
            Object::Ref(page_ref),
            Object::Name(Name::new("XYZ")),
            Object::Int(0),
            Object::Int(800),
            Object::Int(600),
        ]);
        let dict = make_outline_item(
            "P2",
            Some(dest),
            None,
            None,
            None,
            None,
            None,
            None,
            None,
            None,
        );
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.page_destination(), Some(page_ref));
    }

    #[test]
    fn test_outline_helper_page_destination_from_action() {
        let page_ref = ObjRef::new(50, 0);
        let mut action_dict = HashMap::new();
        action_dict.insert(Name::new("S"), Object::Name(Name::new("GoTo")));
        action_dict.insert(Name::new("D"), Object::Ref(page_ref));
        let action = Object::Dict(action_dict);
        let mut dict = make_outline_item("A", None, None, None, None, None, None, None, None, None);
        dict.insert(Name::new("A"), action);
        let helper = OutlineHelper::new(&dict);
        assert_eq!(helper.page_destination(), Some(page_ref));
    }

    #[test]
    fn test_outline_doc_helper_get_top_level_items() {
        let mut objects = HashMap::new();
        let ref1 = ObjRef::new(1, 0);
        let ref2 = ObjRef::new(2, 0);
        let ref3 = ObjRef::new(3, 0);

        let mut item1 =
            make_outline_item("1", None, None, None, None, None, None, None, None, None);
        item1.insert(Name::new("Next"), Object::Ref(ref2));
        objects.insert(ref1, Object::Dict(item1));

        let mut item2 =
            make_outline_item("2", None, None, None, None, None, None, None, None, None);
        item2.insert(Name::new("Next"), Object::Ref(ref3));
        objects.insert(ref2, Object::Dict(item2));

        let mut item3 =
            make_outline_item("3", None, None, None, None, None, None, None, None, None);
        objects.insert(ref3, Object::Dict(item3));

        let mut outlines = HashMap::new();
        outlines.insert(Name::new("Type"), Object::Name(Name::new("Outlines")));
        outlines.insert(Name::new("First"), Object::Ref(ref1));

        let items = OutlineDocHelper::get_top_level_items(&outlines, &objects);
        assert_eq!(items, vec![ref1, ref2, ref3]);
    }

    #[test]
    fn test_outline_doc_helper_flatten_outlines() {
        let mut objects = HashMap::new();
        let ref1 = ObjRef::new(1, 0);
        let ref2 = ObjRef::new(2, 0);
        let ref3 = ObjRef::new(3, 0);

        let mut item1 = make_outline_item(
            "1",
            None,
            Some(1),
            Some(ref2),
            Some(ref2),
            Some(ref3),
            None,
            None,
            None,
            None,
        );
        objects.insert(ref1, Object::Dict(item1));

        let mut item2 = make_outline_item(
            "2",
            None,
            None,
            None,
            None,
            None,
            None,
            Some(ref1),
            None,
            None,
        );
        objects.insert(ref2, Object::Dict(item2));

        let item3 = make_outline_item("3", None, None, None, None, None, None, None, None, None);
        objects.insert(ref3, Object::Dict(item3));

        let mut outlines = HashMap::new();
        outlines.insert(Name::new("Type"), Object::Name(Name::new("Outlines")));
        outlines.insert(Name::new("First"), Object::Ref(ref1));

        let flat = OutlineDocHelper::flatten_outlines(&outlines, &objects);
        assert_eq!(flat.len(), 3);
        assert_eq!(flat[0], (0, ref1));
        assert_eq!(flat[1], (1, ref2));
        assert_eq!(flat[2], (0, ref3));
    }

    #[test]
    fn test_outline_doc_helper_count_outlines() {
        let mut objects = HashMap::new();
        let ref1 = ObjRef::new(1, 0);
        let ref2 = ObjRef::new(2, 0);

        let mut item1 = make_outline_item(
            "1",
            None,
            None,
            None,
            None,
            Some(ref2),
            None,
            None,
            None,
            None,
        );
        objects.insert(ref1, Object::Dict(item1));

        let item2 = make_outline_item("2", None, None, None, None, None, None, None, None, None);
        objects.insert(ref2, Object::Dict(item2));

        let mut outlines = HashMap::new();
        outlines.insert(Name::new("First"), Object::Ref(ref1));

        assert_eq!(OutlineDocHelper::count_outlines(&outlines, &objects), 2);
    }

    #[test]
    fn test_build_outline_item() {
        let dest = Object::Array(vec![
            Object::Ref(ObjRef::new(5, 0)),
            Object::Name(Name::new("Fit")),
        ]);
        let dict = build_outline_item("New Bookmark", dest);
        assert_eq!(
            dict.get(&Name::new("Title"))
                .and_then(|o| decode_outline_title(o)),
            Some("New Bookmark".to_string())
        );
        assert!(dict.contains_key(&Name::new("Dest")));
    }

    #[test]
    fn test_decode_outline_title_latin1() {
        let obj = Object::String(PdfString::new(b"Hello World".to_vec()));
        assert_eq!(decode_outline_title(&obj), Some("Hello World".to_string()));
    }

    #[test]
    fn test_decode_outline_title_utf16be() {
        // "Hi" in UTF-16BE: 0x00 0x48 0x00 0x69
        let bytes = vec![0xFE, 0xFF, 0x00, 0x48, 0x00, 0x69];
        let obj = Object::String(PdfString::new(bytes));
        assert_eq!(decode_outline_title(&obj), Some("Hi".to_string()));
    }

    #[test]
    fn test_decode_outline_title_not_string() {
        let obj = Object::Int(42);
        assert_eq!(decode_outline_title(&obj), None);
    }
}
