//! PDF document implementation

use crate::fitz::error::{Error, Result};
use std::collections::HashMap;

use super::object::{Dict, Name, Object};
use super::parser::Parser;
use super::xref::{XrefEntryType, XrefTable};

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PdfVersion {
    V1_0,
    V1_1,
    V1_2,
    V1_3,
    V1_4,
    V1_5,
    V1_6,
    V1_7,
    V2_0,
}

impl PdfVersion {
    pub fn as_str(&self) -> &'static str {
        match self {
            PdfVersion::V1_0 => "1.0",
            PdfVersion::V1_1 => "1.1",
            PdfVersion::V1_2 => "1.2",
            PdfVersion::V1_3 => "1.3",
            PdfVersion::V1_4 => "1.4",
            PdfVersion::V1_5 => "1.5",
            PdfVersion::V1_6 => "1.6",
            PdfVersion::V1_7 => "1.7",
            PdfVersion::V2_0 => "2.0",
        }
    }

    pub fn from_str(s: &str) -> Option<Self> {
        match s.trim() {
            "1.0" => Some(PdfVersion::V1_0),
            "1.1" => Some(PdfVersion::V1_1),
            "1.2" => Some(PdfVersion::V1_2),
            "1.3" => Some(PdfVersion::V1_3),
            "1.4" => Some(PdfVersion::V1_4),
            "1.5" => Some(PdfVersion::V1_5),
            "1.6" => Some(PdfVersion::V1_6),
            "1.7" => Some(PdfVersion::V1_7),
            "2.0" => Some(PdfVersion::V2_0),
            _ => None,
        }
    }
}

pub struct PdfDocument {
    pub objects: HashMap<(i32, i32), Object>,
    pub xref: XrefTable,
    pub trailer: Dict,
    pub version: String,
}

impl PdfDocument {
    pub fn new() -> Self {
        Self {
            objects: HashMap::new(),
            xref: XrefTable::new(),
            trailer: Dict::new(),
            version: "1.7".to_string(),
        }
    }

    pub fn from_bytes(data: &[u8]) -> Result<Self> {
        let version = parse_version(data)?;
        let startxref = find_startxref(data)?;
        let mut parser = Parser::new(&data[startxref..]);
        let xref = parser.parse_xref_table()?;
        let trailer = parser.parse_dict()?;

        let mut objects: HashMap<(i32, i32), Object> = HashMap::new();
        for num in xref.object_numbers() {
            if let Some(entry) = xref.get(num) {
                if entry.entry_type != XrefEntryType::InUse {
                    continue;
                }
                let offset = entry.offset as usize;
                if offset >= data.len() {
                    continue;
                }
                let mut obj_parser = Parser::new(&data[offset..]);
                if let Ok((n, g, obj)) = obj_parser.parse_indirect_object() {
                    objects.insert((n, g), obj);
                }
            }
        }

        Ok(Self {
            objects,
            xref,
            trailer,
            version,
        })
    }

    pub fn resolve(&self, obj: &Object) -> Object {
        match obj {
            Object::Ref(r) => self
                .get_object(r.num, r.generation)
                .cloned()
                .unwrap_or(Object::Null),
            Object::Array(arr) => {
                let resolved: Vec<Object> = arr.iter().map(|o| self.resolve(o)).collect();
                Object::Array(resolved)
            }
            Object::Dict(dict) => {
                let resolved: Dict = dict
                    .iter()
                    .map(|(k, v)| (k.clone(), self.resolve(v)))
                    .collect();
                Object::Dict(resolved)
            }
            Object::Stream { dict, data } => {
                let resolved: Dict = dict
                    .iter()
                    .map(|(k, v)| (k.clone(), self.resolve(v)))
                    .collect();
                Object::Stream {
                    dict: resolved,
                    data: data.clone(),
                }
            }
            other => other.clone(),
        }
    }

    pub fn get_object(&self, num: i32, generation: i32) -> Option<&Object> {
        self.objects.get(&(num, generation))
    }

    fn get_dict(&self, obj: &Object) -> Option<&Dict> {
        match obj {
            Object::Ref(r) => self
                .get_object(r.num, r.generation)
                .and_then(|o| o.as_dict()),
            _ => None,
        }
    }

    pub fn catalog(&self) -> Result<&Dict> {
        let root = self
            .trailer
            .get(&Name::new("Root"))
            .ok_or_else(|| Error::Generic("No /Root in trailer".into()))?;
        self.get_dict(root)
            .ok_or_else(|| Error::Generic("/Root is not a dictionary".into()))
    }

    pub fn page_count(&self) -> Result<usize> {
        let pages = self.page_tree_root()?;
        let count_obj = pages
            .get(&Name::new("Count"))
            .ok_or_else(|| Error::Generic("No /Count in Pages".into()))?;
        let resolved = self.resolve(count_obj);
        let count = resolved
            .as_int()
            .ok_or_else(|| Error::Generic("/Count is not an integer".into()))?;
        Ok(count as usize)
    }

