//! Image listing and resolution inspection.
//!
//! Walks page /Resources /XObject dictionaries to find image objects
//! and reports their dimensions, colour space, compression, and DPI.
//!
//! Zero `unsafe` in this file.

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

// ── Types ───────────────────────────────────────────────────────────

#[derive(Debug, Clone, Serialize)]
pub struct ImageInfo {
    pub page: usize,
    pub name: String,
    pub width: u32,
    pub height: u32,
    pub colorspace: String,
    pub bpc: u8,
    pub filter: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct ImageResolutionInfo {
    pub page: usize,
    pub name: String,
    pub x_dpi: f64,
    pub y_dpi: f64,
}

// ── Helpers ─────────────────────────────────────────────────────────

fn find_xobject_refs(pdf: &[u8]) -> Vec<(String, i32)> {
    let text = String::from_utf8_lossy(pdf);
    let mut results = Vec::new();

    if let Some(idx) = text.find("/XObject") {
        let rest = &text[idx + 8..];
        let trimmed = rest.trim_start();
        if trimmed.starts_with("<<") {
            let close = trimmed.find(">>").unwrap_or(trimmed.len());
            let dict = &trimmed[2..close];
            let mut pos = 0;
            while let Some(slash) = dict[pos..].find('/') {
                let abs = pos + slash;
                let name_end = dict[abs + 1..]
                    .find(|c: char| c.is_whitespace() || c == '/')
                    .unwrap_or(dict.len() - abs - 1);
                let name = &dict[abs..abs + 1 + name_end];

                let after = dict[abs + 1 + name_end..].trim_start();
                let num_end = after
                    .find(|c: char| !c.is_ascii_digit())
                    .unwrap_or(after.len());
                if let Ok(n) = after[..num_end].parse::<i32>() {
                    results.push((name.to_string(), n));
                }
                pos = abs + 1 + name_end;
            }
        }
    }

    results
}

fn parse_image_object(pdf: &[u8], obj_num: i32) -> Option<(u32, u32, String, u8, String)> {
    let text = String::from_utf8_lossy(pdf);
    let needle = format!("{obj_num} 0 obj");
    let start = text.find(&needle)?;
    let dict_start = text[start..].find("<<")?;
    let dict_end = text[start + dict_start..].find(">>")?;
    let dict = &text[start + dict_start..start + dict_start + dict_end + 2];

    if !dict.contains("/Subtype/Image") && !dict.contains("/Subtype /Image") {
        return None;
    }

    let width = extract_dict_int(dict, "/Width").unwrap_or(0) as u32;
    let height = extract_dict_int(dict, "/Height").unwrap_or(0) as u32;
    let bpc = extract_dict_int(dict, "/BitsPerComponent").unwrap_or(8) as u8;

    let colorspace = if dict.contains("/DeviceRGB") {
        "DeviceRGB"
    } else if dict.contains("/DeviceCMYK") {
        "DeviceCMYK"
    } else if dict.contains("/DeviceGray") {
        "DeviceGray"
    } else {
        "Unknown"
    }
    .to_string();

    let filter = if dict.contains("/DCTDecode") {
        "DCTDecode"
    } else if dict.contains("/FlateDecode") {
        "FlateDecode"
    } else if dict.contains("/CCITTFaxDecode") {
        "CCITTFaxDecode"
    } else if dict.contains("/JBIG2Decode") {
        "JBIG2Decode"
    } else if dict.contains("/JPXDecode") {
        "JPXDecode"
    } else {
        "None"
    }
    .to_string();

    Some((width, height, colorspace, bpc, filter))
}

fn extract_dict_int(dict: &str, key: &str) -> Option<i64> {
    let idx = dict.find(key)?;
    let rest = &dict[idx + key.len()..];
    let trimmed = rest.trim_start();
    let end = trimmed
        .find(|c: char| !c.is_ascii_digit() && c != '-')
        .unwrap_or(trimmed.len());
    trimmed[..end].parse().ok()
}

// ── Public API ──────────────────────────────────────────────────────

/// List all images in pages of the given range.
pub fn list_images(doc: &CpdfDocument, range: &[usize]) -> Result<Vec<ImageInfo>> {
    let n = doc.page_count();
    let mut results = Vec::new();

    for &page_num in range {
        if page_num == 0 || page_num > n {
            continue;
        }
        let page_pdf = copy_page_to_bytes(&doc.to_bytes(), page_num - 1)
            .map_err(|e| CpdfError::Parse(format!("extract page {page_num}: {e}")))?;

        let xobjs = find_xobject_refs(&page_pdf);
        for (name, obj_num) in &xobjs {
            if let Some((w, h, cs, bpc, filter)) = parse_image_object(&page_pdf, *obj_num) {
                results.push(ImageInfo {
                    page: page_num,
                    name: name.clone(),
                    width: w,
                    height: h,
                    colorspace: cs,
                    bpc,
                    filter,
                });
            }
        }
    }

    Ok(results)
}

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

/// Return image resolution info for images below `min_dpi`.
pub fn image_resolution(
    doc: &CpdfDocument,
    range: &[usize],
    min_dpi: f64,
) -> Result<Vec<ImageResolutionInfo>> {
    let images = list_images(doc, range)?;
    let mut results = Vec::new();

    for img in &images {
        // Default assumption: image fills a standard letter page
        let x_dpi = if img.width > 0 {
            img.width as f64 / (612.0 / 72.0)
        } else {
            0.0
        };
        let y_dpi = if img.height > 0 {
            img.height as f64 / (792.0 / 72.0)
        } else {
            0.0
        };

        if x_dpi < min_dpi || y_dpi < min_dpi {
            results.push(ImageResolutionInfo {
                page: img.page,
                name: img.name.clone(),
                x_dpi,
                y_dpi,
            });
        }
    }

    Ok(results)
}

/// Extract images to files in `output_dir`. JPEG images get .jpg, others .png.
pub fn extract_images(_doc: &CpdfDocument, _range: &[usize], _output_dir: &str) -> Result<()> {
    Err(CpdfError::Unsupported(
        "extract_images: full image decoding not yet implemented".into(),
    ))
}

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

    fn blank() -> CpdfDocument {
        CpdfDocument::blank(612.0, 792.0, 1).unwrap()
    }

    #[test]
    fn blank_has_no_images() {
        let images = list_images(&blank(), &[1]).unwrap();
        assert!(images.is_empty());
    }

    #[test]
    fn list_images_json_empty() {
        let json = list_images_json(&blank(), &[1]).unwrap();
        assert_eq!(json.trim(), "[]");
    }

    #[test]
    fn image_resolution_empty() {
        let res = image_resolution(&blank(), &[1], 72.0).unwrap();
        assert!(res.is_empty());
    }
}
