//! Print production and prepress features per PDF spec
//!
//! Page boxes, output intents, separation info, halftone, overprint, OPI, and transfer functions.

use crate::fitz::error::{Error, Result};
use crate::pdf::document::PdfDocument;
use crate::pdf::object::{Array, Dict, Name, ObjRef, Object, PdfString};
use crate::pdf::page::Rect;
use std::collections::HashMap;

// ============================================================================
// PageBox and PageBoxes
// ============================================================================

/// PDF page box types (ISO 32000)
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum PageBox {
    /// MediaBox - physical page size (required, inheritable)
    MediaBox,
    /// CropBox - visible page area (defaults to MediaBox)
    CropBox,
    /// BleedBox - area including bleed
    BleedBox,
    /// TrimBox - final trimmed size
    TrimBox,
    /// ArtBox - meaningful content area
    ArtBox,
}

impl PageBox {
    pub fn dict_key(&self) -> Name {
        match self {
            PageBox::MediaBox => Name::new("MediaBox"),
            PageBox::CropBox => Name::new("CropBox"),
            PageBox::BleedBox => Name::new("BleedBox"),
            PageBox::TrimBox => Name::new("TrimBox"),
            PageBox::ArtBox => Name::new("ArtBox"),
        }
    }
}

/// All five page box rectangles with inheritance applied
#[derive(Debug, Clone)]
pub struct PageBoxes {
    pub media_box: Rect,
    pub crop_box: Option<Rect>,
    pub bleed_box: Option<Rect>,
    pub trim_box: Option<Rect>,
    pub art_box: Option<Rect>,
}

impl PageBoxes {
    pub fn crop_box(&self) -> Rect {
        self.crop_box.unwrap_or(self.media_box)
    }

    pub fn bleed_box(&self) -> Rect {
        self.bleed_box.or(self.trim_box).unwrap_or(self.crop_box())
    }

    pub fn trim_box(&self) -> Rect {
        self.trim_box.or(self.crop_box).unwrap_or(self.media_box)
    }

    pub fn art_box(&self) -> Rect {
        self.art_box.or(self.crop_box).unwrap_or(self.media_box)
    }
}

fn parse_rect_from_dict(dict: &Dict, key: &Name) -> Result<Option<Rect>> {
    let obj = match dict.get(key) {
        Some(o) => o,
        None => return Ok(None),
    };
    let arr = match obj {
        Object::Array(a) => a,
        Object::Ref(_) => {
            return Err(Error::Generic(format!(
                "{} must be resolved before parsing",
                key.as_str()
            )));
        }
        _ => return Err(Error::Generic(format!("{} must be an array", key.as_str()))),
    };
    Ok(Some(Rect::from_array(arr)?))
}

/// Extract all box rectangles from a page dictionary with inheritance from parent Pages
pub fn parse_page_boxes(doc: &PdfDocument, page_dict: &Dict) -> Result<PageBoxes> {
    let mut media_box = None;
    let mut crop_box = None;
    let mut bleed_box = None;
    let mut trim_box = None;
    let mut art_box = None;

    let mut current: Option<Dict> = Some(page_dict.clone());
    while let Some(dict) = current {
        if media_box.is_none() {
            if let Some(r) = parse_rect_from_dict(&dict, &Name::new("MediaBox"))? {
                media_box = Some(r);
            }
        }
        if crop_box.is_none() {
            if let Some(r) = parse_rect_from_dict(&dict, &Name::new("CropBox"))? {
                crop_box = Some(r);
            }
        }
        if bleed_box.is_none() {
            if let Some(r) = parse_rect_from_dict(&dict, &Name::new("BleedBox"))? {
                bleed_box = Some(r);
            }
        }
        if trim_box.is_none() {
            if let Some(r) = parse_rect_from_dict(&dict, &Name::new("TrimBox"))? {
                trim_box = Some(r);
            }
        }
        if art_box.is_none() {
            if let Some(r) = parse_rect_from_dict(&dict, &Name::new("ArtBox"))? {
                art_box = Some(r);
            }
        }

        let parent_obj = dict.get(&Name::new("Parent"));
        current = parent_obj.and_then(|o| {
            let resolved = doc.resolve(o);
            resolved.as_dict().cloned()
        });
    }

    let media_box = media_box.ok_or_else(|| Error::Generic("MediaBox is required".into()))?;

    Ok(PageBoxes {
        media_box,
        crop_box,
        bleed_box,
        trim_box,
        art_box,
    })
}

