kitmd 0.1.0

A terminal-based markdown and mermaid renderer/viewer using the Kitty graphics protocol
use std::cmp::Ordering;
use std::collections::{BTreeMap, HashMap};

use crate::mermaid_engine::config::LayoutConfig;
use crate::mermaid_engine::ir::Graph;
use crate::mermaid_engine::theme::Theme;

use super::text::measure_label_with_font_size;
use super::{
    DiagramData, Layout, PieData, PieLegendItem, PieSliceLayout, PieTitleLayout, TextBlock,
};

fn pie_palette(theme: &Theme) -> Vec<String> {
    theme.pie_colors.to_vec()
}

fn sanitize_pie_value(value: f32) -> f32 {
    if value.is_finite() {
        value.max(0.0)
    } else {
        0.0
    }
}

#[allow(dead_code)]
fn format_pie_value(value: f32) -> String {
    let rounded = (value * 100.0).round() / 100.0;
    if (rounded - rounded.round()).abs() < 0.001 {
        format!("{:.0}", rounded)
    } else {
        format!("{:.2}", rounded)
    }
}

pub(super) fn compute_pie_layout(graph: &Graph, theme: &Theme, config: &LayoutConfig) -> Layout {
    let pie_cfg = &config.pie;
    let mut slices = Vec::new();
    let mut legend = Vec::new();
    let title_block = graph.pie_title.as_ref().map(|title| {
        measure_label_with_font_size(
            title,
            theme.pie_title_text_size,
            config,
            false,
            theme.font_family.as_str(),
        )
    });

    let palette = pie_palette(theme);
    let total: f32 = graph
        .pie_slices
        .iter()
        .map(|slice| sanitize_pie_value(slice.value))
        .sum();
    let fallback_total = graph.pie_slices.len().max(1) as f32;
    let total = if total > 0.0 { total } else { fallback_total };

    #[derive(Clone)]
    struct PieDatum {
        index: usize,
        label: String,
        value: f32,
    }

    let mut filtered: Vec<PieDatum> = Vec::new();
    for (idx, slice) in graph.pie_slices.iter().enumerate() {
        let value = sanitize_pie_value(slice.value);
        let percent = if total > 0.0 {
            value / total * 100.0
        } else {
            0.0
        };
        if percent >= pie_cfg.min_percent {
            filtered.push(PieDatum {
                index: idx,
                label: slice.label.clone(),
                value,
            });
        }
    }
    filtered.sort_by(|a, b| {
        b.value
            .partial_cmp(&a.value)
            .unwrap_or(Ordering::Equal)
            .then_with(|| a.index.cmp(&b.index))
    });

    let mut color_map: HashMap<String, String> = HashMap::new();
    let mut color_index: usize = 0;
    let mut resolve_color = |label: &str| -> String {
        if let Some(color) = color_map.get(label) {
            return color.clone();
        }
        let color = palette[color_index % palette.len()].clone();
        color_index += 1;
        color_map.insert(label.to_string(), color.clone());
        color
    };

    let mut angle = 0.0_f32;
    for datum in &filtered {
        let span = if total > 0.0 {
            datum.value / total * std::f32::consts::PI * 2.0
        } else {
            std::f32::consts::PI * 2.0 / fallback_total
        };
        let label = measure_label_with_font_size(
            &datum.label,
            theme.pie_section_text_size,
            config,
            false,
            theme.font_family.as_str(),
        );
        let color = resolve_color(&datum.label);
        slices.push(PieSliceLayout {
            label,
            value: datum.value,
            start_angle: angle,
            end_angle: angle + span,
            color,
        });
        angle += span;
    }

    let mut legend_width: f32 = 0.0;
    let mut legend_items: Vec<(TextBlock, String)> = Vec::new();
    for slice in &graph.pie_slices {
        let value_text = format_pie_value(sanitize_pie_value(slice.value));
        let label_text = if graph.pie_show_data {
            format!("{} [{}]", slice.label, value_text)
        } else {
            slice.label.clone()
        };
        let label = measure_label_with_font_size(
            &label_text,
            theme.pie_legend_text_size,
            config,
            false,
            theme.font_family.as_str(),
        );
        legend_width = legend_width.max(label.width);
        let color = resolve_color(&slice.label);
        legend_items.push((label, color));
    }

    let legend_text_height = theme.pie_legend_text_size * 1.25;
    let legend_item_height =
        (pie_cfg.legend_rect_size + pie_cfg.legend_spacing).max(legend_text_height);
    let legend_offset = legend_item_height * legend_items.len() as f32 / 2.0;

    let height = pie_cfg.height.max(1.0);
    let pie_width = height;
    let radius = (pie_width.min(height) / 2.0 - pie_cfg.margin).max(1.0);
    let center_x = pie_width / 2.0;
    let center_y = height / 2.0;
    let suppress_outside_labels = graph.pie_slices.len() >= 4;
    let mut right_outside_label_width: f32 = 0.0;
    if !suppress_outside_labels {
        for slice in &slices {
            let span = (slice.end_angle - slice.start_angle).abs();
            if span <= 0.0001 || total <= 0.0 {
                continue;
            }
            let percent_text = format!("{:.0}%", slice.value / total * 100.0);
            let percent_width = crate::mermaid_engine::text_metrics::measure_text_width(
                percent_text.as_str(),
                theme.pie_section_text_size,
                theme.font_family.as_str(),
            )
            .unwrap_or(percent_text.chars().count() as f32 * theme.pie_section_text_size * 0.55);
            let arc_len = radius * span;
            let outside = arc_len < percent_width * 1.35 || span < 0.4;
            let mid_angle = (slice.start_angle + slice.end_angle) / 2.0;
            if outside && mid_angle.cos() >= 0.0 {
                right_outside_label_width = right_outside_label_width.max(slice.label.width);
            }
        }
    }
    let outside_label_clearance = if right_outside_label_width > 0.0 {
        right_outside_label_width + pie_cfg.margin * 0.35
    } else {
        0.0
    };
    let legend_x = center_x + radius + pie_cfg.margin * 0.6 + outside_label_clearance;

    for (idx, (label, color)) in legend_items.into_iter().enumerate() {
        let vertical = idx as f32 * legend_item_height - legend_offset;
        legend.push(PieLegendItem {
            x: legend_x,
            y: center_y + vertical,
            label,
            color,
            marker_size: pie_cfg.legend_rect_size,
            value: sanitize_pie_value(graph.pie_slices[idx].value),
        });
    }

    let width = legend_x
        + pie_cfg.legend_rect_size
        + pie_cfg.legend_spacing
        + legend_width
        + pie_cfg.margin * 0.4;
    let title_layout = title_block.map(|text| PieTitleLayout {
        x: center_x,
        y: center_y - (height - 50.0) / 2.0,
        text,
    });

    Layout {
        kind: graph.kind,
        nodes: BTreeMap::new(),
        edges: Vec::new(),
        subgraphs: Vec::new(),
        width: width.max(200.0),
        height: height.max(1.0),
        diagram: DiagramData::Pie(PieData {
            slices,
            legend,
            center: (center_x, center_y),
            radius,
            title: title_layout,
        }),
    }
}