//! Bridge between text-document snapshot types and text-typeset layout params.
//!
//! Converts `FlowSnapshot`, `BlockSnapshot`, `TextFormat`, etc. into
//! `BlockLayoutParams`, `FragmentParams`, `TableLayoutParams`, etc.

use text_document::{
    BlockSnapshot, CellSnapshot, FlowElementSnapshot, FlowSnapshot, FragmentContent, FrameSnapshot,
    TableSnapshot,
};

use crate::layout::block::{BlockLayoutParams, FragmentParams};
use crate::layout::frame::{FrameLayoutParams, FramePosition};
use crate::layout::paragraph::Alignment;
use crate::layout::table::{CellLayoutParams, TableLayoutParams};

const DEFAULT_LIST_INDENT: f32 = 24.0;
const INDENT_PER_LEVEL: f32 = 24.0;

/// Convert a FlowSnapshot into layout params that can be fed to the Typesetter.
pub fn convert_flow(flow: &FlowSnapshot) -> FlowElements {
    let mut blocks = Vec::new();
    let mut tables = Vec::new();
    let mut frames = Vec::new();

    for (i, element) in flow.elements.iter().enumerate() {
        match element {
            FlowElementSnapshot::Block(block) => {
                blocks.push((i, convert_block(block)));
            }
            FlowElementSnapshot::Table(table) => {
                tables.push((i, convert_table(table)));
            }
            FlowElementSnapshot::Frame(frame) => {
                frames.push((i, convert_frame(frame)));
            }
        }
    }

    FlowElements {
        blocks,
        tables,
        frames,
    }
}

/// Converted flow elements, ordered by their position in the flow.
pub struct FlowElements {
    /// (flow_index, params)
    pub blocks: Vec<(usize, BlockLayoutParams)>,
    pub tables: Vec<(usize, TableLayoutParams)>,
    pub frames: Vec<(usize, FrameLayoutParams)>,
}

pub fn convert_block(block: &BlockSnapshot) -> BlockLayoutParams {
    let alignment = block
        .block_format
        .alignment
        .as_ref()
        .map(convert_alignment)
        .unwrap_or_default();

    let heading_scale = match block.block_format.heading_level {
        Some(1) => 2.0,
        Some(2) => 1.5,
        Some(3) => 1.25,
        Some(4) => 1.1,
        _ => 1.0,
    };

    let fragments: Vec<FragmentParams> = block
        .fragments
        .iter()
        .map(|f| convert_fragment(f, heading_scale))
        .collect();

    let indent_level = block.block_format.indent.unwrap_or(0) as f32;

    let (list_marker, list_indent) = if let Some(ref info) = block.list_info {
        let list_indent_level = info.indent as f32;
        (
            info.marker.clone(),
            DEFAULT_LIST_INDENT + list_indent_level * INDENT_PER_LEVEL,
        )
    } else {
        (String::new(), indent_level * INDENT_PER_LEVEL)
    };

    let checkbox = match block.block_format.marker {
        Some(text_document::MarkerType::Checked) => Some(true),
        Some(text_document::MarkerType::Unchecked) => Some(false),
        _ => None,
    };

    BlockLayoutParams {
        block_id: block.block_id,
        position: block.position,
        text: block.text.clone(),
        fragments,
        alignment,
        top_margin: block.block_format.top_margin.unwrap_or(0) as f32,
        bottom_margin: block.block_format.bottom_margin.unwrap_or(0) as f32,
        left_margin: block.block_format.left_margin.unwrap_or(0) as f32,
        right_margin: block.block_format.right_margin.unwrap_or(0) as f32,
        text_indent: block.block_format.text_indent.unwrap_or(0) as f32,
        list_marker,
        list_indent,
        tab_positions: block
            .block_format
            .tab_positions
            .iter()
            .map(|&t| t as f32)
            .collect(),
        line_height_multiplier: block.block_format.line_height,
        non_breakable_lines: block.block_format.non_breakable_lines.unwrap_or(false)
            || block.block_format.is_code_block == Some(true),
        checkbox,
        background_color: block
            .block_format
            .background_color
            .as_ref()
            .and_then(|s| parse_css_color(s))
            .or_else(|| {
                if block.block_format.is_code_block == Some(true) {
                    Some([0.95, 0.95, 0.95, 1.0])
                } else {
                    None
                }
            }),
    }
}