// ============================================================================
// OutputIntent
// ============================================================================

/// OutputIntent subtype (S key)
#[derive(Debug, Clone, PartialEq, Eq)]
#[allow(non_camel_case_types)] // PDF spec uses these exact names
pub enum OutputIntentType {
    GTS_PDFX,
    GTS_PDFA1,
    ISO_PDFE1,
    Other(String),
}

impl OutputIntentType {
    fn from_name(s: &str) -> Self {
        match s {
            "GTS_PDFX" => OutputIntentType::GTS_PDFX,
            "GTS_PDFA1" => OutputIntentType::GTS_PDFA1,
            "ISO_PDFE1" => OutputIntentType::ISO_PDFE1,
            other => OutputIntentType::Other(other.to_string()),
        }
    }
}

/// Output intent for print/archival/engineering
#[derive(Debug, Clone)]
pub struct OutputIntent {
    pub intent_type: OutputIntentType,
    pub output_condition: Option<String>,
    pub output_condition_identifier: Option<String>,
    pub info: Option<String>,
    pub registry: Option<String>,
    pub dest_output_profile: Option<Vec<u8>>,
}

/// Parse /OutputIntents array from catalog
pub fn parse_output_intents(doc: &PdfDocument) -> Result<Vec<OutputIntent>> {
    let catalog = doc.catalog()?;
    let intents_obj = match catalog.get(&Name::new("OutputIntents")) {
        Some(o) => o,
        None => return Ok(vec![]),
    };
    let resolved = doc.resolve(intents_obj);
    let arr = resolved
        .as_array()
        .ok_or_else(|| Error::Generic("/OutputIntents must be an array".into()))?;

    let mut result = Vec::new();
    for item in arr {
        let resolved_item = doc.resolve(item);
        let dict = resolved_item
            .as_dict()
            .ok_or_else(|| Error::Generic("OutputIntent entry must be a dictionary".into()))?;

        let s = dict
            .get(&Name::new("S"))
            .and_then(|o| o.as_name())
            .map(|n| n.as_str().to_string())
            .unwrap_or_else(|| "GTS_PDFX".to_string());
        let intent_type = OutputIntentType::from_name(&s);

        let output_condition = dict
            .get(&Name::new("OutputCondition"))
            .map(|o| doc.resolve(o))
            .and_then(|r| r.as_string().and_then(|s| s.as_str().map(String::from)));

        let output_condition_identifier = dict
            .get(&Name::new("OutputConditionIdentifier"))
            .map(|o| doc.resolve(o))
            .and_then(|r| r.as_string().and_then(|s| s.as_str().map(String::from)));

        let info = dict
            .get(&Name::new("Info"))
            .map(|o| doc.resolve(o))
            .and_then(|r| r.as_string().and_then(|s| s.as_str().map(String::from)));

        let registry = dict
            .get(&Name::new("RegistryName"))
            .map(|o| doc.resolve(o))
            .and_then(|r| r.as_string().and_then(|s| s.as_str().map(String::from)));

        let dest_output_profile = dict.get(&Name::new("DestOutputProfile")).and_then(|o| {
            let resolved = doc.resolve(o);
            match &resolved {
                Object::Stream { data, .. } => Some(data.clone()),
                _ => None,
            }
        });

        result.push(OutputIntent {
            intent_type,
            output_condition,
            output_condition_identifier,
            info,
            registry,
            dest_output_profile,
        });
    }
    Ok(result)
}

