use std::collections::{BTreeSet, HashMap};
use std::hash::{DefaultHasher, Hash, Hasher};
use std::mem;
use std::sync::Arc;
use std::time::Instant;

use cosmic_text::{
    Attrs, Buffer, Color as CosmicColor, FontSystem, Metrics, Shaping, Style as CosmicStyle,
    Weight as CosmicWeight, Wrap, fontdb,
};
use fontconfig::Fontconfig;

use crate::{
    Color, GlyphInstance, Point, PreparedText, PreparedTextLine, Rect, TextLayoutData, UiSize,
};

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TextAlign {
    Start,
    Center,
    End,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TextSpanWeight {
    Normal,
    Medium,
    Semibold,
    Bold,
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TextSpanSlant {
    Normal,
    Italic,
    Oblique,
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum TextFontFamily {
    SansSerif,
    Serif,
    Monospace,
    Cursive,
    Fantasy,
    Named(String),
}

impl TextFontFamily {
    pub fn named(name: impl Into<String>) -> Self {
        Self::Named(name.into())
    }
}

#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub struct TextSpan {
    pub text: String,
    pub color: Option<Color>,
    pub weight: TextSpanWeight,
    pub slant: TextSpanSlant,
    pub font_family: Option<TextFontFamily>,
}

impl TextSpan {
    pub fn new(text: impl Into<String>) -> Self {
        Self {
            text: text.into(),
            color: None,
            weight: TextSpanWeight::Normal,
            slant: TextSpanSlant::Normal,
            font_family: None,
        }
    }

    pub fn color(mut self, color: Color) -> Self {
        self.color = Some(color);
        self
    }

    pub fn weight(mut self, weight: TextSpanWeight) -> Self {
        self.weight = weight;
        self
    }

    pub fn slant(mut self, slant: TextSpanSlant) -> Self {
        self.slant = slant;
        self
    }

    pub fn family(mut self, family: TextFontFamily) -> Self {
        self.font_family = Some(family);
        self
    }
}

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum TextWrap {
    None,
    Word,
}

impl TextWrap {
    const fn to_cosmic(self) -> Wrap {
        match self {
            Self::None => Wrap::None,
            Self::Word => Wrap::WordOrGlyph,
        }
    }
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct TextSelectionStyle {
    pub highlight_color: Color,
    pub text_color: Option<Color>,
}

impl TextSelectionStyle {
    pub const DEFAULT: Self = Self {
        highlight_color: Color::rgba(0x5B, 0x9D, 0xFF, 0x55),
        text_color: None,
    };

    pub const fn new(highlight_color: Color) -> Self {
        Self {
            highlight_color,
            text_color: None,
        }
    }

    pub const fn with_text_color(mut self, text_color: Color) -> Self {
        self.text_color = Some(text_color);
        self
    }
}

#[derive(Clone, Debug, PartialEq)]
pub struct TextStyle {
    pub font_size: f32,
    pub line_height: f32,
    pub color: Color,
    pub font_family: TextFontFamily,
    pub bounds: Option<UiSize>,
    pub wrap: TextWrap,
    pub align: TextAlign,
    pub max_lines: Option<usize>,
    pub selectable: bool,
    pub selection_style: TextSelectionStyle,
}

impl TextStyle {
    pub const fn new(font_size: f32, color: Color) -> Self {
        Self {
            font_size,
            line_height: font_size * 1.2,
            color,
            font_family: TextFontFamily::SansSerif,
            bounds: None,
            wrap: TextWrap::None,
            align: TextAlign::Start,
            max_lines: None,
            selectable: true,
            selection_style: TextSelectionStyle::DEFAULT,
        }
    }

    pub const fn with_line_height(mut self, line_height: f32) -> Self {
        self.line_height = line_height;
        self
    }

    pub fn with_font_family(mut self, font_family: TextFontFamily) -> Self {
        self.font_family = font_family;
        self
    }

    pub const fn with_bounds(mut self, bounds: UiSize) -> Self {
        self.bounds = Some(bounds);
        self
    }

    pub const fn with_wrap(mut self, wrap: TextWrap) -> Self {
        self.wrap = wrap;
        self
    }

    pub const fn with_align(mut self, align: TextAlign) -> Self {
        self.align = align;
        self
    }

    pub const fn with_max_lines(mut self, max_lines: usize) -> Self {
        self.max_lines = Some(max_lines);
        self
    }

    pub const fn with_selectable(mut self, selectable: bool) -> Self {
        self.selectable = selectable;
        self
    }

    pub const fn with_selection_style(mut self, selection_style: TextSelectionStyle) -> Self {
        self.selection_style = selection_style;
        self
    }
}

// ---------------------------------------------------------------------------
// LRU text layout cache
// ---------------------------------------------------------------------------

/// Simple generation-based LRU cache for `Arc<TextLayoutData>`.
/// No external dependencies — evicts the least-recently-used entry on insert
/// when at capacity.
struct LruTextCache {
    map: HashMap<u64, (u64, Arc<TextLayoutData>)>, // key → (generation, data)
    generation: u64,
    capacity: usize,
}

impl LruTextCache {
    fn new(capacity: usize) -> Self {
        Self {
            map: HashMap::with_capacity(capacity.min(64)),
            generation: 0,
            capacity,
        }
    }

    fn get(&mut self, key: u64) -> Option<Arc<TextLayoutData>> {
        if let Some((lru, data)) = self.map.get_mut(&key) {
            self.generation += 1;
            *lru = self.generation;
            return Some(Arc::clone(data));
        }
        None
    }

    fn insert(&mut self, key: u64, data: Arc<TextLayoutData>) {
        if self.map.len() >= self.capacity && !self.map.contains_key(&key) {
            // Evict the entry with the lowest generation (least recently used).
            if let Some(oldest_key) = self
                .map
                .iter()
                .min_by_key(|(_, (lru, _))| *lru)
                .map(|(k, _)| *k)
            {
                self.map.remove(&oldest_key);
            }
        }
        self.generation += 1;
        self.map.insert(key, (self.generation, data));
    }

    #[allow(dead_code)]
    fn len(&self) -> usize {
        self.map.len()
    }
}

// ---------------------------------------------------------------------------
// TextSystem
// ---------------------------------------------------------------------------

pub struct TextSystem {
    font_system: FontSystem,
    family_resolver: FontFamilyResolver,
    layout_cache: LruTextCache,
    frame_stats: TextFrameStats,
}

#[derive(Clone, Copy, Debug, Default)]
pub struct TextFrameStats {
    pub requests: u32,
    pub cache_hits: u32,
    pub cache_misses: u32,
    pub output_glyphs: u32,
    pub family_resolve_ms: f64,
    pub buffer_build_ms: f64,
    pub glyph_collect_ms: f64,
    pub miss_ms: f64,
}

struct FontFamilyResolver {
    fontconfig: Option<Fontconfig>,
    cache: HashMap<(TextFontFamily, TextSpanSlant), Option<String>>,
}

impl Default for TextSystem {
    fn default() -> Self {
        Self::new()
    }
}

impl TextSystem {
    pub fn new() -> Self {
        let mut font_system = FontSystem::new();
        let mut family_resolver = FontFamilyResolver::new();
        configure_default_font_families(&mut font_system, &mut family_resolver);
        Self {
            font_system,
            family_resolver,
            layout_cache: LruTextCache::new(1024),
            frame_stats: TextFrameStats::default(),
        }
    }

    pub(crate) fn reset_frame_stats(&mut self) {
        self.frame_stats = TextFrameStats::default();
    }

    pub(crate) fn take_frame_stats(&mut self) -> TextFrameStats {
        mem::take(&mut self.frame_stats)
    }

    pub fn prepare(
        &mut self,
        text: impl Into<String>,
        origin: Point,
        style: &TextStyle,
    ) -> PreparedText {
        let spans = [TextSpan::new(text)];
        self.prepare_spans(&spans, origin, style, style.bounds)
    }

    /// Shape `spans` and return a `PreparedText` with origin-relative glyph positions.
    ///
    /// Glyphs are stored in LOCAL coordinates (relative to `origin`).  The
    /// renderer adds `text.origin` when emitting geometry.  This allows the
    /// cached `Arc<TextLayoutData>` to be shared directly — no per-call
    /// translation is needed.
    ///
    /// Glyphs that use the default color are stored with `Color::SENTINEL`;
    /// the renderer substitutes `PreparedText::default_color` for those.  This
    /// makes the shape cache color-independent: the same shaped data is reused
    /// even when `style.color` differs between calls.
    pub fn prepare_spans(
        &mut self,
        spans: &[TextSpan],
        origin: Point,
        style: &TextStyle,
        bounds: Option<UiSize>,
    ) -> PreparedText {
        let bounds = bounds.or(style.bounds);
        // Use the cached Arc directly — glyphs are in local (origin-0) coords.
        let layout_data = self.layout(
            spans,
            style,
            bounds.map(|b| b.width),
            bounds.map(|b| b.height),
        );

        PreparedText::from_layout(
            None,
            origin,
            bounds,
            style.font_size,
            style.line_height,
            style.color,
            style.selectable,
            style.selection_style,
            layout_data,
        )
    }

    pub fn measure(
        &mut self,
        text: &str,
        style: &TextStyle,
        width: Option<f32>,
        height: Option<f32>,
    ) -> UiSize {
        let spans = [TextSpan::new(text)];
        self.measure_spans(&spans, style, width, height)
    }

    pub fn measure_spans(
        &mut self,
        spans: &[TextSpan],
        style: &TextStyle,
        width: Option<f32>,
        height: Option<f32>,
    ) -> UiSize {
        let layout = self.layout(spans, style, width, height);
        // Clamp the measured size to the supplied bounds (mirrors old clamp_text_layout).
        let mut size = layout.size;
        if let Some(w) = width {
            size.width = size.width.min(w.max(0.0));
        }
        if let Some(h) = height {
            size.height = size.height.min(h.max(0.0));
        }
        size
    }

    /// Return the cached (origin-0) `Arc<TextLayoutData>` for the given spans/style.
    /// Builds and caches on miss.  Color is NOT part of the cache key; glyphs
    /// that take the default color are stored with `Color::SENTINEL`.
    fn layout(
        &mut self,
        spans: &[TextSpan],
        style: &TextStyle,
        width: Option<f32>,
        height: Option<f32>,
    ) -> Arc<TextLayoutData> {
        self.frame_stats.requests = self.frame_stats.requests.saturating_add(1);
        let cache_key = layout_cache_key(spans, style, width, height);

        if let Some(cached) = self.layout_cache.get(cache_key) {
            self.frame_stats.cache_hits = self.frame_stats.cache_hits.saturating_add(1);
            self.frame_stats.output_glyphs = self
                .frame_stats
                .output_glyphs
                .saturating_add(cached.glyphs.len() as u32);
            return cached;
        }

        self.frame_stats.cache_misses = self.frame_stats.cache_misses.saturating_add(1);
        let miss_started = Instant::now();

        let uses_plain_text_fast_path = uses_plain_text_fast_path(spans);
        let family_resolve_started = Instant::now();
        let default_family = self.resolve_font_family(&style.font_family, TextSpanSlant::Normal);
        let resolved_span_families = (!uses_plain_text_fast_path).then(|| {
            spans
                .iter()
                .map(|span| {
                    self.resolve_font_family(
                        span.font_family.as_ref().unwrap_or(&style.font_family),
                        span.slant,
                    )
                })
                .collect::<Vec<_>>()
        });
        self.frame_stats.family_resolve_ms +=
            family_resolve_started.elapsed().as_secs_f64() * 1_000.0;

        let buffer_build_started = Instant::now();
        let mut buffer = Buffer::new_empty(Metrics::new(style.font_size, style.line_height));
        {
            let mut borrowed = buffer.borrow_with(&mut self.font_system);
            borrowed.set_wrap(style.wrap.to_cosmic());
            // For no-wrap + start-aligned text, width doesn't affect shaping.
            // Pass None so cosmic-text doesn't truncate and the result is
            // consistent with the width-independent cache key.
            let effective_width = if width_affects_layout(style) {
                width
            } else {
                None
            };
            borrowed.set_size(effective_width, None);
            let default_attrs = default_attrs_for_style(style, default_family.as_deref());
            if uses_plain_text_fast_path {
                borrowed.set_text(
                    spans[0].text.as_str(),
                    &default_attrs,
                    Shaping::Advanced,
                    None,
                );
            } else if let Some(resolved_span_families) = resolved_span_families.as_ref() {
                borrowed.set_rich_text(
                    spans.iter().zip(resolved_span_families.iter()).map(
                        |(span, resolved_family)| {
                            (
                                span.text.as_str(),
                                attrs_for_span(span, style, resolved_family.as_deref()),
                            )
                        },
                    ),
                    &default_attrs,
                    Shaping::Advanced,
                    None,
                );
            }
        }
        self.frame_stats.buffer_build_ms += buffer_build_started.elapsed().as_secs_f64() * 1_000.0;

        let text = combined_text(spans);
        let line_starts = line_start_offsets(&text);
        let mut measured_width: f32 = 0.0;
        let mut measured_height: f32 = 0.0;
        let mut lines = Vec::new();
        let mut glyphs = Vec::new();
        let glyph_collect_started = Instant::now();
        for (line_index, run) in buffer.layout_runs().enumerate() {
            if matches!(style.max_lines, Some(max_lines) if line_index >= max_lines) {
                break;
            }
            measured_width = measured_width.max(run.line_w);
            measured_height = measured_height.max(run.line_top + run.line_height);
            let x_offset = aligned_line_offset(style.align, width, run.line_w);
            let line_offset = line_starts.get(run.line_i).copied().unwrap_or(0);
            let glyph_start = glyphs.len();
            glyphs.extend(run.glyphs.iter().map(move |glyph| {
                let physical = glyph.physical((x_offset, run.line_y), 1.0);
                GlyphInstance {
                    position: Point::new(physical.x as f32, physical.y as f32),
                    advance: glyph.w,
                    // Use SENTINEL for default-colored glyphs so that the cache
                    // is color-independent.  Per-span colors (from glyph.color_opt)
                    // are stored directly since they are part of the shape key via
                    // the spans hash.
                    color: glyph.color_opt.map_or(Color::SENTINEL, color_from_cosmic),
                    cache_key: Some(physical.cache_key),
                    text_start: line_offset + glyph.start,
                    text_end: line_offset + glyph.end,
                }
            }));
            let glyph_end = glyphs.len();
            let text_start = run
                .glyphs
                .first()
                .map_or(line_offset, |glyph| line_offset + glyph.start);
            let text_end = run
                .glyphs
                .last()
                .map_or(text_start, |glyph| line_offset + glyph.end);
            lines.push(PreparedTextLine {
                rect: Rect::new(x_offset, run.line_top, run.line_w.max(0.0), run.line_height),
                text_start,
                text_end,
                glyph_start,
                glyph_end,
            });
        }
        self.frame_stats.glyph_collect_ms +=
            glyph_collect_started.elapsed().as_secs_f64() * 1_000.0;

        let layout = Arc::new(TextLayoutData {
            text: Arc::from(text),
            cache_key,
            lines,
            glyphs,
            size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
        });

        self.frame_stats.output_glyphs = self
            .frame_stats
            .output_glyphs
            .saturating_add(layout.glyphs.len() as u32);
        self.frame_stats.miss_ms += miss_started.elapsed().as_secs_f64() * 1_000.0;

        // Warn loudly when a single text node is large enough to cause frame drops.
        // At 16px, 10_000 glyphs ≈ 600 lines of text.  Use a virtual list instead.
        const GLYPH_COUNT_WARNING_THRESHOLD: usize = 10_000;
        if layout.glyphs.len() > GLYPH_COUNT_WARNING_THRESHOLD {
            tracing::warn!(
                target: "ruin_ui::text_perf",
                glyph_count = layout.glyphs.len(),
                shape_ms = miss_started.elapsed().as_secs_f64() * 1_000.0,
                "large text node shaped: consider splitting into a virtual list \
                 (each item a separate Element::text) so off-screen items can be \
                 culled and unchanged items skip reshaping"
            );
        }

        self.layout_cache.insert(cache_key, Arc::clone(&layout));
        layout
    }

    fn resolve_font_family(
        &mut self,
        family: &TextFontFamily,
        slant: TextSpanSlant,
    ) -> Option<String> {
        self.family_resolver
            .resolve(self.font_system.db(), family, slant)
    }
}

fn aligned_line_offset(align: TextAlign, width: Option<f32>, line_width: f32) -> f32 {
    let Some(width) = width else {
        return 0.0;
    };
    let remaining = (width - line_width).max(0.0);
    match align {
        TextAlign::Start => 0.0,
        TextAlign::Center => remaining * 0.5,
        TextAlign::End => remaining,
    }
}

fn combined_text(spans: &[TextSpan]) -> String {
    let total_len = spans.iter().map(|span| span.text.len()).sum();
    let mut text = String::with_capacity(total_len);
    for span in spans {
        text.push_str(&span.text);
    }
    text
}

fn line_start_offsets(text: &str) -> Vec<usize> {
    let mut starts = vec![0];
    let bytes = text.as_bytes();
    let mut index = 0;
    while index < bytes.len() {
        match bytes[index] {
            b'\n' => starts.push(index + 1),
            b'\r' => {
                if bytes.get(index + 1) == Some(&b'\n') {
                    starts.push(index + 2);
                    index += 1;
                } else {
                    starts.push(index + 1);
                }
            }
            _ => {}
        }
        index += 1;
    }
    starts
}

/// Cache key for shaped text.  `style.color` is intentionally excluded:
/// glyphs that use the default color are stored with `Color::SENTINEL`, making
/// the shape cache color-independent.  Per-span colors are part of `spans`.
///
/// Width is also excluded when it cannot affect the layout: no-wrap +
/// start-aligned text has the same glyph positions at any available width.
/// Omitting it prevents resize from invalidating these (often large) entries.
fn layout_cache_key(
    spans: &[TextSpan],
    style: &TextStyle,
    width: Option<f32>,
    _height: Option<f32>,
) -> u64 {
    let mut hasher = DefaultHasher::new();
    spans.hash(&mut hasher);
    style.font_size.to_bits().hash(&mut hasher);
    style.line_height.to_bits().hash(&mut hasher);
    // style.color intentionally omitted — see Color::SENTINEL
    style.font_family.hash(&mut hasher);
    style.wrap.hash(&mut hasher);
    style.align.hash(&mut hasher);
    style.max_lines.hash(&mut hasher);
    // Width is excluded for no-wrap + start-aligned text: glyph positions are
    // independent of container width, so the cached data is valid at any width.
    if width_affects_layout(style) {
        quantized_layout_dimension(width).hash(&mut hasher);
    }
    hasher.finish()
}

/// Returns `true` if the available width can change the shaped glyph positions.
///
/// For `TextWrap::None` + `TextAlign::Start`, text flows past any boundary
/// and lines are left-aligned, so width has no effect on the output.
#[inline]
fn width_affects_layout(style: &TextStyle) -> bool {
    style.wrap != TextWrap::None || style.align != TextAlign::Start
}

fn quantized_layout_dimension(value: Option<f32>) -> Option<u32> {
    const LAYOUT_CACHE_BUCKET_PX: f32 = 1.0;
    value.map(|value| {
        (value / LAYOUT_CACHE_BUCKET_PX)
            .round()
            .mul_add(LAYOUT_CACHE_BUCKET_PX, 0.0)
            .max(0.0)
            .to_bits()
    })
}

fn uses_plain_text_fast_path(spans: &[TextSpan]) -> bool {
    matches!(
        spans,
        [TextSpan {
            color: None,
            weight: TextSpanWeight::Normal,
            slant: TextSpanSlant::Normal,
            font_family: None,
            ..
        }]
    )
}

fn default_attrs_for_style<'a>(
    style: &'a TextStyle,
    resolved_family: Option<&'a str>,
) -> Attrs<'a> {
    // Do NOT pass style.color to cosmic-text — default-colored glyphs should have
    // color_opt = None so they are stored as Color::SENTINEL (color-independent cache).
    Attrs::new()
        .family(font_family_to_cosmic(&style.font_family, resolved_family))
        .metrics(Metrics::new(style.font_size, style.line_height))
}

fn attrs_for_span<'a>(
    span: &'a TextSpan,
    style: &'a TextStyle,
    resolved_family: Option<&'a str>,
) -> Attrs<'a> {
    let font_family = span.font_family.as_ref().unwrap_or(&style.font_family);
    let base = default_attrs_for_style(style, resolved_family)
        .family(font_family_to_cosmic(font_family, resolved_family));
    // Only set a color when the span explicitly overrides the default.  Spans that
    // inherit style.color leave color_opt = None, producing Color::SENTINEL in the cache.
    let base = if let Some(span_color) = span.color {
        base.color(to_cosmic_color(span_color))
    } else {
        base
    };
    base.weight(match span.weight {
        TextSpanWeight::Normal => CosmicWeight::NORMAL,
        TextSpanWeight::Medium => CosmicWeight::MEDIUM,
        TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
        TextSpanWeight::Bold => CosmicWeight::BOLD,
    })
    .style(match span.slant {
        TextSpanSlant::Normal => CosmicStyle::Normal,
        TextSpanSlant::Italic => CosmicStyle::Italic,
        TextSpanSlant::Oblique => CosmicStyle::Oblique,
    })
}

