//! Fonts — list, inspect, remove PDF fonts.
//!
//! Zero `unsafe` in this file.

use crate::cpdf::bookmarks::rebuild_xref_data;
use crate::cpdf::document::CpdfDocument;
use crate::cpdf::error::{CpdfError, Result};
use crate::enhanced::page_copy::copy_page_to_bytes;
use serde::Serialize;

#[derive(Debug, Clone, Serialize)]
pub struct FontInfo {
    pub page: usize,
    pub name: String,
    pub subtype: String,
    pub basefont: String,
    pub encoding: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct MissingFontInfo {
    pub page: usize,
    pub name: String,
    pub subtype: String,
}

const STANDARD_14: &[&str] = &[
    "Courier",
    "Courier-Bold",
    "Courier-Oblique",
    "Courier-BoldOblique",
    "Helvetica",
    "Helvetica-Bold",
    "Helvetica-Oblique",
    "Helvetica-BoldOblique",
    "Times-Roman",
    "Times-Bold",
    "Times-Italic",
    "Times-BoldItalic",
    "Symbol",
    "ZapfDingbats",
];

fn extract_dict_value<'a>(dict: &'a str, key: &str) -> Option<&'a str> {
    let idx = dict.find(key)?;
    let rest = dict[idx + key.len()..].trim_start();
    if rest.starts_with('/') {
        let end = rest[1..]
            .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
            .unwrap_or(rest.len() - 1);
        Some(&rest[1..1 + end])
    } else if rest.starts_with('(') {
        let close = rest[1..].find(')')?;
        Some(&rest[1..1 + close])
    } else {
        let end = rest
            .find(|c: char| c == '/' || c == '>')
            .unwrap_or(rest.len());
        Some(rest[..end].trim())
    }
}

fn parse_fonts_from_page(page_pdf: &[u8], page_num: usize) -> Vec<FontInfo> {
    let text = String::from_utf8_lossy(page_pdf);
    let mut fonts = Vec::new();
    let mut pos = 0;

    while let Some(idx) = text[pos..]
        .find("/Type /Font")
        .or_else(|| text[pos..].find("/Type/Font"))
    {
        let abs = pos + idx;
        let dict_start = text[..abs].rfind("<<").unwrap_or(abs);
        let dict_end = text[abs..]
            .find(">>")
            .map(|i| abs + i + 2)
            .unwrap_or(text.len());
        let dict = &text[dict_start..dict_end];

        let subtype = extract_dict_value(dict, "/Subtype")
            .unwrap_or("")
            .to_string();
        let basefont = extract_dict_value(dict, "/BaseFont")
            .unwrap_or("")
            .to_string();
        let encoding = extract_dict_value(dict, "/Encoding")
            .unwrap_or("")
            .to_string();

        let name_search = &text[..dict_start];
        let name = if let Some(slash) = name_search.rfind('/') {
            let after = &name_search[slash + 1..];
            let end = after
                .find(|c: char| c.is_whitespace() || c == '>' || c == '/')
                .unwrap_or(after.len());
            after[..end].to_string()
        } else {
            basefont.clone()
        };

        fonts.push(FontInfo {
            page: page_num,
            name,
            subtype,
            basefont,
            encoding,
        });

        pos = dict_end;
    }

    fonts
}

/// List all fonts used on the specified pages.
pub fn list_fonts(doc: &CpdfDocument, range: &[usize]) -> Result<Vec<FontInfo>> {
    let bytes = doc.to_bytes();
    let page_count = doc.page_count();
    let mut all_fonts = Vec::new();

    for &page in range {
        if page < 1 || page > page_count {
            continue;
        }
        match copy_page_to_bytes(&bytes, page - 1) {
            Ok(page_pdf) => {
                let fonts = parse_fonts_from_page(&page_pdf, page);
                all_fonts.extend(fonts);
            }
            Err(_) => continue,
        }
    }

    Ok(all_fonts)
}

/// List fonts as JSON.
pub fn list_fonts_json(doc: &CpdfDocument, range: &[usize]) -> Result<String> {
    let fonts = list_fonts(doc, range)?;
    serde_json::to_string_pretty(&fonts).map_err(|e| CpdfError::Parse(e.to_string()))
}

/// Remove embedded font data (keep references for system fallback).
pub fn remove_fonts(doc: CpdfDocument) -> Result<CpdfDocument> {
    let bytes = doc.to_bytes();
    let mut text = String::from_utf8_lossy(&bytes).to_string();

    // Remove /FontFile, /FontFile2, /FontFile3 references from font descriptors
    for key in ["/FontFile3", "/FontFile2", "/FontFile"] {
        while let Some(idx) = text.find(key) {
            let rest = &text[idx + key.len()..];
            let trimmed = rest.trim_start();
            let end = trimmed
                .find(|c: char| c == '/' || c == '>')
                .unwrap_or(trimmed.len());
            let offset = rest.len() - trimmed.len();
            let remove_end = idx + key.len() + offset + end;
            text = format!("{}{}", &text[..idx], &text[remove_end..]);
        }
    }

    let final_bytes = rebuild_xref_data(text.as_bytes());
    CpdfDocument::from_bytes(final_bytes)
}

/// Identify fonts that are not among the standard 14 and lack embedded data.
pub fn missing_fonts(doc: &CpdfDocument, range: &[usize]) -> Result<Vec<MissingFontInfo>> {
    let fonts = list_fonts(doc, range)?;
    let bytes = doc.to_bytes();
    let text = String::from_utf8_lossy(&bytes);

    let has_embedded =
        text.contains("/FontFile") || text.contains("/FontFile2") || text.contains("/FontFile3");

    let mut missing = Vec::new();
    for f in &fonts {
        let is_standard = STANDARD_14.iter().any(|s| f.basefont == *s);
        if !is_standard && !has_embedded {
            missing.push(MissingFontInfo {
                page: f.page,
                name: f.basefont.clone(),
                subtype: f.subtype.clone(),
            });
        }
    }

    Ok(missing)
}