// ============================================================================
// TrapNet
// ============================================================================

/// Trapping information (PDF 1.3+)
#[derive(Debug, Clone)]
pub struct TrapNet {
    pub dict: Dict,
}

// ============================================================================
// SeparationInfo
// ============================================================================

/// Separation color names and colorant order
#[derive(Debug, Clone)]
pub struct SeparationInfo {
    pub colorant_names: Vec<String>,
    pub colorant_order: Option<Vec<String>>,
}

/// Parse /SeparationInfo from page dictionary
pub fn parse_separation_info(
    doc: &PdfDocument,
    page_dict: &Dict,
) -> Result<Option<SeparationInfo>> {
    let info_obj = match page_dict.get(&Name::new("SeparationInfo")) {
        Some(o) => o,
        None => return Ok(None),
    };
    let resolved = doc.resolve(info_obj);
    let dict = resolved
        .as_dict()
        .ok_or_else(|| Error::Generic("/SeparationInfo must be a dictionary".into()))?;

    let colorant_names = dict
        .get(&Name::new("Colorants"))
        .map(|o| {
            let r = doc.resolve(o);
            r.as_array()
                .map(|arr| {
                    arr.iter()
                        .filter_map(|item| {
                            doc.resolve(item)
                                .as_string()
                                .and_then(|s| s.as_str().map(String::from))
                        })
                        .collect()
                })
                .unwrap_or_default()
        })
        .unwrap_or_default();

    let colorant_order = dict.get(&Name::new("ColorantOrder")).and_then(|o| {
        let r = doc.resolve(o);
        r.as_array().map(|arr| {
            arr.iter()
                .filter_map(|item| {
                    doc.resolve(item)
                        .as_string()
                        .and_then(|s| s.as_str().map(String::from))
                })
                .collect()
        })
    });

    Ok(Some(SeparationInfo {
        colorant_names,
        colorant_order,
    }))
}

// ============================================================================
// OPI (Open Prepress Interface)
// ============================================================================

/// OPI version and file reference
#[derive(Debug, Clone)]
pub struct OPI {
    pub version: Option<String>,
    pub file_spec: Option<String>,
    pub size: Option<(f32, f32)>,
    pub crop_rect: Option<Rect>,
    pub position: Option<Rect>,
}

// ============================================================================
// TransferFunction
// ============================================================================

/// Transfer function for color (TR, TR2 in graphics state)
#[derive(Debug, Clone)]
pub struct TransferFunction {
    pub function: Dict,
}

// ============================================================================
// HalftoneInfo
// ============================================================================

/// Halftone type
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HalftoneType {
    Default,
    Simple,
    SimpleColor,
    Spot,
    SpotColor,
    Frequency,
    FrequencyColor,
    Other(String),
}

/// Halftone information
#[derive(Debug, Clone)]
pub struct HalftoneInfo {
    pub halftone_type: HalftoneType,
    pub frequency: Option<f32>,
    pub angle: Option<f32>,
    pub spot_function: Option<Name>,
    pub transfer_function: Option<TransferFunction>,
}

fn halftone_type_from_name(s: &str) -> HalftoneType {
    match s {
        "Default" => HalftoneType::Default,
        "Simple" => HalftoneType::Simple,
        "SimpleColor" => HalftoneType::SimpleColor,
        "Spot" => HalftoneType::Spot,
        "SpotColor" => HalftoneType::SpotColor,
        "Frequency" => HalftoneType::Frequency,
        "FrequencyColor" => HalftoneType::FrequencyColor,
        other => HalftoneType::Other(other.to_string()),
    }
}

fn halftone_type_from_int(i: i64) -> HalftoneType {
    match i {
        1 => HalftoneType::Simple,
        2 => HalftoneType::SimpleColor,
        3 => HalftoneType::Spot,
        4 => HalftoneType::SpotColor,
        5 => HalftoneType::Frequency,
        6 => HalftoneType::FrequencyColor,
        _ => HalftoneType::Other(format!("{}", i)),
    }
}