fn to_cosmic_color(color: Color) -> CosmicColor {
    CosmicColor::rgba(color.r, color.g, color.b, color.a)
}

fn color_from_cosmic(color: CosmicColor) -> Color {
    Color::rgba(color.r(), color.g(), color.b(), color.a())
}

fn font_family_to_cosmic<'a>(
    family: &'a TextFontFamily,
    resolved_family: Option<&'a str>,
) -> fontdb::Family<'a> {
    if let Some(resolved_family) = resolved_family {
        return fontdb::Family::Name(resolved_family);
    }

    match family {
        TextFontFamily::SansSerif => fontdb::Family::SansSerif,
        TextFontFamily::Serif => fontdb::Family::Serif,
        TextFontFamily::Monospace => fontdb::Family::Monospace,
        TextFontFamily::Cursive => fontdb::Family::Cursive,
        TextFontFamily::Fantasy => fontdb::Family::Fantasy,
        TextFontFamily::Named(name) => fontdb::Family::Name(name.as_str()),
    }
}

const SANS_SERIF_FAMILY_CANDIDATES: &[&str] = &[
    "Noto Sans",
    "DejaVu Sans",
    "Liberation Sans",
    "Ubuntu",
    "Cantarell",
    "Arial",
    "Open Sans",
];
const SERIF_FAMILY_CANDIDATES: &[&str] = &[
    "Noto Serif",
    "DejaVu Serif",
    "Liberation Serif",
    "Times New Roman",
    "Open Serif",
];
const MONOSPACE_FAMILY_CANDIDATES: &[&str] = &[
    "Noto Sans Mono",
    "DejaVu Sans Mono",
    "Liberation Mono",
    "Ubuntu Mono",
    "Courier New",
];

