use serde::Serialize;

use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use crate::strings::{StringKey, StringTable};
use crate::theme::{
    Border, Color, ElevationLevel, Radius, Spacing, StrokeWidth, Theme, TypographyRole,
};
use crate::widget_id::WidgetId;

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum ButtonPaintKind {
    Filled,
    Outlined,
    Ghost,
    Danger,
    IconOnly,
}

#[derive(Copy, Clone, Debug, PartialEq, Eq, Serialize)]
pub enum GlyphMark {
    Checkmark,
    Indeterminate,
    RadioDot,
    Caret,
    Chevron,
    SliderThumb,
    Spinner,
    Close,
    DisclosureClosed,
    DisclosureOpen,
    SubmenuArrow,
    SortAscending,
    SortDescending,
    Ellipsis,
    TreeFeature,
    TreePlane,
    TreeSketch,
    TabTree,
    TabProperties,
    TabConfiguration,
    TabDimensionExpert,
    TabDisplay,
}

impl GlyphMark {
    #[must_use]
    pub const fn glyph(self) -> &'static str {
        match self {
            Self::Checkmark => "\u{2713}",
            Self::Indeterminate => "\u{2212}",
            Self::RadioDot | Self::SliderThumb => "\u{25CF}",
            Self::Caret | Self::Chevron | Self::DisclosureOpen | Self::SortDescending => "\u{25BC}",
            Self::DisclosureClosed | Self::SubmenuArrow => "\u{25B6}",
            Self::SortAscending => "\u{25B2}",
            Self::Close => "\u{00D7}",
            Self::Spinner => "\u{25D0}",
            Self::Ellipsis => "\u{2026}",
            Self::TreeFeature => "\u{25CB}",
            Self::TreePlane => "\u{25C7}",
            Self::TreeSketch => "\u{25A1}",
            Self::TabTree => "\u{25A6}",
            Self::TabProperties => "\u{25A4}",
            Self::TabConfiguration => "\u{25A5}",
            Self::TabDimensionExpert => "\u{25C8}",
            Self::TabDisplay => "\u{25D1}",
        }
    }
}

#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub enum LabelText {
    Key(StringKey),
    Owned(String),
}

impl LabelText {
    #[must_use]
    pub fn resolve<'a>(&'a self, strings: &'a StringTable) -> &'a str {
        match self {
            Self::Key(key) => strings.resolve(*key),
            Self::Owned(s) => s.as_str(),
        }
    }
}

impl From<StringKey> for LabelText {
    fn from(key: StringKey) -> Self {
        Self::Key(key)
    }
}

impl From<String> for LabelText {
    fn from(value: String) -> Self {
        Self::Owned(value)
    }
}

impl From<&str> for LabelText {
    fn from(value: &str) -> Self {
        Self::Owned(value.to_owned())
    }
}

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize)]
pub enum HorizontalAlign {
    #[default]
    Center,
    Start,
    End,
}

#[derive(Clone, Debug, PartialEq, Serialize)]
pub enum WidgetPaint {
    Surface {
        rect: LayoutRect,
        fill: Color,
        border: Option<Border>,
        radius: Radius,
        elevation: Option<ElevationLevel>,
    },
    Label {
        rect: LayoutRect,
        text: LabelText,
        color: Color,
        role: TypographyRole,
    },
    AlignedLabel {
        rect: LayoutRect,
        text: LabelText,
        color: Color,
        role: TypographyRole,
        align: HorizontalAlign,
    },
    Mark {
        rect: LayoutRect,
        kind: GlyphMark,
        color: Color,
    },
    FocusRing {
        rect: LayoutRect,
        color: Color,
        radius: Radius,
        thickness: Spacing,
    },
    SelectionHighlight {
        rect: LayoutRect,
        color: Color,
    },
    Caret {
        rect: LayoutRect,
        color: Color,
    },
    Tooltip {
        rect: LayoutRect,
        text: LabelText,
        anchor: WidgetId,
        elevation: ElevationLevel,
    },
}

#[derive(Copy, Clone, Debug, PartialEq)]
pub struct PaintPrim {
    pub rect: LayoutRect,
    pub fill: Color,
    pub border: Option<Border>,
    pub radius: Radius,
}

impl PaintPrim {
    const fn solid(rect: LayoutRect, fill: Color) -> Self {
        Self {
            rect,
            fill,
            border: None,
            radius: Radius::px(0.0),
        }
    }
}

const LABEL_PLACEHOLDER_ALPHA: f32 = 0.55;
const SELECTION_PLACEHOLDER_ALPHA: f32 = 0.35;
const MARK_PLACEHOLDER_FACTOR: f32 = 0.55;

