//! PDF Linearization (Web Optimization)
//!
//! This module provides support for reading and writing linearized PDFs,
//! which are optimized for page-at-a-time downloading over the web.
//!
//! # Linearization Overview
//!
//! A linearized PDF has the following structure:
//! 1. Header and linearization dictionary
//! 2. First page cross-reference table and trailer
//! 3. Document catalog, first page, and related objects
//! 4. Hint stream
//! 5. Remaining pages
//! 6. Remaining objects
//! 7. Final cross-reference table and trailer

use super::document::Document;
use super::error::{QpdfError, Result};
use crate::pdf::object::{Dict, Name, ObjRef, Object};
use std::collections::HashMap;
use std::io::{Seek, Write};

/// Linearization parameters from the linearization dictionary
#[derive(Debug, Clone)]
pub struct LinearizationParams {
    /// File length
    pub file_length: u64,
    /// Primary hint stream offset
    pub hint_offset: u64,
    /// Primary hint stream length
    pub hint_length: u64,
    /// Overflow hint stream offset (optional)
    pub overflow_hint_offset: Option<u64>,
    /// Overflow hint stream length (optional)
    pub overflow_hint_length: Option<u64>,
    /// First page object number
    pub first_page_obj: u32,
    /// End of first page
    pub first_page_end: u64,
    /// Number of pages
    pub page_count: u32,
    /// Offset of main XRef table
    pub main_xref_offset: u64,
    /// First page number (usually 0)
    pub first_page_num: u32,
}

/// Page offset hint table entry
#[derive(Debug, Clone, Default)]
pub struct PageOffsetHintEntry {
    /// Number of objects on the page minus minimum
    pub objects_delta: u32,
    /// Page length minus minimum
    pub length_delta: u64,
    /// Number of shared object references
    pub shared_count: u32,
    /// Shared object identifiers
    pub shared_identifiers: Vec<u32>,
    /// Shared object numerators
    pub shared_numerators: Vec<u32>,
    /// Content stream offset delta
    pub content_offset_delta: u64,
    /// Content stream length delta
    pub content_length_delta: u64,
}

/// Page offset hint table header
#[derive(Debug, Clone)]
pub struct PageOffsetHintHeader {
    /// Minimum object count per page
    pub min_objects: u32,
    /// First page object location
    pub first_page_loc: u64,
    /// Bits for object count delta
    pub bits_objects: u32,
    /// Minimum page length
    pub min_length: u64,
    /// Bits for length delta
    pub bits_length: u32,
    /// Minimum content stream offset
    pub min_content_offset: u64,
    /// Bits for content offset delta
    pub bits_content_offset: u32,
    /// Minimum content stream length
    pub min_content_length: u64,
    /// Bits for content length delta
    pub bits_content_length: u32,
    /// Bits for shared reference count
    pub bits_shared: u32,
    /// Bits for shared group identifier
    pub bits_shared_group: u32,
    /// Bits for shared numerator
    pub bits_shared_numerator: u32,
    /// Shared group denominator
    pub shared_denominator: u32,
}

/// Shared object hint table entry
#[derive(Debug, Clone)]
pub struct SharedObjectHintEntry {
    /// Object group length delta
    pub length_delta: u64,
    /// Signature flag
    pub signature: bool,
    /// Number of objects in group minus minimum
    pub objects_delta: u32,
}

/// Shared object hint table header
#[derive(Debug, Clone)]
pub struct SharedObjectHintHeader {
    /// Object number of first shared object
    pub first_object: u32,
    /// Location of first shared object
    pub first_object_loc: u64,
    /// Number of shared object entries
    pub entry_count: u32,
    /// Number of first page shared objects
    pub first_page_count: u32,
    /// Minimum group length
    pub min_length: u64,
    /// Bits for group length delta
    pub bits_length: u32,
}

/// Linearization validation result
#[derive(Debug, Clone)]
pub struct LinearizationCheckResult {
    /// Whether the file is linearized
    pub is_linearized: bool,
    /// Errors found during validation
    pub errors: Vec<String>,
    /// Warnings found during validation
    pub warnings: Vec<String>,
}