impl FontFamilyResolver {
    fn new() -> Self {
        Self {
            fontconfig: Fontconfig::new(),
            cache: HashMap::new(),
        }
    }

    fn resolve(
        &mut self,
        db: &fontdb::Database,
        family: &TextFontFamily,
        slant: TextSpanSlant,
    ) -> Option<String> {
        let key = (family.clone(), slant);
        if let Some(cached) = self.cache.get(&key) {
            return cached.clone();
        }

        let resolved = self.resolve_uncached(db, family, slant);
        self.cache.insert(key, resolved.clone());
        resolved
    }

    fn resolve_uncached(
        &self,
        db: &fontdb::Database,
        family: &TextFontFamily,
        slant: TextSpanSlant,
    ) -> Option<String> {
        if let Some(fontconfig) = self.fontconfig.as_ref()
            && let Some(font) =
                fontconfig.find(family.fontconfig_query_name(), fontconfig_style_name(slant))
        {
            if let Some(family_name) = family_name_for_font_match(db, &font) {
                return Some(family_name);
            }
            return Some(font.name);
        }

        fallback_family_name(db, family)
    }
}

fn configure_default_font_families(
    font_system: &mut FontSystem,
    family_resolver: &mut FontFamilyResolver,
) {
    if let Some(sans_family) = family_resolver.resolve(
        font_system.db(),
        &TextFontFamily::SansSerif,
        TextSpanSlant::Normal,
    ) {
        font_system.db_mut().set_sans_serif_family(&sans_family);
    }
    if let Some(serif_family) = family_resolver.resolve(
        font_system.db(),
        &TextFontFamily::Serif,
        TextSpanSlant::Normal,
    ) {
        font_system.db_mut().set_serif_family(&serif_family);
    }
    if let Some(monospace_family) = family_resolver.resolve(
        font_system.db(),
        &TextFontFamily::Monospace,
        TextSpanSlant::Normal,
    ) {
        font_system.db_mut().set_monospace_family(&monospace_family);
    }
}

