//! Text extraction and removal.
//!
//! Walks content streams decoding text operators (Tj, TJ, ', ") to extract
//! text. Can also strip BT…ET blocks to remove text from pages.
//!
//! Zero `unsafe` in this file.

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

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

fn decompress_if_needed(data: &[u8]) -> Vec<u8> {
    let mut decoder = flate2::read::ZlibDecoder::new(data);
    let mut buf = Vec::new();
    if std::io::Read::read_to_end(&mut decoder, &mut buf).is_ok() && !buf.is_empty() {
        return buf;
    }
    data.to_vec()
}

fn extract_stream_data(pdf: &[u8], obj_num: i32) -> Option<Vec<u8>> {
    let text = String::from_utf8_lossy(pdf);
    let needle = format!("{obj_num} 0 obj");
    let start = text.find(&needle)?;
    let after = &text[start..];

    let stream_kw = after.find("stream")?;
    let abs_stream = start + stream_kw + 6;
    let mut data_start = abs_stream;
    if data_start < pdf.len() && pdf[data_start] == b'\r' {
        data_start += 1;
    }
    if data_start < pdf.len() && pdf[data_start] == b'\n' {
        data_start += 1;
    }

    let endstream = text[data_start..].find("endstream")?;
    let raw = &pdf[data_start..data_start + endstream];

    // Check if FlateDecode
    let dict_region = &text[start..start + stream_kw];
    if dict_region.contains("FlateDecode") || dict_region.contains("/Fl ") {
        let mut decoder = flate2::read::ZlibDecoder::new(raw);
        let mut buf = Vec::new();
        if std::io::Read::read_to_end(&mut decoder, &mut buf).is_ok() {
            return Some(buf);
        }
    }

    Some(raw.to_vec())
}

fn get_content_refs(pdf: &[u8]) -> Vec<i32> {
    let text = String::from_utf8_lossy(pdf);
    let mut pos = 0;
    while let Some(idx) = text[pos..]
        .find("/Type/Page")
        .or_else(|| text[pos..].find("/Type /Page"))
    {
        let abs = pos + idx;
        let page_marker_end = abs
            + if text[abs..].starts_with("/Type/Page") {
                10
            } else {
                11
            };
        if page_marker_end < text.len() && text.as_bytes()[page_marker_end] == b's' {
            pos = page_marker_end;
            continue;
        }
        let dict_start = text[..abs].rfind("<<").unwrap_or(abs);
        let dict_end = text[abs..]
            .find(">>")
            .map(|e| abs + e + 2)
            .unwrap_or(text.len());
        let dict = &text[dict_start..dict_end];

        if let Some(ci) = dict.find("/Contents") {
            let rest = &dict[ci + 9..];
            let trimmed = rest.trim_start();
            if trimmed.starts_with('[') {
                let close = trimmed.find(']').unwrap_or(trimmed.len());
                let inner = &trimmed[1..close];
                return inner
                    .split_whitespace()
                    .filter_map(|tok| tok.parse::<i32>().ok())
                    .collect();
            } else {
                let end = trimmed
                    .find(|c: char| !c.is_ascii_digit())
                    .unwrap_or(trimmed.len());
                if let Ok(n) = trimmed[..end].parse::<i32>() {
                    return vec![n];
                }
            }
        }
        break;
    }
    Vec::new()
}

fn extract_text_from_stream(data: &[u8]) -> String {
    let text = String::from_utf8_lossy(data);
    let mut result = String::new();
    let mut i = 0;
    let chars: Vec<char> = text.chars().collect();

    while i < chars.len() {
        // Find text showing operators
        if i + 1 < chars.len() && chars[i] == 'T' && chars[i + 1] == 'j' {
            // Look backward for the string
            if let Some(s) = find_preceding_string(&chars, i) {
                result.push_str(&s);
            }
            i += 2;
            continue;
        }

        if i + 1 < chars.len() && chars[i] == 'T' && chars[i + 1] == 'J' {
            // TJ takes an array - extract all strings from the array
            if let Some(s) = find_preceding_array_strings(&chars, i) {
                result.push_str(&s);
            }
            i += 2;
            continue;
        }

        // ' and " operators (show text with newline)
        if chars[i] == '\'' && i > 0 && (chars[i - 1] == ')' || chars[i - 1].is_whitespace()) {
            if let Some(s) = find_preceding_string(&chars, i) {
                result.push('\n');
                result.push_str(&s);
            }
            i += 1;
            continue;
        }

        i += 1;
    }

    result
}

fn find_preceding_string(chars: &[char], pos: usize) -> Option<String> {
    let mut end = pos;
    while end > 0 && chars[end - 1].is_whitespace() {
        end -= 1;
    }
    if end == 0 || chars[end - 1] != ')' {
        return None;
    }
    let close = end - 1;
    let mut depth = 1;
    let mut j = close;
    while j > 0 {
        j -= 1;
        if chars[j] == ')' && (j == 0 || chars[j - 1] != '\\') {
            depth += 1;
        } else if chars[j] == '(' && (j == 0 || chars[j - 1] != '\\') {
            depth -= 1;
            if depth == 0 {
                let s: String = chars[j + 1..close].iter().collect();
                return Some(
                    s.replace("\\(", "(")
                        .replace("\\)", ")")
                        .replace("\\\\", "\\"),
                );
            }
        }
    }
    None
}