fn convert_fragment(frag: &FragmentContent, heading_scale: f32) -> FragmentParams {
    match frag {
        FragmentContent::Text {
            text,
            format,
            offset,
            length,
        } => FragmentParams {
            text: text.clone(),
            offset: *offset,
            length: *length,
            font_family: format.font_family.clone(),
            font_weight: format.font_weight,
            font_bold: format.font_bold,
            font_italic: format.font_italic,
            font_point_size: if heading_scale != 1.0 {
                // Apply heading scale; use 16 as default if no explicit size
                Some((format.font_point_size.unwrap_or(16) as f32 * heading_scale) as u32)
            } else {
                format.font_point_size
            },
            underline_style: convert_underline_style(format),
            overline: format.font_overline.unwrap_or(false),
            strikeout: format.font_strikeout.unwrap_or(false),
            is_link: format.is_anchor.unwrap_or(false),
            letter_spacing: format.letter_spacing.unwrap_or(0) as f32,
            word_spacing: format.word_spacing.unwrap_or(0) as f32,
            foreground_color: format.foreground_color.as_ref().map(convert_color),
            underline_color: format.underline_color.as_ref().map(convert_color),
            background_color: format.background_color.as_ref().map(convert_color),
            anchor_href: format.anchor_href.clone(),
            tooltip: format.tooltip.clone(),
            vertical_alignment: convert_vertical_alignment(format),
            image_name: None,
            image_width: 0.0,
            image_height: 0.0,
        },
        FragmentContent::Image {
            name,
            width,
            height,
            quality: _,
            format,
            offset,
        } => FragmentParams {
            text: "\u{FFFC}".to_string(),
            offset: *offset,
            length: 1,
            font_family: None,
            font_weight: None,
            font_bold: None,
            font_italic: None,
            font_point_size: None,
            underline_style: crate::types::UnderlineStyle::None,
            overline: false,
            strikeout: false,
            is_link: format.is_anchor.unwrap_or(false),
            letter_spacing: 0.0,
            word_spacing: 0.0,
            foreground_color: None,
            underline_color: None,
            background_color: None,
            anchor_href: format.anchor_href.clone(),
            tooltip: format.tooltip.clone(),
            vertical_alignment: crate::types::VerticalAlignment::Normal,
            image_name: Some(name.clone()),
            image_width: *width as f32,
            image_height: *height as f32,
        },
    }
}

fn convert_vertical_alignment(
    format: &text_document::TextFormat,
) -> crate::types::VerticalAlignment {
    use crate::types::VerticalAlignment;
    match format.vertical_alignment {
        Some(text_document::CharVerticalAlignment::SuperScript) => VerticalAlignment::SuperScript,
        Some(text_document::CharVerticalAlignment::SubScript) => VerticalAlignment::SubScript,
        _ => VerticalAlignment::Normal,
    }
}

fn convert_underline_style(format: &text_document::TextFormat) -> crate::types::UnderlineStyle {
    use crate::types::UnderlineStyle;
    match format.underline_style {
        Some(text_document::UnderlineStyle::SingleUnderline) => UnderlineStyle::Single,
        Some(text_document::UnderlineStyle::DashUnderline) => UnderlineStyle::Dash,
        Some(text_document::UnderlineStyle::DotLine) => UnderlineStyle::Dot,
        Some(text_document::UnderlineStyle::DashDotLine) => UnderlineStyle::DashDot,
        Some(text_document::UnderlineStyle::DashDotDotLine) => UnderlineStyle::DashDotDot,
        Some(text_document::UnderlineStyle::WaveUnderline) => UnderlineStyle::Wave,
        Some(text_document::UnderlineStyle::SpellCheckUnderline) => UnderlineStyle::SpellCheck,
        Some(text_document::UnderlineStyle::NoUnderline) => UnderlineStyle::None,
        None => {
            if format.font_underline.unwrap_or(false) {
                UnderlineStyle::Single
            } else {
                UnderlineStyle::None
            }
        }
    }
}

