//! Page Labels — numbering style per page range.
//!
//! Zero `unsafe` in this file.

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

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum LabelStyle {
    DecimalArabic,
    RomanUpper,
    RomanLower,
    AlphaUpper,
    AlphaLower,
    NoNumber,
}

impl LabelStyle {
    pub fn to_pdf_name(&self) -> &'static str {
        match self {
            Self::DecimalArabic => "/D",
            Self::RomanUpper => "/R",
            Self::RomanLower => "/r",
            Self::AlphaUpper => "/A",
            Self::AlphaLower => "/a",
            Self::NoNumber => "",
        }
    }

    pub fn from_pdf_name(s: &str) -> Self {
        match s.trim_start_matches('/') {
            "D" => Self::DecimalArabic,
            "R" => Self::RomanUpper,
            "r" => Self::RomanLower,
            "A" => Self::AlphaUpper,
            "a" => Self::AlphaLower,
            _ => Self::NoNumber,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageLabelRange {
    pub start_page: usize,
    pub style: LabelStyle,
    pub prefix: String,
    pub start_value: usize,
}

fn find_key_not_prefix(text: &str, key: &str, prefixes: &[&str]) -> Option<usize> {
    let mut start = 0;
    while let Some(idx) = text[start..].find(key) {
        let abs = start + idx;
        let is_prefix = prefixes.iter().any(|p| text[abs..].starts_with(p));
        if !is_prefix {
            return Some(abs);
        }
        start = abs + key.len();
    }
    None
}

/// Get existing page label ranges from the document.
pub fn get_page_labels(doc: &CpdfDocument) -> Result<Vec<PageLabelRange>> {
    let bytes = doc.to_bytes();
    let text = String::from_utf8_lossy(&bytes);

    let nums_idx = match text.find("/PageLabels") {
        Some(i) => i,
        None => return Ok(Vec::new()),
    };

    let after = &text[nums_idx..];
    let nums_start = match after.find("/Nums") {
        Some(i) => nums_idx + i,
        None => return Ok(Vec::new()),
    };

    let arr_start = match text[nums_start..].find('[') {
        Some(i) => nums_start + i + 1,
        None => return Ok(Vec::new()),
    };
    let arr_end = match text[arr_start..].find(']') {
        Some(i) => arr_start + i,
        None => return Ok(Vec::new()),
    };

    let arr = &text[arr_start..arr_end];
    let mut labels = Vec::new();
    let mut chars = arr.chars().peekable();
    let mut pos = 0;

    while pos < arr.len() {
        let remaining = arr[pos..].trim_start();
        if remaining.is_empty() {
            break;
        }

        let num_end = remaining
            .find(|c: char| !c.is_ascii_digit())
            .unwrap_or(remaining.len());
        if num_end == 0 {
            pos += 1;
            continue;
        }

        let page: usize = match remaining[..num_end].parse() {
            Ok(n) => n,
            Err(_) => {
                pos += 1;
                continue;
            }
        };
        pos += (arr.len() - pos) - remaining.len() + num_end;

        let dict_remaining = arr[pos..].trim_start();
        if !dict_remaining.starts_with("<<") {
            labels.push(PageLabelRange {
                start_page: page,
                style: LabelStyle::DecimalArabic,
                prefix: String::new(),
                start_value: 1,
            });
            continue;
        }

        let dict_end = match dict_remaining.find(">>") {
            Some(i) => i + 2,
            None => break,
        };
        let dict = &dict_remaining[2..dict_end - 2];

        let style = if let Some(si) = find_key_not_prefix(dict, "/S", &["/St"]) {
            let rest = dict[si + 2..].trim_start();
            if rest.starts_with('/') {
                let name_end = rest[1..]
                    .find(|c: char| c.is_whitespace() || c == '/' || c == '>')
                    .map(|i| i + 1)
                    .unwrap_or(rest.len());
                LabelStyle::from_pdf_name(&rest[..name_end])
            } else {
                LabelStyle::NoNumber
            }
        } else {
            LabelStyle::NoNumber
        };

        let prefix = if let Some(pi) = dict.find("/P") {
            let rest = dict[pi + 2..].trim_start();
            if rest.starts_with('(') {
                let close = rest[1..].find(')').unwrap_or(rest.len());
                rest[1..1 + close].to_string()
            } else {
                String::new()
            }
        } else {
            String::new()
        };

        let start_value = if let Some(sti) = dict.find("/St") {
            let rest = dict[sti + 3..].trim_start();
            let num_end = rest
                .find(|c: char| !c.is_ascii_digit())
                .unwrap_or(rest.len());
            rest[..num_end].parse().unwrap_or(1)
        } else {
            1
        };

        labels.push(PageLabelRange {
            start_page: page,
            style,
            prefix,
            start_value,
        });

        pos += (arr.len() - pos) - dict_remaining.len() + dict_end;
    }

    Ok(labels)
}

/// Add page labels to a range.
pub fn add_page_labels(
    doc: CpdfDocument,
    start_page: usize,
    style: LabelStyle,
    prefix: &str,
    start_value: usize,
) -> Result<CpdfDocument> {
    let bytes = doc.to_bytes();
    let mut text = String::from_utf8_lossy(&bytes).to_string();

    let style_entry = if style != LabelStyle::NoNumber {
        format!(" /S {}", style.to_pdf_name())
    } else {
        String::new()
    };
    let prefix_entry = if !prefix.is_empty() {
        format!(" /P ({})", prefix.replace('(', "\\(").replace(')', "\\)"))
    } else {
        String::new()
    };
    let st_entry = if start_value != 1 {
        format!(" /St {start_value}")
    } else {
        String::new()
    };
    let label_entry = format!("{start_page} <<{style_entry}{prefix_entry}{st_entry} >>");

    if let Some(nums_idx) = text.find("/PageLabels") {
        if let Some(arr_open) = text[nums_idx..].find('[') {
            let insert_pos = nums_idx + arr_open + 1;
            text.insert_str(insert_pos, &format!(" {label_entry}"));
        }
    } else {
        let cat_idx = text
            .find("/Type /Catalog")
            .or_else(|| text.find("/Type/Catalog"))
            .ok_or_else(|| CpdfError::Parse("catalog not found".into()))?;
        let dict_end = text[cat_idx..].find(">>").unwrap_or(0) + cat_idx;
        text.insert_str(
            dict_end,
            &format!(" /PageLabels << /Nums [ {label_entry} ] >>"),
        );
    }

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

/// Remove all page labels.
pub fn remove_page_labels(doc: CpdfDocument) -> Result<CpdfDocument> {
    let bytes = doc.to_bytes();
    let mut text = String::from_utf8_lossy(&bytes).to_string();

    if let Some(idx) = text.find("/PageLabels") {
        let after = &text[idx..];
        let depth_start = match after.find("<<") {
            Some(i) => i,
            None => {
                let end = after
                    .find(|c: char| c == '/' || c == '>')
                    .unwrap_or(after.len());
                text = format!("{}{}", &text[..idx], &text[idx + end..]);
                let final_bytes = rebuild_xref_data(text.as_bytes());
                return CpdfDocument::from_bytes(final_bytes);
            }
        };
        let mut depth = 0i32;
        let mut end = depth_start;
        let chars: Vec<char> = after.chars().collect();
        let mut i = depth_start;
        while i < chars.len().saturating_sub(1) {
            if chars[i] == '<' && chars[i + 1] == '<' {
                depth += 1;
                i += 2;
            } else if chars[i] == '>' && chars[i + 1] == '>' {
                depth -= 1;
                if depth == 0 {
                    end = i + 2;
                    break;
                }
                i += 2;
            } else {
                i += 1;
            }
        }
        text = format!("{}{}", &text[..idx], &text[idx + end..]);
    }

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

/// Get page labels as JSON.
pub fn get_page_labels_json(doc: &CpdfDocument) -> Result<String> {
    let labels = get_page_labels(doc)?;
    serde_json::to_string_pretty(&labels).map_err(|e| CpdfError::Parse(e.to_string()))
}
