//! Document I/O — load, save, merge, split, page range parsing.
//!
//! `CpdfDocument` wraps raw PDF bytes. All operations read/write bytes
//! directly using the existing enhanced page_copy and page_merge infrastructure.

use crate::cpdf::error::{CpdfError, Result};
use crate::enhanced::page_copy::copy_page_to_bytes;
use crate::enhanced::page_merge::{combine_page_pdfs_bytes, count_pages};

/// Core cpdf document. Holds the parsed PDF as raw bytes.
///
/// All cpdf operations take ownership of or borrow `CpdfDocument` values.
/// The FFI layer stores these in a `HandleStore<CpdfDocument>`.
#[derive(Clone)]
pub struct CpdfDocument {
    /// Raw PDF bytes — the source of truth for all operations.
    pub(crate) data: Vec<u8>,
}

impl CpdfDocument {
    /// Load a document from raw bytes.
    pub fn from_bytes(bytes: Vec<u8>) -> Result<Self> {
        if !bytes.starts_with(b"%PDF-") {
            return Err(CpdfError::Parse("not a PDF: missing %PDF- header".into()));
        }
        Ok(Self { data: bytes })
    }

    /// Load a document from a file path.
    pub fn from_file(path: &str) -> Result<Self> {
        let bytes = std::fs::read(path).map_err(CpdfError::Io)?;
        Self::from_bytes(bytes)
    }

    /// Serialize the document to bytes.
    pub fn to_bytes(&self) -> Vec<u8> {
        self.data.clone()
    }

    /// Write the document to a file.
    pub fn to_file(&self, path: &str) -> Result<()> {
        std::fs::write(path, &self.data).map_err(CpdfError::Io)
    }

    /// Number of pages in the document.
    pub fn page_count(&self) -> usize {
        count_pages(&self.data).unwrap_or(0)
    }

    /// PDF version string (e.g. "1.7").
    pub fn version(&self) -> String {
        // Header is "%PDF-X.Y\n", version starts at byte 5.
        if self.data.len() >= 8 {
            if let Ok(s) = std::str::from_utf8(&self.data[5..8]) {
                return s
                    .trim_end_matches(|c: char| !c.is_ascii_digit() && c != '.')
                    .to_owned();
            }
        }
        "1.4".into()
    }

    /// Create a blank document with `n` pages of the given size in points.
    pub fn blank(width: f64, height: f64, n: usize) -> Result<Self> {
        if n == 0 {
            return Err(CpdfError::InvalidArgument("page count must be > 0".into()));
        }
        let bytes = build_blank_pdf(width, height, n);
        Ok(Self { data: bytes })
    }
}

/// Build a minimal but valid PDF with `n` blank pages.
///
/// Object layout:
///   1: Catalog → Pages (obj 2)
///   2: Pages → Kids [3 … 2+n]
///   3..2+n: Page objects
fn build_blank_pdf(width: f64, height: f64, n: usize) -> Vec<u8> {
    let mut out: Vec<u8> = Vec::new();
    let mut offsets: Vec<usize> = Vec::new();

    // Header
    out.extend_from_slice(b"%PDF-1.4\n");

    // Obj 1: Catalog
    offsets.push(out.len());
    out.extend_from_slice(b"1 0 obj\n<</Type /Catalog /Pages 2 0 R>>\nendobj\n");

    // Obj 2: Pages
    let kids: String = (3..=2 + n)
        .map(|i| format!("{i} 0 R"))
        .collect::<Vec<_>>()
        .join(" ");
    offsets.push(out.len());
    out.extend_from_slice(
        format!("2 0 obj\n<</Type /Pages /Kids [{kids}] /Count {n}>>\nendobj\n").as_bytes(),
    );

    // Page objects
    for i in 3..=2 + n {
        offsets.push(out.len());
        out.extend_from_slice(
            format!(
                "{i} 0 obj\n<</Type /Page /Parent 2 0 R \
                 /MediaBox [0 0 {width} {height}]>>\nendobj\n"
            )
            .as_bytes(),
        );
    }

    // xref
    let xref_pos = out.len();
    let total = 2 + n + 1; // free + catalog + pages + n pages
    out.extend_from_slice(format!("xref\n0 {total}\n").as_bytes());
    out.extend_from_slice(b"0000000000 65535 f \n");
    for &off in &offsets {
        out.extend_from_slice(format!("{off:010} 00000 n \n").as_bytes());
    }

    // Trailer
    out.extend_from_slice(
        format!("trailer\n<</Size {total} /Root 1 0 R>>\nstartxref\n{xref_pos}\n%%EOF\n")
            .as_bytes(),
    );

    out
}

/// Merge multiple documents into one, pages in order.
pub fn merge(docs: Vec<CpdfDocument>) -> Result<CpdfDocument> {
    if docs.is_empty() {
        return Err(CpdfError::InvalidArgument(
            "merge requires at least one document".into(),
        ));
    }
    if docs.len() == 1 {
        return Ok(docs.into_iter().next().unwrap());
    }

    let byte_refs: Vec<&[u8]> = docs.iter().map(|d| d.data.as_slice()).collect();
    let merged = merge_pdf_bytes(&byte_refs)?;
    Ok(CpdfDocument { data: merged })
}