#[must_use]
pub fn lower_paint(theme: &Theme, paint: &WidgetPaint) -> PaintPrim {
    match paint {
        WidgetPaint::Surface {
            rect,
            fill,
            border,
            radius,
            ..
        } => PaintPrim {
            rect: *rect,
            fill: *fill,
            border: *border,
            radius: *radius,
        },
        WidgetPaint::Label { rect, color, .. } | WidgetPaint::AlignedLabel { rect, color, .. } => {
            PaintPrim::solid(
                label_placeholder_bar(*rect),
                color.with_alpha(LABEL_PLACEHOLDER_ALPHA * color.alpha()),
            )
        }
        WidgetPaint::Mark { rect, color, .. } => {
            PaintPrim::solid(centered_square(*rect, MARK_PLACEHOLDER_FACTOR), *color)
        }
        WidgetPaint::FocusRing {
            rect,
            color,
            radius,
            thickness,
        } => PaintPrim {
            rect: *rect,
            fill: Color::TRANSPARENT,
            border: Some(Border {
                color: *color,
                width: StrokeWidth::px(thickness.value_px()),
            }),
            radius: *radius,
        },
        WidgetPaint::SelectionHighlight { rect, color } => PaintPrim::solid(
            *rect,
            color.with_alpha(SELECTION_PLACEHOLDER_ALPHA * color.alpha()),
        ),
        WidgetPaint::Caret { rect, color } => PaintPrim::solid(*rect, *color),
        WidgetPaint::Tooltip {
            rect, elevation, ..
        } => PaintPrim {
            rect: *rect,
            fill: theme.colors.surface(elevation.surface),
            border: elevation.border,
            radius: Radius::px(0.0),
        },
    }
}

fn label_placeholder_bar(rect: LayoutRect) -> LayoutRect {
    let height = (rect.size.height.value() * 0.4).max(2.0);
    let pad = (rect.size.height.value() - height) * 0.5;
    let inset_x = (rect.size.width.value() * 0.1).min(8.0);
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::saturating(rect.origin.x.value() + inset_x),
            LayoutPx::saturating(rect.origin.y.value() + pad),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(rect.size.width.value() - 2.0 * inset_x),
            LayoutPx::saturating_nonneg(height),
        ),
    )
}

const LABEL_AVG_ADVANCE_RATIO: f32 = 0.6;

#[must_use]
pub fn estimate_label_width_px(text: &str, font_size_px: f32, horizontal_padding_px: f32) -> f32 {
    #[allow(
        clippy::cast_precision_loss,
        reason = "string lengths fit in f32 mantissa for any realistic label"
    )]
    let chars = text.chars().count() as f32;
    chars * font_size_px * LABEL_AVG_ADVANCE_RATIO + 2.0 * horizontal_padding_px
}

fn centered_square(rect: LayoutRect, factor: f32) -> LayoutRect {
    let side = rect
        .size
        .width
        .value()
        .min(rect.size.height.value())
        .max(0.0)
        * factor;
    let cx = rect.origin.x.value() + 0.5 * rect.size.width.value();
    let cy = rect.origin.y.value() + 0.5 * rect.size.height.value();
    LayoutRect::new(
        LayoutPos::new(
            LayoutPx::saturating(cx - 0.5 * side),
            LayoutPx::saturating(cy - 0.5 * side),
        ),
        LayoutSize::new(
            LayoutPx::saturating_nonneg(side),
            LayoutPx::saturating_nonneg(side),
        ),
    )
}

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

    #[test]
    fn every_mark_maps_to_a_non_empty_bmp_glyph() {
        let cases = [
            GlyphMark::Checkmark,
            GlyphMark::Indeterminate,
            GlyphMark::RadioDot,
            GlyphMark::Caret,
            GlyphMark::Chevron,
            GlyphMark::SliderThumb,
            GlyphMark::Spinner,
            GlyphMark::Close,
            GlyphMark::DisclosureClosed,
            GlyphMark::DisclosureOpen,
            GlyphMark::SubmenuArrow,
            GlyphMark::SortAscending,
            GlyphMark::SortDescending,
            GlyphMark::Ellipsis,
        ];
        cases.into_iter().for_each(|kind| {
            let glyph = kind.glyph();
            assert!(!glyph.is_empty(), "{kind:?} → empty glyph");
            assert_eq!(
                glyph.chars().count(),
                1,
                "{kind:?} → expected single codepoint",
            );
        });
    }

    #[test]
    fn disclosure_open_and_closed_pick_distinct_glyphs() {
        assert_ne!(
            GlyphMark::DisclosureOpen.glyph(),
            GlyphMark::DisclosureClosed.glyph(),
        );
    }

    #[test]
    fn sort_directions_pick_distinct_glyphs() {
        assert_ne!(
            GlyphMark::SortAscending.glyph(),
            GlyphMark::SortDescending.glyph(),
        );
    }
}