fn fallback_family_name(db: &fontdb::Database, family: &TextFontFamily) -> Option<String> {
    match family {
        TextFontFamily::Named(name) => Some(name.clone()),
        TextFontFamily::SansSerif => {
            configured_generic_family_name(db, SANS_SERIF_FAMILY_CANDIDATES)
        }
        TextFontFamily::Serif => configured_generic_family_name(db, SERIF_FAMILY_CANDIDATES),
        TextFontFamily::Monospace => {
            configured_generic_family_name(db, MONOSPACE_FAMILY_CANDIDATES)
        }
        TextFontFamily::Cursive | TextFontFamily::Fantasy => db
            .faces()
            .find(|face| !face.monospaced && !face.post_script_name.contains("Emoji"))
            .and_then(|face| face.families.first().map(|(name, _)| name.clone())),
    }
}

fn configured_generic_family_name(
    db: &fontdb::Database,
    candidates: &[&'static str],
) -> Option<String> {
    if let Some(preferred) = preferred_family_name(
        db.faces()
            .flat_map(|face| face.families.iter().map(|(name, _)| name.as_str())),
        candidates,
    ) {
        return Some(preferred.to_owned());
    }

    db.faces()
        .find(|face| {
            !face.monospaced
                && !face.post_script_name.contains("Emoji")
                && face.style == fontdb::Style::Normal
                && face.weight == fontdb::Weight::NORMAL
        })
        .and_then(|face| face.families.first().map(|(name, _)| name.clone()))
}

fn family_name_for_font_match(db: &fontdb::Database, font: &fontconfig::Font) -> Option<String> {
    db.faces()
        .find(|face| match &face.source {
            fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => {
                path == &font.path
                    && font
                        .index
                        .is_none_or(|index| face.index == index.max(0) as u32)
            }
            fontdb::Source::Binary(_) => false,
        })
        .and_then(|face| face.families.first().map(|(name, _)| name.clone()))
}

fn fontconfig_style_name(slant: TextSpanSlant) -> Option<&'static str> {
    match slant {
        TextSpanSlant::Normal => None,
        TextSpanSlant::Italic => Some("Italic"),
        TextSpanSlant::Oblique => Some("Oblique"),
    }
}