/// Split a document into multiple documents by page ranges.
///
/// `ranges` is a list of 1-indexed page number lists. Each sublist produces
/// one output document containing exactly those pages in the given order.
pub fn split_by_range(doc: &CpdfDocument, ranges: &[Vec<usize>]) -> Result<Vec<CpdfDocument>> {
    ranges
        .iter()
        .map(|range| {
            let bytes = extract_pages_from_bytes(&doc.data, range)?;
            Ok(CpdfDocument { data: bytes })
        })
        .collect()
}

/// Split into one document per page.
pub fn split_every_page(doc: &CpdfDocument) -> Result<Vec<CpdfDocument>> {
    let n = doc.page_count();
    if n == 0 {
        return Err(CpdfError::InvalidArgument("document has no pages".into()));
    }
    let ranges: Vec<Vec<usize>> = (1..=n).map(|i| vec![i]).collect();
    split_by_range(doc, &ranges)
}

/// Parse a cpdf page range specification string into 1-indexed page numbers.
///
/// Supports: `all`, `odd`, `even`, `end`, `1-3`, `1,3,5`, `~1` (from end).
pub fn parse_page_range(spec: &str, page_count: usize) -> Result<Vec<usize>> {
    match spec.trim() {
        "all" => return Ok((1..=page_count).collect()),
        "odd" => return Ok((1..=page_count).filter(|n| n % 2 == 1).collect()),
        "even" => return Ok((1..=page_count).filter(|n| n % 2 == 0).collect()),
        "end" => {
            return if page_count == 0 {
                Err(CpdfError::InvalidRange("document has no pages".into()))
            } else {
                Ok(vec![page_count])
            };
        }
        _ => {}
    }

    let mut result = Vec::new();
    for part in spec.split(',') {
        let part = part.trim();
        if let Some(stripped) = part.strip_prefix('~') {
            let n: usize = stripped
                .parse()
                .map_err(|_| CpdfError::InvalidRange(format!("invalid ~n: {stripped}")))?;
            if n == 0 || n > page_count {
                return Err(CpdfError::InvalidRange(format!(
                    "~{n} out of range (1..={page_count})"
                )));
            }
            result.push(page_count + 1 - n);
        } else if let Some((s, e)) = part.split_once('-') {
            let start = resolve_page_ref(s, page_count)?;
            let end = resolve_page_ref(e, page_count)?;
            if start <= end {
                result.extend(start..=end);
            } else {
                result.extend((end..=start).rev());
            }
        } else {
            result.push(resolve_page_ref(part, page_count)?);
        }
    }
    Ok(result)
}

fn resolve_page_ref(s: &str, page_count: usize) -> Result<usize> {
    if s == "end" {
        return Ok(page_count);
    }
    let n: usize = s
        .parse()
        .map_err(|_| CpdfError::InvalidRange(format!("invalid page ref: '{s}'")))?;
    if n == 0 || n > page_count {
        Err(CpdfError::InvalidRange(format!(
            "page {n} out of range (1..={page_count})"
        )))
    } else {
        Ok(n)
    }
}

/// Merge multiple PDFs (each potentially multi-page) into one document.
///
/// Strategy: extract every page of every source PDF into individual
/// single-page PDFs using the existing `copy_page_to_bytes` infrastructure,
/// then stitch them together with `combine_page_pdfs_bytes`.
fn merge_pdf_bytes(docs: &[&[u8]]) -> Result<Vec<u8>> {
    let mut single_pages: Vec<Vec<u8>> = Vec::new();

    for &doc_bytes in docs {
        let n = count_pages(doc_bytes)
            .map_err(|e| CpdfError::Parse(format!("could not count pages: {e}")))?;
        for page_idx in 0..n {
            let page_pdf = copy_page_to_bytes(doc_bytes, page_idx)
                .map_err(|e| CpdfError::Parse(format!("could not extract page {page_idx}: {e}")))?;
            single_pages.push(page_pdf);
        }
    }

    if single_pages.is_empty() {
        return Err(CpdfError::InvalidArgument(
            "no pages found in source documents".into(),
        ));
    }

    combine_page_pdfs_bytes(&single_pages)
        .map_err(|e| CpdfError::Parse(format!("could not combine pages: {e}")))
}

/// Extract the pages listed in `page_nums` (1-indexed) from `data` into a new PDF.
fn extract_pages_from_bytes(data: &[u8], page_nums: &[usize]) -> Result<Vec<u8>> {
    if page_nums.is_empty() {
        return Err(CpdfError::InvalidArgument("page range is empty".into()));
    }

    let single_pages: Result<Vec<Vec<u8>>> = page_nums
        .iter()
        .map(|&p| {
            // copy_page_to_bytes uses 0-based indexing
            copy_page_to_bytes(data, p - 1)
                .map_err(|e| CpdfError::Parse(format!("could not extract page {p}: {e}")))
        })
        .collect();

    let single_pages = single_pages?;

    if single_pages.len() == 1 {
        return Ok(single_pages.into_iter().next().unwrap());
    }

    combine_page_pdfs_bytes(&single_pages)
        .map_err(|e| CpdfError::Parse(format!("could not combine pages: {e}")))
}