fn convert_color(c: &text_document::Color) -> [f32; 4] {
    [
        c.red as f32 / 255.0,
        c.green as f32 / 255.0,
        c.blue as f32 / 255.0,
        c.alpha as f32 / 255.0,
    ]
}

/// Parse a CSS color string into RGBA floats (0.0-1.0).
///
/// Supports: `#RGB`, `#RRGGBB`, `#RRGGBBAA`, `rgb(r,g,b)`, `rgba(r,g,b,a)`,
/// and common named colors.
fn parse_css_color(s: &str) -> Option<[f32; 4]> {
    let s = s.trim();

    // Named colors
    match s.to_ascii_lowercase().as_str() {
        "transparent" => return Some([0.0, 0.0, 0.0, 0.0]),
        "black" => return Some([0.0, 0.0, 0.0, 1.0]),
        "white" => return Some([1.0, 1.0, 1.0, 1.0]),
        "red" => return Some([1.0, 0.0, 0.0, 1.0]),
        "green" => return Some([0.0, 128.0 / 255.0, 0.0, 1.0]),
        "blue" => return Some([0.0, 0.0, 1.0, 1.0]),
        "yellow" => return Some([1.0, 1.0, 0.0, 1.0]),
        "cyan" | "aqua" => return Some([0.0, 1.0, 1.0, 1.0]),
        "magenta" | "fuchsia" => return Some([1.0, 0.0, 1.0, 1.0]),
        "gray" | "grey" => return Some([128.0 / 255.0, 128.0 / 255.0, 128.0 / 255.0, 1.0]),
        _ => {}
    }

    // Hex formats
    if let Some(hex) = s.strip_prefix('#') {
        let hex = hex.trim();
        return match hex.len() {
            3 => {
                // #RGB
                let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
                let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
                let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
                Some([
                    (r * 17) as f32 / 255.0,
                    (g * 17) as f32 / 255.0,
                    (b * 17) as f32 / 255.0,
                    1.0,
                ])
            }
            4 => {
                // #RGBA
                let r = u8::from_str_radix(&hex[0..1], 16).ok()?;
                let g = u8::from_str_radix(&hex[1..2], 16).ok()?;
                let b = u8::from_str_radix(&hex[2..3], 16).ok()?;
                let a = u8::from_str_radix(&hex[3..4], 16).ok()?;
                Some([
                    (r * 17) as f32 / 255.0,
                    (g * 17) as f32 / 255.0,
                    (b * 17) as f32 / 255.0,
                    (a * 17) as f32 / 255.0,
                ])
            }
            6 => {
                // #RRGGBB
                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
                Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
            }
            8 => {
                // #RRGGBBAA
                let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
                let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
                let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
                let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
                Some([
                    r as f32 / 255.0,
                    g as f32 / 255.0,
                    b as f32 / 255.0,
                    a as f32 / 255.0,
                ])
            }
            _ => None,
        };
    }

    // rgb(r, g, b) and rgba(r, g, b, a)
    let inner = if let Some(inner) = s.strip_prefix("rgba(").and_then(|s| s.strip_suffix(')')) {
        inner
    } else if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
        inner
    } else {
        return None;
    };

    let parts: Vec<&str> = inner.split(',').collect();
    match parts.len() {
        3 => {
            let r: u8 = parts[0].trim().parse().ok()?;
            let g: u8 = parts[1].trim().parse().ok()?;
            let b: u8 = parts[2].trim().parse().ok()?;
            Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, 1.0])
        }
        4 => {
            let r: u8 = parts[0].trim().parse().ok()?;
            let g: u8 = parts[1].trim().parse().ok()?;
            let b: u8 = parts[2].trim().parse().ok()?;
            let a: f32 = parts[3].trim().parse().ok()?;
            Some([r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a])
        }
        _ => None,
    }
}