fn preferred_family_name<'a>(
    available_families: impl IntoIterator<Item = &'a str>,
    candidates: &[&'static str],
) -> Option<&'static str> {
    let available: BTreeSet<&str> = available_families.into_iter().collect();
    candidates
        .iter()
        .copied()
        .find(|candidate| available.contains(candidate))
}

impl TextFontFamily {
    fn fontconfig_query_name(&self) -> &str {
        match self {
            TextFontFamily::SansSerif => "sans-serif",
            TextFontFamily::Serif => "serif",
            TextFontFamily::Monospace => "monospace",
            TextFontFamily::Cursive => "cursive",
            TextFontFamily::Fantasy => "fantasy",
            TextFontFamily::Named(name) => name.as_str(),
        }
    }
}

// ---------------------------------------------------------------------------
// Hash implementations for f32-containing types.

impl std::hash::Hash for TextSelectionStyle {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.highlight_color.hash(state);
        self.text_color.hash(state);
    }
}

impl std::hash::Hash for TextStyle {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.font_size.to_bits().hash(state);
        self.line_height.to_bits().hash(state);
        self.color.hash(state);
        self.font_family.hash(state);
        self.bounds
            .map(|b| (b.width.to_bits(), b.height.to_bits()))
            .hash(state);
        self.wrap.hash(state);
        self.align.hash(state);
        self.max_lines.hash(state);
        self.selectable.hash(state);
        self.selection_style.hash(state);
    }
}