/// Parse /HT (halftone) from graphics state or page resources
pub fn parse_halftone(doc: &PdfDocument, dict: &Dict) -> Result<Option<HalftoneInfo>> {
    let ht_obj = match dict.get(&Name::new("HT")) {
        Some(o) => o,
        None => return Ok(None),
    };
    let resolved = doc.resolve(ht_obj);

    let (ht_dict, halftone_type) = match &resolved {
        Object::Name(n) => {
            if n.as_str() == "Default" {
                return Ok(Some(HalftoneInfo {
                    halftone_type: HalftoneType::Default,
                    frequency: None,
                    angle: None,
                    spot_function: None,
                    transfer_function: None,
                }));
            }
            return Ok(None);
        }
        Object::Dict(d) => {
            let t = d
                .get(&Name::new("HalftoneType"))
                .and_then(|o| doc.resolve(o).as_int())
                .map(halftone_type_from_int)
                .or_else(|| {
                    d.get(&Name::new("Type"))
                        .map(|o| doc.resolve(o))
                        .and_then(|r| r.as_name().map(|n| halftone_type_from_name(n.as_str())))
                })
                .unwrap_or(HalftoneType::Other("Unknown".to_string()));
            (d.clone(), t)
        }
        _ => return Ok(None),
    };

    let frequency = ht_dict
        .get(&Name::new("Frequency"))
        .and_then(|o| doc.resolve(o).as_real())
        .map(|r| r as f32);

    let angle = ht_dict
        .get(&Name::new("Angle"))
        .and_then(|o| doc.resolve(o).as_real())
        .map(|r| r as f32);

    let spot_function = ht_dict
        .get(&Name::new("SpotFunction"))
        .map(|o| doc.resolve(o))
        .and_then(|r| r.as_name().cloned());

    let transfer_function = ht_dict.get(&Name::new("TransferFunction")).and_then(|o| {
        let r = doc.resolve(o);
        r.as_dict().map(|d| TransferFunction {
            function: d.clone(),
        })
    });

    Ok(Some(HalftoneInfo {
        halftone_type,
        frequency,
        angle,
        spot_function,
        transfer_function,
    }))
}

// ============================================================================
// OverprintSettings
// ============================================================================

/// Overprint settings (OP, op, OPM)
#[derive(Debug, Clone)]
pub struct OverprintSettings {
    pub stroke_overprint: bool,
    pub fill_overprint: bool,
    pub overprint_mode: i32,
}