    pub fn page_tree_root(&self) -> Result<&Dict> {
        let catalog = self.catalog()?;
        let pages = catalog
            .get(&Name::new("Pages"))
            .ok_or_else(|| Error::Generic("No /Pages in catalog".into()))?;
        self.get_dict(pages)
            .ok_or_else(|| Error::Generic("/Pages is not a dictionary".into()))
    }

    pub fn info(&self) -> Option<&Dict> {
        let info_obj = self.trailer.get(&Name::new("Info"))?;
        self.get_dict(info_obj)
    }
}

impl Default for PdfDocument {
    fn default() -> Self {
        Self::new()
    }
}

fn parse_version(data: &[u8]) -> Result<String> {
    let header = b"%PDF-";
    if data.len() < header.len() + 3 {
        return Err(Error::Format("PDF header too short".into()));
    }
    if !data.starts_with(header) {
        return Err(Error::Format("Invalid PDF header".into()));
    }
    let mut end = header.len();
    while end < data.len() && data[end] != b'\n' && data[end] != b'\r' {
        end += 1;
    }
    let version = std::str::from_utf8(&data[header.len()..end])
        .map_err(|_| Error::Format("Invalid PDF version".into()))?
        .trim()
        .to_string();
    if version.is_empty() {
        return Err(Error::Format("PDF version empty".into()));
    }
    Ok(version)
}
fn find_startxref(data: &[u8]) -> Result<usize> {
    let marker = b"startxref";
    let search_start = data.len().saturating_sub(2048);
    let search_region = &data[search_start..];
    let pos = search_region
        .windows(marker.len())
        .position(|w| w == marker)
        .map(|p| search_start + p)
        .ok_or_else(|| Error::Format("startxref not found".into()))?;

    let after = pos + marker.len();
    let mut i = after;
    while i < data.len()
        && (data[i] == b' ' || data[i] == b'\t' || data[i] == b'\n' || data[i] == b'\r')
    {
        i += 1;
    }
    let num_start = i;
    while i < data.len() && data[i].is_ascii_digit() {
        i += 1;
    }
    let num_str = std::str::from_utf8(&data[num_start..i])
        .map_err(|_| Error::Format("Invalid startxref offset".into()))?;
    let offset: usize = num_str
        .parse()
        .map_err(|_| Error::Format("Invalid startxref offset".into()))?;
    let offset = offset.min(data.len());
    Ok(offset)
}
#[cfg(test)]
mod tests {
    use super::*;
    use crate::pdf::object::ObjRef;

    #[test]
    fn test_pdf_document_get_object() {
        let mut doc = PdfDocument::new();
        doc.objects.insert((5, 0), Object::Int(42));
        assert_eq!(doc.get_object(5, 0).unwrap().as_int(), Some(42));
    }
    #[test]
    fn test_pdf_version() {
        assert_eq!(PdfVersion::V1_7.as_str(), "1.7");
        assert_eq!(PdfVersion::from_str("1.7"), Some(PdfVersion::V1_7));
        assert_eq!(PdfVersion::from_str("9.9"), None);
    }
    #[test]
    fn test_pdf_document_new() {
        let doc = PdfDocument::new();
        assert!(doc.objects.is_empty());
        assert!(doc.xref.is_empty());
        assert!(doc.trailer.is_empty());
        assert_eq!(doc.version, "1.7");
    }

    #[test]
    fn test_pdf_document_resolve_ref() {
        let mut doc = PdfDocument::new();
        doc.objects.insert(
            (1, 0),
            Object::Dict(
                [(Name::new("Type"), Object::Name(Name::new("Catalog")))]
                    .into_iter()
                    .collect(),
            ),
        );
        let resolved = doc.resolve(&Object::Ref(ObjRef::new(1, 0)));
        let dict = resolved.as_dict().unwrap();
        assert_eq!(
            dict.get(&Name::new("Type"))
                .unwrap()
                .as_name()
                .unwrap()
                .as_str(),
            "Catalog"
        );
    }

    #[test]
    fn test_pdf_document_from_bytes() {
        let pdf = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
2 0 obj << /Type /Pages /Count 1 /Kids [3 0 R] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R >> endobj
xref
0 4
0000000000 65535 f 
0000000009 00000 n 
0000000058 00000 n 
0000000115 00000 n 
trailer << /Size 4 /Root 1 0 R /Info 4 0 R >>
startxref
162
%%EOF";
        let doc = PdfDocument::from_bytes(pdf).unwrap();
        assert_eq!(doc.version, "1.7");
        assert_eq!(doc.objects.len(), 3);
        assert_eq!(doc.page_count().unwrap(), 1);
        assert!(doc.catalog().is_ok() && doc.page_tree_root().is_ok());
        assert!(doc.get_object(1, 0).is_some() && doc.info().is_none());
    }
}