#[cfg(test)]
mod tests {
    use super::{
        MONOSPACE_FAMILY_CANDIDATES, TextAlign, TextFontFamily, TextSpan, TextSpanSlant,
        TextSpanWeight, TextStyle, TextSystem, TextWrap, configured_generic_family_name,
        family_name_for_font_match, font_family_to_cosmic, fontdb, preferred_family_name,
    };
    use crate::{Color, Point, UiSize};
    use fontconfig::Font;
    use std::path::PathBuf;

    #[test]
    fn max_lines_limits_measured_height() {
        let mut text_system = TextSystem::new();
        let style = TextStyle::new(16.0, Color::rgb(0xFF, 0xFF, 0xFF))
            .with_line_height(20.0)
            .with_wrap(TextWrap::Word);
        let unclamped = text_system.measure(
            "alpha beta gamma delta epsilon zeta eta theta iota kappa",
            &style,
            Some(120.0),
            None,
        );
        let clamped_style = style.with_max_lines(2);
        let clamped = text_system.measure(
            "alpha beta gamma delta epsilon zeta eta theta iota kappa",
            &clamped_style,
            Some(120.0),
            None,
        );
        assert!(unclamped.height > clamped.height);
        assert!(clamped.height <= 40.0);
    }

    #[test]
    fn centered_text_shifts_glyph_positions_within_bounds() {
        let mut text_system = TextSystem::new();
        let origin = Point::new(0.0, 0.0);
        let start = text_system.prepare(
            "title",
            origin,
            &TextStyle::new(20.0, Color::rgb(0xFF, 0xFF, 0xFF))
                .with_bounds(UiSize::new(200.0, 40.0)),
        );
        let centered = text_system.prepare(
            "title",
            origin,
            &TextStyle::new(20.0, Color::rgb(0xFF, 0xFF, 0xFF))
                .with_bounds(UiSize::new(200.0, 40.0))
                .with_align(TextAlign::Center),
        );
        assert!(!start.glyphs.is_empty());
        assert!(!centered.glyphs.is_empty());
        assert!(centered.glyphs[0].position.x > start.glyphs[0].position.x);
    }

