//! PDF page implementation

use crate::fitz::error::{Error, Result};

use super::object::{Dict, Name, ObjRef, Object};

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rect {
    pub x0: f32,
    pub y0: f32,
    pub x1: f32,
    pub y1: f32,
}

impl Rect {
    pub fn new(x0: f32, y0: f32, x1: f32, y1: f32) -> Self {
        Self { x0, y0, x1, y1 }
    }

    pub fn width(&self) -> f32 {
        (self.x1 - self.x0).abs()
    }

    pub fn height(&self) -> f32 {
        (self.y1 - self.y0).abs()
    }

    pub fn is_empty(&self) -> bool {
        self.x0 >= self.x1 || self.y0 >= self.y1
    }

    pub fn from_array(arr: &[Object]) -> Result<Self> {
        if arr.len() < 4 {
            return Err(Error::Generic("Rect array must have 4 elements".into()));
        }
        let x0 = arr[0]
            .as_real()
            .ok_or_else(|| Error::Generic("Rect x0 must be a number".into()))?
            as f32;
        let y0 = arr[1]
            .as_real()
            .ok_or_else(|| Error::Generic("Rect y0 must be a number".into()))?
            as f32;
        let x1 = arr[2]
            .as_real()
            .ok_or_else(|| Error::Generic("Rect x1 must be a number".into()))?
            as f32;
        let y1 = arr[3]
            .as_real()
            .ok_or_else(|| Error::Generic("Rect y1 must be a number".into()))?
            as f32;
        Ok(Self::new(x0, y0, x1, y1))
    }
}

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)?))
}

fn parse_required_rect(dict: &Dict, key: &Name) -> Result<Rect> {
    parse_rect_from_dict(dict, key)?
        .ok_or_else(|| Error::Generic(format!("Missing required {}", key.as_str())))
}

#[derive(Debug, Clone)]
pub struct Page {
    pub dict: Dict,
    pub number: usize,
    pub media_box: Rect,
    pub crop_box: Option<Rect>,
    pub bleed_box: Option<Rect>,
    pub trim_box: Option<Rect>,
    pub art_box: Option<Rect>,
    pub rotation: i32,
}

impl Page {
    pub fn from_dict(dict: Dict, number: usize) -> Result<Self> {
        let media_box = parse_required_rect(&dict, &Name::new("MediaBox"))?;
        let crop_box = parse_rect_from_dict(&dict, &Name::new("CropBox"))?;
        let bleed_box = parse_rect_from_dict(&dict, &Name::new("BleedBox"))?;
        let trim_box = parse_rect_from_dict(&dict, &Name::new("TrimBox"))?;
        let art_box = parse_rect_from_dict(&dict, &Name::new("ArtBox"))?;
        let rotation = dict
            .get(&Name::new("Rotate"))
            .and_then(|o| o.as_int())
            .unwrap_or(0) as i32;
        Ok(Self {
            dict,
            number,
            media_box,
            crop_box,
            bleed_box,
            trim_box,
            art_box,
            rotation,
        })
    }

    pub fn media_box(&self) -> &Rect {
        &self.media_box
    }

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

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

    pub fn rotation(&self) -> i32 {
        self.rotation
    }

    pub fn width(&self) -> f32 {
        self.media_box.width()
    }

    pub fn height(&self) -> f32 {
        self.media_box.height()
    }

    pub fn resources(&self) -> Option<&Dict> {
        self.dict
            .get(&Name::new("Resources"))
            .and_then(|o| o.as_dict())
    }