impl LinearizationCheckResult {
    /// Create a new result indicating not linearized
    pub fn not_linearized() -> Self {
        Self {
            is_linearized: false,
            errors: Vec::new(),
            warnings: Vec::new(),
        }
    }

    /// Create a new result indicating linearized with no issues
    pub fn valid() -> Self {
        Self {
            is_linearized: true,
            errors: Vec::new(),
            warnings: Vec::new(),
        }
    }

    /// Check if the linearization is valid (linearized with no errors)
    pub fn is_valid(&self) -> bool {
        self.is_linearized && self.errors.is_empty()
    }

    /// Add an error
    pub fn add_error(&mut self, error: String) {
        self.errors.push(error);
    }

    /// Add a warning
    pub fn add_warning(&mut self, warning: String) {
        self.warnings.push(warning);
    }
}

/// Check if a PDF file is linearized
///
/// This checks if the file starts with a linearization parameter dictionary.
pub fn is_linearized(data: &[u8]) -> bool {
    // Look for linearization dictionary near the start of the file
    // It should be within the first few kilobytes
    let search_range = data.len().min(4096);
    let search_data = &data[..search_range];

    // Look for "/Linearized" in the search range
    let linearized_marker = b"/Linearized";
    search_data
        .windows(linearized_marker.len())
        .any(|window| window == linearized_marker)
}

/// Check linearization and return detailed results
pub fn check_linearization(data: &[u8]) -> Result<LinearizationCheckResult> {
    if !is_linearized(data) {
        return Ok(LinearizationCheckResult::not_linearized());
    }

    let mut result = LinearizationCheckResult::valid();

    // Parse linearization dictionary
    match parse_linearization_dict(data) {
        Ok(params) => {
            // Validate file length
            if params.file_length != data.len() as u64 {
                result.add_warning(format!(
                    "File length mismatch: linearization dict says {}, actual is {}",
                    params.file_length,
                    data.len()
                ));
            }

            // Validate hint stream
            if params.hint_offset >= data.len() as u64 {
                result.add_error("Hint stream offset beyond file end".to_string());
            }

            // Additional validation would go here...
        }
        Err(e) => {
            result.add_error(format!("Failed to parse linearization dictionary: {}", e));
        }
    }

    Ok(result)
}

/// Bit reader for reading N-bit integers from a byte stream (MSB first)
struct BitReader<'a> {
    data: &'a [u8],
    byte_pos: usize,
    bit_pos: u8,
}

impl<'a> BitReader<'a> {
    fn new(data: &'a [u8]) -> Self {
        Self {
            data,
            byte_pos: 0,
            bit_pos: 0,
        }
    }

    fn read_bits(&mut self, n: u32) -> Result<u64> {
        if n == 0 {
            return Ok(0);
        }
        if n > 64 {
            return Err(QpdfError::Linearization(
                "Cannot read more than 64 bits at once".to_string(),
            ));
        }
        let mut value: u64 = 0;
        let mut bits_remaining = n;
        while bits_remaining > 0 {
            if self.byte_pos >= self.data.len() {
                return Err(QpdfError::Linearization(
                    "Unexpected end of hint stream data".to_string(),
                ));
            }
            let bits_in_byte = 8 - self.bit_pos;
            let bits_to_read = bits_remaining.min(bits_in_byte as u32);
            let shift = bits_in_byte - bits_to_read as u8;
            let mask = ((1u64 << bits_to_read) - 1) << shift;
            let byte_val = self.data[self.byte_pos] as u64;
            value = value.checked_shl(bits_to_read).ok_or_else(|| {
                QpdfError::Linearization("Bit shift overflow in hint stream".to_string())
            })? | ((byte_val & mask) >> shift);
            bits_remaining -= bits_to_read;
            self.bit_pos += bits_to_read as u8;
            if self.bit_pos >= 8 {
                self.bit_pos = 0;
                self.byte_pos += 1;
            }
        }
        Ok(value)
    }

    fn align_to_byte(&mut self) {
        if self.bit_pos != 0 {
            self.bit_pos = 0;
            self.byte_pos += 1;
        }
    }
}