    #[test]
    fn rich_spans_preserve_per_glyph_colors() {
        let mut text_system = TextSystem::new();
        let spans = [
            TextSpan::new("Red ").color(Color::rgb(0xFF, 0x33, 0x33)),
            TextSpan::new("Blue")
                .color(Color::rgb(0x33, 0x66, 0xFF))
                .weight(TextSpanWeight::Bold)
                .slant(TextSpanSlant::Italic),
        ];
        let style = TextStyle::new(18.0, Color::rgb(0xEE, 0xEE, 0xEE))
            .with_wrap(TextWrap::Word)
            .with_bounds(UiSize::new(240.0, 80.0));
        let prepared =
            text_system.prepare_spans(&spans, Point::new(0.0, 0.0), &style, style.bounds);

        assert!(
            prepared
                .glyphs
                .iter()
                .any(|glyph| glyph.color == Color::rgb(0xFF, 0x33, 0x33))
        );
        assert!(
            prepared
                .glyphs
                .iter()
                .any(|glyph| glyph.color == Color::rgb(0x33, 0x66, 0xFF))
        );
    }

    /// Same text at two different default colors must produce identical shaped glyph data
    /// (same positions, same cache keys, SENTINEL colors) since color is not part of
    /// the shape cache key.
    #[test]
    fn same_text_different_default_color_shares_layout_data() {
        let mut text_system = TextSystem::new();
        let style_red = TextStyle::new(16.0, Color::rgb(0xFF, 0x00, 0x00));
        let style_blue = TextStyle::new(16.0, Color::rgb(0x00, 0x00, 0xFF));
        let origin = Point::new(0.0, 0.0);
        let red = text_system.prepare("hello", origin, &style_red);
        let blue = text_system.prepare("hello", origin, &style_blue);
        // Both PreparedTexts must have identical glyph shapes and sentinel colors —
        // the second call hits the shape cache and produces the same layout data.
        assert_eq!(
            red.glyphs.len(),
            blue.glyphs.len(),
            "glyph count must match"
        );
        for (r, b) in red.glyphs.iter().zip(blue.glyphs.iter()) {
            assert_eq!(r.position, b.position, "glyph positions must match");
            assert_eq!(r.cache_key, b.cache_key, "glyph cache keys must match");
            assert_eq!(
                r.color,
                Color::SENTINEL,
                "default-color glyphs must use SENTINEL"
            );
            assert_eq!(
                b.color,
                Color::SENTINEL,
                "default-color glyphs must use SENTINEL"
            );
        }
        // PreparedText::clone() is an Arc clone — O(1).
        let cloned = red.clone();
        assert_eq!(
            cloned.layout_ptr(),
            red.layout_ptr(),
            "clone must share the same Arc"
        );
        // But they should carry different default colors.
        assert_eq!(red.default_color, Color::rgb(0xFF, 0x00, 0x00));
        assert_eq!(blue.default_color, Color::rgb(0x00, 0x00, 0xFF));
    }