fn convert_alignment(a: &text_document::Alignment) -> Alignment {
    match a {
        text_document::Alignment::Left => Alignment::Left,
        text_document::Alignment::Right => Alignment::Right,
        text_document::Alignment::Center => Alignment::Center,
        text_document::Alignment::Justify => Alignment::Justify,
    }
}

pub fn convert_table(table: &TableSnapshot) -> TableLayoutParams {
    let column_widths: Vec<f32> = table.column_widths.iter().map(|&w| w as f32).collect();

    let cells: Vec<CellLayoutParams> = table.cells.iter().map(convert_cell).collect();

    TableLayoutParams {
        table_id: table.table_id,
        rows: table.rows,
        columns: table.columns,
        column_widths,
        border_width: table.format.border.unwrap_or(1) as f32,
        cell_spacing: table.format.cell_spacing.unwrap_or(0) as f32,
        cell_padding: table.format.cell_padding.unwrap_or(4) as f32,
        cells,
    }
}

fn convert_cell(cell: &CellSnapshot) -> CellLayoutParams {
    let blocks: Vec<BlockLayoutParams> = cell.blocks.iter().map(convert_block).collect();

    let background_color = cell
        .format
        .background_color
        .as_ref()
        .and_then(|s| parse_css_color(s));

    CellLayoutParams {
        row: cell.row,
        column: cell.column,
        blocks,
        background_color,
    }
}

pub fn convert_frame(frame: &FrameSnapshot) -> FrameLayoutParams {
    let mut blocks = Vec::new();
    let mut tables = Vec::new();
    let mut frames = Vec::new();

    for (i, element) in frame.elements.iter().enumerate() {
        match element {
            FlowElementSnapshot::Block(block) => {
                blocks.push(convert_block(block));
            }
            FlowElementSnapshot::Table(table) => {
                tables.push((i, convert_table(table)));
            }
            FlowElementSnapshot::Frame(inner_frame) => {
                frames.push((i, convert_frame(inner_frame)));
            }
        }
    }

    let position = match &frame.format.position {
        Some(text_document::FramePosition::InFlow) | None => FramePosition::Inline,
        Some(text_document::FramePosition::FloatLeft) => FramePosition::FloatLeft,
        Some(text_document::FramePosition::FloatRight) => FramePosition::FloatRight,
    };

    let is_blockquote = frame.format.is_blockquote == Some(true);

    FrameLayoutParams {
        frame_id: frame.frame_id,
        position,
        width: frame.format.width.map(|w| w as f32),
        height: frame.format.height.map(|h| h as f32),
        margin_top: frame
            .format
            .top_margin
            .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
        margin_bottom: frame
            .format
            .bottom_margin
            .unwrap_or(if is_blockquote { 4 } else { 0 }) as f32,
        margin_left: frame
            .format
            .left_margin
            .unwrap_or(if is_blockquote { 16 } else { 0 }) as f32,
        margin_right: frame.format.right_margin.unwrap_or(0) as f32,
        padding: frame
            .format
            .padding
            .unwrap_or(if is_blockquote { 8 } else { 0 }) as f32,
        border_width: frame
            .format
            .border
            .unwrap_or(if is_blockquote { 3 } else { 0 }) as f32,
        border_style: if is_blockquote {
            crate::layout::frame::FrameBorderStyle::LeftOnly
        } else {
            crate::layout::frame::FrameBorderStyle::Full
        },
        blocks,
        tables,
        frames,
    }
}