/// Parse page offset hint table from hint stream data
pub fn parse_page_offset_hints(
    data: &[u8],
    page_count: u32,
) -> Result<(PageOffsetHintHeader, Vec<PageOffsetHintEntry>)> {
    let mut reader = BitReader::new(data);
    let min_objects = reader.read_bits(32)? as u32;
    let first_page_loc = reader.read_bits(32)? as u64;
    let bits_objects = reader.read_bits(16)? as u32;
    let min_length = reader.read_bits(32)? as u64;
    let bits_length = reader.read_bits(16)? as u32;
    let min_content_offset = reader.read_bits(32)? as u64;
    let bits_content_offset = reader.read_bits(16)? as u32;
    let min_content_length = reader.read_bits(32)? as u64;
    let bits_content_length = reader.read_bits(16)? as u32;
    let bits_shared = reader.read_bits(16)? as u32;
    let bits_shared_group = reader.read_bits(16)? as u32;
    let bits_shared_numerator = reader.read_bits(16)? as u32;
    let shared_denominator = reader.read_bits(16)? as u32;

    let header = PageOffsetHintHeader {
        min_objects,
        first_page_loc,
        bits_objects,
        min_length,
        bits_length,
        min_content_offset,
        bits_content_offset,
        min_content_length,
        bits_content_length,
        bits_shared,
        bits_shared_group,
        bits_shared_numerator,
        shared_denominator,
    };

    let mut entries = Vec::with_capacity(page_count.saturating_sub(1) as usize);
    for _ in 0..page_count.saturating_sub(1) {
        let objects_delta = reader.read_bits(bits_objects)? as u32;
        let length_delta = reader.read_bits(bits_length)? as u64;
        let shared_count = reader.read_bits(bits_shared)? as u32;
        let mut shared_identifiers = Vec::with_capacity(shared_count as usize);
        let mut shared_numerators = Vec::with_capacity(shared_count as usize);
        for _ in 0..shared_count {
            shared_identifiers.push(reader.read_bits(bits_shared_group)? as u32);
            shared_numerators.push(reader.read_bits(bits_shared_numerator)? as u32);
        }
        let content_offset_delta = reader.read_bits(bits_content_offset)? as u64;
        let content_length_delta = reader.read_bits(bits_content_length)? as u64;

        entries.push(PageOffsetHintEntry {
            objects_delta,
            length_delta,
            shared_count,
            shared_identifiers,
            shared_numerators,
            content_offset_delta,
            content_length_delta,
        });
    }

    Ok((header, entries))
}

/// Parse shared object hint table from hint stream data
pub fn parse_shared_object_hints(
    data: &[u8],
) -> Result<(SharedObjectHintHeader, Vec<SharedObjectHintEntry>)> {
    let mut reader = BitReader::new(data);
    let first_object = reader.read_bits(32)? as u32;
    let first_object_loc = reader.read_bits(32)? as u64;
    let entry_count = reader.read_bits(32)? as u32;
    let first_page_count = reader.read_bits(32)? as u32;
    let min_length = reader.read_bits(32)? as u64;
    let bits_length = reader.read_bits(16)? as u32;

    let header = SharedObjectHintHeader {
        first_object,
        first_object_loc,
        entry_count,
        first_page_count,
        min_length,
        bits_length,
    };

    let mut entries = Vec::with_capacity(entry_count as usize);
    for _ in 0..entry_count {
        let length_delta = reader.read_bits(bits_length)? as u64;
        let signature = reader.read_bits(1)? != 0;
        let objects_delta = reader.read_bits(16)? as u32;
        entries.push(SharedObjectHintEntry {
            length_delta,
            signature,
            objects_delta,
        });
    }

    Ok((header, entries))
}