    #[test]
    fn preferred_family_name_uses_first_available_candidate() {
        let selected = preferred_family_name(
            ["DejaVu Serif", "Liberation Sans", "Noto Sans Mono"],
            &["Noto Sans", "Liberation Sans", "Arial"],
        );
        assert_eq!(selected, Some("Liberation Sans"));
    }

    #[test]
    fn configured_generic_family_name_falls_back_to_first_available_normal_face() {
        let mut db = fontdb::Database::new();
        db.push_face_info(fontdb::FaceInfo {
            id: fontdb::ID::dummy(),
            source: fontdb::Source::Binary(std::sync::Arc::new(Vec::new())),
            index: 0,
            families: vec![(
                String::from("Fallback Sans"),
                fontdb::Language::English_UnitedStates,
            )],
            post_script_name: String::from("FallbackSans-Regular"),
            style: fontdb::Style::Normal,
            weight: fontdb::Weight::NORMAL,
            stretch: fontdb::Stretch::Normal,
            monospaced: false,
        });

        assert_eq!(
            configured_generic_family_name(&db, MONOSPACE_FAMILY_CANDIDATES),
            Some(String::from("Fallback Sans"))
        );
    }

    #[test]
    fn font_family_to_cosmic_prefers_resolved_name() {
        match font_family_to_cosmic(&TextFontFamily::SansSerif, Some("Resolved Sans")) {
            fontdb::Family::Name(name) => assert_eq!(name, "Resolved Sans"),
            other => panic!("expected resolved named family, got {other:?}"),
        }
    }

    #[test]
    fn family_name_for_font_match_uses_path_and_index() {
        let mut db = fontdb::Database::new();
        db.push_face_info(fontdb::FaceInfo {
            id: fontdb::ID::dummy(),
            source: fontdb::Source::File(PathBuf::from("/tmp/example-font.ttf")),
            index: 2,
            families: vec![(
                String::from("Example Sans"),
                fontdb::Language::English_UnitedStates,
            )],
            post_script_name: String::from("ExampleSans-Italic"),
            style: fontdb::Style::Italic,
            weight: fontdb::Weight::NORMAL,
            stretch: fontdb::Stretch::Normal,
            monospaced: false,
        });

        let font = Font {
            name: String::from("Example Sans"),
            path: PathBuf::from("/tmp/example-font.ttf"),
            index: Some(2),
        };

        assert_eq!(
            family_name_for_font_match(&db, &font),
            Some(String::from("Example Sans"))
        );
    }
}