    pub fn contents_refs(&self) -> Vec<ObjRef> {
        let obj = match self.dict.get(&Name::new("Contents")) {
            Some(o) => o,
            None => return vec![],
        };
        match obj {
            Object::Ref(r) => vec![*r],
            Object::Array(arr) => arr
                .iter()
                .filter_map(|o| {
                    if let Object::Ref(r) = o {
                        Some(*r)
                    } else {
                        None
                    }
                })
                .collect(),
            _ => vec![],
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PageLabelStyle {
    Decimal,
    UpperRoman,
    LowerRoman,
    UpperAlpha,
    LowerAlpha,
    None,
}

#[derive(Debug, Clone)]
pub struct PageLabel {
    pub style: PageLabelStyle,
    pub prefix: String,
    pub start: i32,
}

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

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

    #[test]
    fn test_rect_new() {
        let r = Rect::new(0.0, 0.0, 100.0, 200.0);
        assert_eq!(r.x0, 0.0);
        assert_eq!(r.y1, 200.0);
        assert_eq!(r.width(), 100.0);
        assert_eq!(r.height(), 200.0);
    }

    #[test]
    fn test_rect_is_empty() {
        assert!(Rect::new(0.0, 0.0, 0.0, 0.0).is_empty());
        assert!(Rect::new(10.0, 10.0, 5.0, 20.0).is_empty());
        assert!(!Rect::new(0.0, 0.0, 1.0, 1.0).is_empty());
    }

    #[test]
    fn test_rect_from_array() {
        let arr = vec![
            Object::Int(0),
            Object::Int(0),
            Object::Real(612.0),
            Object::Real(792.0),
        ];
        let r = Rect::from_array(&arr).unwrap();
        assert_eq!(r.x0, 0.0);
        assert_eq!(r.y0, 0.0);
        assert_eq!(r.x1, 612.0);
        assert_eq!(r.y1, 792.0);
    }

    #[test]
    fn test_rect_from_array_too_short() {
        let arr = vec![Object::Int(0), Object::Int(0)];
        assert!(Rect::from_array(&arr).is_err());
    }

    #[test]
    fn test_page_from_dict() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(612.0, 792.0)),
        );
        let page = Page::from_dict(dict, 0).unwrap();
        assert_eq!(page.number, 0);
        assert_eq!(page.media_box.width(), 612.0);
        assert_eq!(page.height(), 792.0);
        assert_eq!(page.rotation(), 0);
    }

    #[test]
    fn test_page_crop_box_fallback() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(100.0, 100.0)),
        );
        let page = Page::from_dict(dict, 0).unwrap();
        let crop = page.crop_box();
        assert_eq!(crop.x1, 100.0);
    }

    #[test]
    fn test_page_effective_box() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(100.0, 100.0)),
        );
        dict.insert(
            Name::new("TrimBox"),
            Object::Array(vec![
                Object::Int(10),
                Object::Int(10),
                Object::Real(90.0),
                Object::Real(90.0),
            ]),
        );
        let page = Page::from_dict(dict, 0).unwrap();
        let eff = page.effective_box();
        assert_eq!(eff.x0, 10.0);
        assert_eq!(eff.x1, 90.0);
    }

    #[test]
    fn test_page_contents_refs_single() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(100.0, 100.0)),
        );
        dict.insert(Name::new("Contents"), Object::Ref(ObjRef::new(5, 0)));
        let page = Page::from_dict(dict, 0).unwrap();
        let refs = page.contents_refs();
        assert_eq!(refs.len(), 1);
        assert_eq!(refs[0].num, 5);
    }

    #[test]
    fn test_page_contents_refs_array() {
        let mut dict = Dict::new();
        dict.insert(
            Name::new("MediaBox"),
            Object::Array(media_box_array(100.0, 100.0)),
        );
        dict.insert(
            Name::new("Contents"),
            Object::Array(vec![
                Object::Ref(ObjRef::new(10, 0)),
                Object::Ref(ObjRef::new(11, 0)),
            ]),
        );
        let page = Page::from_dict(dict, 0).unwrap();
        let refs = page.contents_refs();
        assert_eq!(refs.len(), 2);
        assert_eq!(refs[0].num, 10);
        assert_eq!(refs[1].num, 11);
    }
}