/// Parse linearization dictionary from PDF data
fn parse_linearization_dict(data: &[u8]) -> Result<LinearizationParams> {
    // Find the linearization dictionary
    let linearized_marker = b"/Linearized";
    let start = data
        .windows(linearized_marker.len())
        .position(|window| window == linearized_marker)
        .ok_or_else(|| QpdfError::Linearization("Linearization marker not found".to_string()))?;

    // Find dictionary boundaries
    let dict_start = data[..start]
        .windows(2)
        .rposition(|window| window == b"<<")
        .ok_or_else(|| {
            QpdfError::Linearization("Linearization dictionary start not found".to_string())
        })?;

    let dict_end = data[start..]
        .windows(2)
        .position(|window| window == b">>")
        .map(|pos| start + pos + 2)
        .ok_or_else(|| {
            QpdfError::Linearization("Linearization dictionary end not found".to_string())
        })?;

    let dict_data = &data[dict_start..dict_end];
    let dict_str = std::str::from_utf8(dict_data)
        .map_err(|_| QpdfError::Linearization("Invalid UTF-8 in linearization dict".to_string()))?;

    // Parse required values (match full PDF names, e.g. /L not /Linearized)
    let parse_value = |key: &str| -> Result<u64> {
        let key_pattern = format!("/{} ", key.trim_start_matches('/'));
        let key_pos = dict_str.find(&key_pattern).ok_or_else(|| {
            QpdfError::Linearization(format!("Missing {} in linearization dict", key))
        })?;

        let value_start = key_pos + key_pattern.len();
        let value_str = dict_str[value_start..]
            .trim_start()
            .split_whitespace()
            .next()
            .ok_or_else(|| QpdfError::Linearization(format!("Invalid {} value", key)))?;

        value_str
            .parse()
            .map_err(|_| QpdfError::Linearization(format!("Invalid {} value: {}", key, value_str)))
    };

    let parse_h_array = |s: &str| -> Result<(u64, u64, Option<u64>, Option<u64>)> {
        let key_pos = s.find("/H").ok_or_else(|| {
            QpdfError::Linearization("Missing /H in linearization dict".to_string())
        })?;
        let after_key = &s[key_pos + 2..];
        let bracket = after_key
            .find('[')
            .ok_or_else(|| QpdfError::Linearization("/H value is not an array".to_string()))?;
        let arr_content = &after_key[bracket + 1..];
        let end_bracket = arr_content
            .find(']')
            .ok_or_else(|| QpdfError::Linearization("/H array not properly closed".to_string()))?;
        let nums: Vec<&str> = arr_content[..end_bracket]
            .split_whitespace()
            .filter(|x| !x.is_empty())
            .collect();
        if nums.len() >= 2 {
            let offset: u64 = nums[0]
                .parse()
                .map_err(|_| QpdfError::Linearization("Invalid /H offset".to_string()))?;
            let length: u64 = nums[1]
                .parse()
                .map_err(|_| QpdfError::Linearization("Invalid /H length".to_string()))?;
            let overflow_offset = nums.get(2).and_then(|n| n.parse().ok());
            let overflow_length = nums.get(3).and_then(|n| n.parse().ok());
            Ok((offset, length, overflow_offset, overflow_length))
        } else {
            Err(QpdfError::Linearization(
                "/H array must contain at least 2 values (offset, length)".to_string(),
            ))
        }
    };

    let file_length = parse_value("/L")?;
    let (hint_offset, hint_length, overflow_hint_offset, overflow_hint_length) =
        parse_h_array(dict_str)?;
    let first_page_obj = parse_value("/O")? as u32;
    let first_page_end = parse_value("/E")?;
    let page_count = parse_value("/N")? as u32;
    let main_xref_offset = parse_value("/T")?;

    Ok(LinearizationParams {
        file_length,
        hint_offset,
        hint_length,
        overflow_hint_offset,
        overflow_hint_length,
        first_page_obj,
        first_page_end,
        page_count,
        main_xref_offset,
        first_page_num: 0,
    })
}

/// Configuration for creating a linearized PDF
#[derive(Debug, Clone)]
pub struct LinearizationConfig {
    /// Whether to include outline (bookmark) data in the first page section
    pub include_outline: bool,
    /// Whether to include thumbnail images in hint tables
    pub include_thumbnails: bool,
}