fn find_preceding_array_strings(chars: &[char], pos: usize) -> Option<String> {
    let mut end = pos;
    while end > 0 && chars[end - 1].is_whitespace() {
        end -= 1;
    }
    if end == 0 || chars[end - 1] != ']' {
        return None;
    }
    let close = end - 1;
    let mut j = close;
    while j > 0 {
        j -= 1;
        if chars[j] == '[' {
            let inner: String = chars[j + 1..close].iter().collect();
            let mut result = String::new();
            let mut in_string = false;
            let mut current = String::new();

            for ch in inner.chars() {
                if ch == '(' && !in_string {
                    in_string = true;
                    current.clear();
                } else if ch == ')' && in_string {
                    in_string = false;
                    result.push_str(
                        &current
                            .replace("\\(", "(")
                            .replace("\\)", ")")
                            .replace("\\\\", "\\"),
                    );
                } else if in_string {
                    current.push(ch);
                }
            }
            return Some(result);
        }
    }
    None
}

fn strip_bt_et(stream: &[u8]) -> Vec<u8> {
    let text = String::from_utf8_lossy(stream);
    let mut result = String::new();
    let mut pos = 0;

    while pos < text.len() {
        if let Some(bt) = text[pos..].find("BT") {
            let abs_bt = pos + bt;
            // Ensure BT is at word boundary
            let before_ok = abs_bt == 0
                || text.as_bytes()[abs_bt - 1].is_ascii_whitespace()
                || text.as_bytes()[abs_bt - 1] == b'\n';
            if before_ok {
                if let Some(et) = text[abs_bt..].find("ET") {
                    let abs_et = abs_bt + et + 2;
                    result.push_str(&text[pos..abs_bt]);
                    pos = abs_et;
                    continue;
                }
            }
        }
        result.push_str(&text[pos..]);
        break;
    }

    result.into_bytes()
}

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

/// Extract text from pages in range.  Pages separated by `\n\n`.
pub fn extract_text(doc: &CpdfDocument, range: &[usize]) -> Result<String> {
    let n = doc.page_count();
    let mut parts = Vec::new();

    for page_num in range {
        if *page_num == 0 || *page_num > n {
            continue;
        }
        parts.push(extract_page_text(doc, *page_num)?);
    }

    Ok(parts.join("\n\n"))
}

/// Extract text from a single page (1-indexed).
pub fn extract_page_text(doc: &CpdfDocument, page: usize) -> Result<String> {
    let page_pdf = copy_page_to_bytes(&doc.to_bytes(), page - 1)
        .map_err(|e| CpdfError::Parse(format!("extract page {page}: {e}")))?;

    let content_refs = get_content_refs(&page_pdf);
    let mut combined_text = String::new();

    for obj_num in &content_refs {
        if let Some(data) = extract_stream_data(&page_pdf, *obj_num) {
            let t = extract_text_from_stream(&data);
            combined_text.push_str(&t);
        }
    }

    Ok(combined_text)
}

/// Remove all text (BT…ET blocks) from pages in range.
pub fn remove_text(doc: CpdfDocument, range: &[usize]) -> Result<CpdfDocument> {
    pages::apply_to_pages_pub(doc, range, |page_pdf| {
        let content_refs = get_content_refs(page_pdf);
        if content_refs.is_empty() {
            return Ok(page_pdf.to_vec());
        }

        let mut out = page_pdf.to_vec();
        for obj_num in &content_refs {
            if let Some(data) = extract_stream_data(&out, *obj_num) {
                let stripped = strip_bt_et(&data);
                out = replace_stream_simple(&out, *obj_num, &stripped)?;
            }
        }
        Ok(out)
    })
}

fn replace_stream_simple(pdf: &[u8], obj_num: i32, new_data: &[u8]) -> Result<Vec<u8>> {
    let text = String::from_utf8_lossy(pdf);
    let needle = format!("{obj_num} 0 obj");
    let start = text
        .find(&needle)
        .ok_or_else(|| CpdfError::Parse(format!("object {obj_num} not found")))?;

    let stream_kw = text[start..]
        .find("stream")
        .ok_or_else(|| CpdfError::Parse("stream not found".into()))?;
    let abs_stream = start + stream_kw;

    let dict_start = text[start..abs_stream]
        .find("<<")
        .map(|p| start + p)
        .ok_or_else(|| CpdfError::Parse("dict not found".into()))?;

    let endstream = text[abs_stream + 6..]
        .find("endstream")
        .ok_or_else(|| CpdfError::Parse("endstream not found".into()))?;
    let abs_endstream = abs_stream + 6 + endstream;

    let endobj = text[abs_endstream..]
        .find("endobj")
        .map(|p| abs_endstream + p + 6)
        .unwrap_or(pdf.len());
    let mut obj_end = endobj;
    while obj_end < pdf.len() && (pdf[obj_end] == b'\n' || pdf[obj_end] == b'\r') {
        obj_end += 1;
    }

    let line_start = text[..start].rfind('\n').map(|p| p + 1).unwrap_or(start);

    let new_obj = format!(
        "{obj_num} 0 obj\n<< /Length {} >>\nstream\n",
        new_data.len()
    );
    let mut new_bytes = new_obj.into_bytes();
    new_bytes.extend_from_slice(new_data);
    new_bytes.extend_from_slice(b"\nendstream\nendobj\n");

    let mut out = Vec::with_capacity(pdf.len());
    out.extend_from_slice(&pdf[..line_start]);
    out.extend_from_slice(&new_bytes);
    out.extend_from_slice(&pdf[obj_end..]);

    // Rebuild xref
    Ok(crate::cpdf::bookmarks::rebuild_xref_data(&out))
}

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

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

    #[test]
    fn blank_has_no_text() {
        let text = extract_page_text(&blank(1), 1).unwrap();
        assert!(text.is_empty());
    }

    #[test]
    fn remove_text_on_blank_is_noop() {
        let doc = blank(1);
        let doc = remove_text(doc, &[1]).unwrap();
        assert_eq!(doc.page_count(), 1);
    }
}