/// Parse overprint settings from extended graphics state
pub fn parse_overprint(dict: &Dict) -> OverprintSettings {
    let stroke_overprint = dict
        .get(&Name::new("OP"))
        .and_then(|o| o.as_bool())
        .unwrap_or(false);

    let fill_overprint = dict
        .get(&Name::new("op"))
        .and_then(|o| o.as_bool())
        .unwrap_or(false);

    let overprint_mode = dict
        .get(&Name::new("OPM"))
        .and_then(|o| o.as_int())
        .unwrap_or(0) as i32;

    OverprintSettings {
        stroke_overprint,
        fill_overprint,
        overprint_mode,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::pdf::object::ObjRef;

    fn media_box_array(x1: f64, y1: f64) -> Vec<Object> {
        vec![
            Object::Int(0),
            Object::Int(0),
            Object::Real(x1),
            Object::Real(y1),
        ]
    }

    #[test]
    fn test_page_box_inheritance_media_from_parent() {
        let mut doc = PdfDocument::new();
        let mut pages_dict = Dict::new();
        pages_dict.insert(Name::new("Type"), Object::Name(Name::new("Pages")));
        pages_dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(612.0, 792.0)),
        );
        pages_dict.insert(
            Name::new("CropBox"),
            Object::Array(vec![
                Object::Int(10),
                Object::Int(10),
                Object::Real(602.0),
                Object::Real(782.0),
            ]),
        );
        doc.objects.insert((2, 0), Object::Dict(pages_dict));

        let mut page_dict = Dict::new();
        page_dict.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        page_dict.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
        doc.objects.insert((3, 0), Object::Dict(page_dict.clone()));

        let boxes = parse_page_boxes(&doc, &page_dict).unwrap();
        assert_eq!(boxes.media_box.x1, 612.0);
        assert_eq!(boxes.media_box.y1, 792.0);
        assert_eq!(boxes.crop_box().x0, 10.0);
        assert_eq!(boxes.crop_box().x1, 602.0);
    }

    #[test]
    fn test_page_box_inheritance_override() {
        let mut doc = PdfDocument::new();
        let mut pages_dict = Dict::new();
        pages_dict.insert(Name::new("Type"), Object::Name(Name::new("Pages")));
        pages_dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(612.0, 792.0)),
        );
        doc.objects.insert((2, 0), Object::Dict(pages_dict));

        let mut page_dict = Dict::new();
        page_dict.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        page_dict.insert(Name::new("Parent"), Object::Ref(ObjRef::new(2, 0)));
        page_dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(595.0, 842.0)),
        );
        doc.objects.insert((3, 0), Object::Dict(page_dict.clone()));

        let boxes = parse_page_boxes(&doc, &page_dict).unwrap();
        assert_eq!(boxes.media_box.x1, 595.0);
        assert_eq!(boxes.media_box.y1, 842.0);
    }

    #[test]
    fn test_page_box_missing_media() {
        let doc = PdfDocument::new();
        let mut page_dict = Dict::new();
        page_dict.insert(Name::new("Type"), Object::Name(Name::new("Page")));
        assert!(parse_page_boxes(&doc, &page_dict).is_err());
    }

    #[test]
    fn test_output_intents_empty() {
        let mut doc = PdfDocument::new();
        let mut catalog = Dict::new();
        catalog.insert(Name::new("Type"), Object::Name(Name::new("Catalog")));
        catalog.insert(Name::new("Pages"), Object::Ref(ObjRef::new(2, 0)));
        doc.trailer
            .insert(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        doc.objects.insert((1, 0), Object::Dict(catalog));
        doc.objects.insert(
            (2, 0),
            Object::Dict(
                [
                    (Name::new("Type"), Object::Name(Name::new("Pages"))),
                    (Name::new("Count"), Object::Int(0)),
                    (Name::new("Kids"), Object::Array(vec![])),
                ]
                .into_iter()
                .collect(),
            ),
        );

        let intents = parse_output_intents(&doc).unwrap();
        assert!(intents.is_empty());
    }

    #[test]
    fn test_output_intents_parsed() {
        let mut doc = PdfDocument::new();
        let mut intent_dict = Dict::new();
        intent_dict.insert(Name::new("S"), Object::Name(Name::new("GTS_PDFX")));
        intent_dict.insert(
            Name::new("OutputConditionIdentifier"),
            Object::String(PdfString::new(b"sRGB IEC61966-2.1".to_vec())),
        );
        intent_dict.insert(
            Name::new("Info"),
            Object::String(PdfString::new(b"sRGB".to_vec())),
        );
        doc.objects.insert((10, 0), Object::Dict(intent_dict));

        let mut catalog = Dict::new();
        catalog.insert(Name::new("Type"), Object::Name(Name::new("Catalog")));
        catalog.insert(Name::new("Pages"), Object::Ref(ObjRef::new(2, 0)));
        catalog.insert(
            Name::new("OutputIntents"),
            Object::Array(vec![Object::Ref(ObjRef::new(10, 0))]),
        );
        doc.trailer
            .insert(Name::new("Root"), Object::Ref(ObjRef::new(1, 0)));
        doc.objects.insert((1, 0), Object::Dict(catalog));
        doc.objects.insert(
            (2, 0),
            Object::Dict(
                [
                    (Name::new("Type"), Object::Name(Name::new("Pages"))),
                    (Name::new("Count"), Object::Int(0)),
                    (Name::new("Kids"), Object::Array(vec![])),
                ]
                .into_iter()
                .collect(),
            ),
        );

        let intents = parse_output_intents(&doc).unwrap();
        assert_eq!(intents.len(), 1);
        assert!(matches!(intents[0].intent_type, OutputIntentType::GTS_PDFX));
        assert_eq!(
            intents[0].output_condition_identifier.as_deref(),
            Some("sRGB IEC61966-2.1")
        );
        assert_eq!(intents[0].info.as_deref(), Some("sRGB"));
    }

    #[test]
    fn test_separation_info_parsed() {
        let mut doc = PdfDocument::new();
        let mut sep_dict = Dict::new();
        sep_dict.insert(
            Name::new("Colorants"),
            Object::Array(vec![
                Object::String(PdfString::new(b"Cyan".to_vec())),
                Object::String(PdfString::new(b"Magenta".to_vec())),
            ]),
        );
        doc.objects.insert((20, 0), Object::Dict(sep_dict));

        let mut page_dict = Dict::new();
        page_dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(612.0, 792.0)),
        );
        page_dict.insert(Name::new("SeparationInfo"), Object::Ref(ObjRef::new(20, 0)));
        doc.objects.insert((3, 0), Object::Dict(page_dict.clone()));

        let info = parse_separation_info(&doc, &page_dict).unwrap();
        let info = info.unwrap();
        assert_eq!(info.colorant_names, vec!["Cyan", "Magenta"]);
    }

    #[test]
    fn test_separation_info_absent() {
        let mut doc = PdfDocument::new();
        let mut page_dict = Dict::new();
        page_dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(612.0, 792.0)),
        );
        let info = parse_separation_info(&doc, &page_dict).unwrap();
        assert!(info.is_none());
    }

    #[test]
    fn test_halftone_default() {
        let mut doc = PdfDocument::new();
        let mut ext_gstate = Dict::new();
        ext_gstate.insert(Name::new("HT"), Object::Name(Name::new("Default")));
        let ht = parse_halftone(&doc, &ext_gstate).unwrap();
        let ht = ht.unwrap();
        assert_eq!(ht.halftone_type, HalftoneType::Default);
    }

    #[test]
    fn test_halftone_simple() {
        let mut doc = PdfDocument::new();
        let mut ht_dict = Dict::new();
        ht_dict.insert(Name::new("Type"), Object::Name(Name::new("Halftone")));
        ht_dict.insert(Name::new("HalftoneType"), Object::Int(1));
        ht_dict.insert(Name::new("Frequency"), Object::Real(150.0));
        ht_dict.insert(Name::new("Angle"), Object::Real(45.0));
        doc.objects.insert((30, 0), Object::Dict(ht_dict));

        let mut ext_gstate = Dict::new();
        ext_gstate.insert(Name::new("HT"), Object::Ref(ObjRef::new(30, 0)));
        doc.objects
            .insert((31, 0), Object::Dict(ext_gstate.clone()));

        let ht = parse_halftone(&doc, &ext_gstate).unwrap();
        let ht = ht.unwrap();
        assert_eq!(ht.frequency, Some(150.0));
        assert_eq!(ht.angle, Some(45.0));
    }

    #[test]
    fn test_parse_overprint() {
        let mut dict = Dict::new();
        dict.insert(Name::new("OP"), Object::Bool(true));
        dict.insert(Name::new("op"), Object::Bool(false));
        dict.insert(Name::new("OPM"), Object::Int(1));
        let op = parse_overprint(&dict);
        assert!(op.stroke_overprint);
        assert!(!op.fill_overprint);
        assert_eq!(op.overprint_mode, 1);
    }

    #[test]
    fn test_parse_overprint_defaults() {
        let dict = Dict::new();
        let op = parse_overprint(&dict);
        assert!(!op.stroke_overprint);
        assert!(!op.fill_overprint);
        assert_eq!(op.overprint_mode, 0);
    }
}