impl Default for LinearizationConfig {
    fn default() -> Self {
        Self {
            include_outline: true,
            include_thumbnails: false,
        }
    }
}

/// Write a linearized PDF from a document (basic implementation)
pub fn write_linearized(doc: &mut Document, config: &LinearizationConfig) -> Result<Vec<u8>> {
    use super::writer::write_indirect_object;
    use std::io::Cursor;

    let _ = config; // Reserved for future use
    let objects: Vec<(ObjRef, Object)> = doc
        .get_all_objects()
        .into_iter()
        .map(|(r, o)| (r, o.clone()))
        .collect();

    if objects.is_empty() {
        return Err(QpdfError::Linearization(
            "Document has no objects to linearize".to_string(),
        ));
    }

    let root_ref = doc
        .get_root_ref()
        .ok_or_else(|| QpdfError::Linearization("Document has no root catalog".to_string()))?;

    let mut first_page_refs = std::collections::HashSet::new();
    first_page_refs.insert(root_ref);
    if let Ok(pages) = doc.get_all_pages() {
        if let Some(first_page) = pages.first().copied() {
            first_page_refs.insert(first_page);
            if let Ok(root) = doc.get_root() {
                if let Some(pages_ref) = root
                    .as_dict()
                    .and_then(|d| d.get(&Name::new("Pages")))
                    .and_then(|o| o.as_obj_ref())
                {
                    first_page_refs.insert(pages_ref);
                }
            }
            if let Some(obj) = doc.get_object(first_page) {
                collect_refs_from_object(obj, doc, &mut first_page_refs);
            }
        }
    }

    let (first_objs, rest_objs): (Vec<_>, Vec<_>) = objects
        .into_iter()
        .partition(|(r, _)| first_page_refs.contains(r));

    let mut output = Vec::new();
    let mut cursor = Cursor::new(&mut output);

    let size = first_objs.len() + rest_objs.len() + 1;
    let first_page_obj = first_objs
        .iter()
        .find(|(_, o)| {
            o.as_dict().map_or(false, |d| {
                d.get(&Name::new("Type"))
                    .and_then(|v| v.as_name())
                    .map_or(false, |n| n.as_str() == "Page")
            })
        })
        .map(|(r, _)| r.num as u32)
        .unwrap_or(0);

    cursor.write_all(format!("%PDF-{}\n", doc.pdf_version()).as_bytes())?;
    cursor.write_all(b"%\xE2\xE3\xCF\xD3\n")?;

    let lin_dict = format!(
        "<< /Linearized 1 /L 0 /H [0 0] /O {} /E 0 /N {} /T 0 >>",
        first_page_obj,
        first_objs.len().max(1)
    );
    cursor.write_all(lin_dict.as_bytes())?;
    cursor.write_all(b"\n")?;

    let mut first_offsets: Vec<u64> = Vec::new();
    for (id, obj) in &first_objs {
        let offset = write_indirect_object(&mut cursor, *id, obj)?;
        first_offsets.push(offset);
    }

    let first_page_end = cursor.stream_position()? as u64;
    let hint_offset = first_page_end;

    let hint_data: Vec<u8> = vec![];
    let hint_obj_num = size;
    cursor.write_all(format!("{} 0 obj\n", hint_obj_num).as_bytes())?;
    cursor.write_all(format!("<< /Length {} >>\n", hint_data.len()).as_bytes())?;
    cursor.write_all(b"stream\n")?;
    cursor.write_all(&hint_data)?;
    cursor.write_all(b"\nendstream\nendobj\n")?;

    let mut rest_offsets: Vec<(ObjRef, u64)> = Vec::new();
    for (id, obj) in &rest_objs {
        let offset = write_indirect_object(&mut cursor, *id, obj)?;
        rest_offsets.push((*id, offset));
    }

    let main_xref_offset = cursor.stream_position()? as u64;
    let mut all_entries: Vec<(i32, u64, i32)> = Vec::new();
    for (i, (id, _)) in first_objs.iter().enumerate() {
        let offset = first_offsets.get(i).copied().unwrap_or(0);
        all_entries.push((id.num, offset as u64, id.generation));
    }
    for (id, offset) in &rest_offsets {
        all_entries.push((id.num, *offset, id.generation));
    }
    all_entries.push((hint_obj_num as i32, hint_offset, 0));
    all_entries.sort_by_key(|(num, _, _)| *num);
    let xref_size = all_entries.len() + 1;

    cursor.write_all(b"xref\n")?;
    cursor.write_all(format!("0 {}\n", xref_size).as_bytes())?;
    cursor.write_all(b"0000000000 65535 f \n")?;
    for (_, offset, generation) in &all_entries {
        cursor.write_all(format!("{:010} {:05} n \n", offset, generation).as_bytes())?;
    }

    cursor.write_all(b"trailer\n<<\n")?;
    cursor.write_all(format!("/Size {}\n", xref_size).as_bytes())?;
    cursor.write_all(format!("/Root {} {} R\n", root_ref.num, root_ref.generation).as_bytes())?;
    cursor.write_all(b">>\n")?;
    cursor.write_all(b"startxref\n")?;
    cursor.write_all(format!("{}\n", main_xref_offset).as_bytes())?;
    cursor.write_all(b"%%EOF\n")?;

    Ok(output)
}

fn collect_refs_from_object(
    obj: &Object,
    doc: &Document,
    refs: &mut std::collections::HashSet<ObjRef>,
) {
    match obj {
        Object::Ref(r) => {
            if refs.insert(*r) {
                if let Some(o) = doc.get_object(*r) {
                    collect_refs_from_object(o, doc, refs);
                }
            }
        }
        Object::Array(arr) => {
            for o in arr {
                collect_refs_from_object(o, doc, refs);
            }
        }
        Object::Dict(d) => {
            for v in d.values() {
                collect_refs_from_object(v, doc, refs);
            }
        }
        Object::Stream { dict, .. } => {
            for v in dict.values() {
                collect_refs_from_object(v, doc, refs);
            }
        }
        _ => {}
    }
}

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

    #[test]
    fn test_is_linearized_false() {
        let data = b"%PDF-1.7\n1 0 obj\n<</Type/Catalog>>\nendobj\n";
        assert!(!is_linearized(data));
    }

    #[test]
    fn test_is_linearized_true() {
        let data = b"%PDF-1.7\n1 0 obj\n<</Linearized 1/L 1000/O 5/E 500/N 10/T 900>>\nendobj\n";
        assert!(is_linearized(data));
    }

    #[test]
    fn test_check_linearization_not_linearized() {
        let data = b"%PDF-1.7\n1 0 obj\n<</Type/Catalog>>\nendobj\n";
        let result = check_linearization(data).unwrap();
        assert!(!result.is_linearized);
    }

    #[test]
    fn test_bit_reader() {
        let data = [0xFFu8, 0xFF, 0xFF, 0xFF];
        let mut reader = BitReader::new(&data);
        assert_eq!(reader.read_bits(8).unwrap(), 0xFF);
        assert_eq!(reader.read_bits(16).unwrap(), 0xFFFF);
        reader.align_to_byte();
        assert_eq!(reader.read_bits(8).unwrap(), 0xFF);
    }

    #[test]
    fn test_parse_linearization_dict_h_array() {
        let data = b"%PDF-1.7\n1 0 obj\n<< /Linearized 1 /L 2000 /H [100 200] /O 3 /E 500 /N 5 /T 1800 >>\nendobj\n";
        let params = parse_linearization_dict(data).unwrap();
        assert_eq!(params.hint_offset, 100);
        assert_eq!(params.hint_length, 200);
    }

    #[test]
    fn test_write_linearized_empty_pdf() {
        use crate::pdf::object::Name;
        let mut doc = Document::empty_pdf();
        let config = LinearizationConfig::default();
        let result = write_linearized(&mut doc, &config);
        assert!(result.is_ok());
        let pdf = result.unwrap();
        assert!(pdf.starts_with(b"%PDF-"));
        assert!(pdf.ends_with(b"%%EOF\n"));
        assert!(pdf.windows(4).any(|w| w == b"xref"));
    }
}
