use bone_document::{Sketch, SketchEntityKind, SketchRelation, SketchStatusReport};
use bone_types::{SketchDimensionId, SketchEntityId, SketchItemId, SketchRelationId, SketchStatus};
use bone_ui::frame::FrameCtx;
use bone_ui::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
use bone_ui::strings::{StringKey, StringTable};
use bone_ui::theme::{CadColors, Color};
use bone_ui::widgets::{
    LabelText, Panel, PanelState, PanelTitlebar, PanelVariant, WidgetPaint, show_panel,
};
use bone_ui::{WidgetId, WidgetKey};

use crate::strings;

const STATUS_PANEL_WIDTH_PX: f32 = 280.0;
const STATUS_PANEL_TITLE_HEIGHT_PX: f32 = 28.0;
const STATUS_PANEL_ROW_HEIGHT_PX: f32 = 22.0;
const STATUS_PANEL_PADDING_PX: f32 = 8.0;
const STATUS_PANEL_MAX_ROWS: usize = 12;
const STATUS_PANEL_GAP_PX: f32 = 6.0;

#[must_use]
pub fn status_label_key(status: SketchStatus) -> StringKey {
    match status {
        SketchStatus::UnderDefined => strings::STATUS_SKETCH_UNDER_DEFINED,
        SketchStatus::FullyDefined => strings::STATUS_SKETCH_FULLY_DEFINED,
        SketchStatus::OverDefined => strings::STATUS_SKETCH_OVER_DEFINED,
        SketchStatus::NoSolutionFound => strings::STATUS_SKETCH_NO_SOLUTION,
        SketchStatus::InvalidSolutionFound => strings::STATUS_SKETCH_INVALID,
        SketchStatus::Dangling => strings::STATUS_SKETCH_DANGLING,
    }
}

#[must_use]
pub fn status_color(status: SketchStatus, cad: &CadColors) -> Color {
    match status {
        SketchStatus::UnderDefined => cad.sketch_under_defined,
        SketchStatus::FullyDefined => cad.sketch_fully_defined,
        SketchStatus::OverDefined
        | SketchStatus::NoSolutionFound
        | SketchStatus::InvalidSolutionFound => cad.sketch_over_defined,
        SketchStatus::Dangling => cad.sketch_dangling,
    }
}

#[must_use]
pub fn status_panel_rect(status_bar: LayoutRect, row_count: usize) -> LayoutRect {
    let rows = row_count.clamp(1, STATUS_PANEL_MAX_ROWS);
    #[allow(
        clippy::cast_precision_loss,
        reason = "row count is bounded by STATUS_PANEL_MAX_ROWS"
    )]
    let body_height = rows as f32 * STATUS_PANEL_ROW_HEIGHT_PX + 2.0 * STATUS_PANEL_PADDING_PX;
    let panel_height = STATUS_PANEL_TITLE_HEIGHT_PX + body_height;
    let panel_width = STATUS_PANEL_WIDTH_PX;
    let bar_right = status_bar.origin.x.value() + status_bar.size.width.value();
    let origin_x = (bar_right - panel_width).max(status_bar.origin.x.value());
    let origin_y = status_bar.origin.y.value() - panel_height - STATUS_PANEL_GAP_PX;
    LayoutRect::new(
        LayoutPos::new(LayoutPx::new(origin_x), LayoutPx::new(origin_y.max(0.0))),
        LayoutSize::new(LayoutPx::new(panel_width), LayoutPx::new(panel_height)),
    )
}

pub fn render_status_panel(
    ctx: &mut FrameCtx<'_>,
    panel_id: WidgetId,
    panel_state: &mut PanelState,
    status_bar_rect: LayoutRect,
    report: &SketchStatusReport,
    sketch: &Sketch,
    paints: &mut Vec<WidgetPaint>,
) {
    let lines = compose_panel_lines(report, sketch, ctx.strings);
    let rect = status_panel_rect(status_bar_rect, lines.len());
    let response = show_panel(
        ctx,
        Panel::new(panel_id, rect, panel_state)
            .variant(PanelVariant::Card)
            .titlebar(PanelTitlebar {
                label: strings::STATUS_PANEL_TITLE,
                height: LayoutPx::new(STATUS_PANEL_TITLE_HEIGHT_PX),
                collapsible: false,
            }),
    );
    paints.extend(response.paint);
    let Some(body) = response.body_rect else {
        return;
    };
    lines.into_iter().enumerate().for_each(|(i, line)| {
        #[allow(
            clippy::cast_precision_loss,
            reason = "row index bounded by STATUS_PANEL_MAX_ROWS"
        )]
        let y = body.origin.y.value()
            + STATUS_PANEL_PADDING_PX
            + (i as f32) * STATUS_PANEL_ROW_HEIGHT_PX;
        paints.push(WidgetPaint::Label {
            rect: LayoutRect::new(
                LayoutPos::new(
                    LayoutPx::new(body.origin.x.value() + STATUS_PANEL_PADDING_PX),
                    LayoutPx::new(y),
                ),
                LayoutSize::new(
                    LayoutPx::saturating_nonneg(
                        body.size.width.value() - 2.0 * STATUS_PANEL_PADDING_PX,
                    ),
                    LayoutPx::new(STATUS_PANEL_ROW_HEIGHT_PX),
                ),
            ),
            text: line,
            color: ctx.theme().colors.text_primary(),
            role: ctx.theme().typography.body,
        });
    });
}

fn compose_panel_lines(
    report: &SketchStatusReport,
    sketch: &Sketch,
    strings_table: &StringTable,
) -> Vec<LabelText> {
    let total = report.offending().len();
    if total == 0 {
        return vec![LabelText::Key(strings::STATUS_PANEL_EMPTY)];
    }
    if total <= STATUS_PANEL_MAX_ROWS {
        return report
            .offending()
            .iter()
            .map(|item| offending_label(*item, sketch, strings_table))
            .collect();
    }
    let visible = STATUS_PANEL_MAX_ROWS - 1;
    let rest = total - visible;
    let head = report
        .offending()
        .iter()
        .take(visible)
        .map(|item| offending_label(*item, sketch, strings_table));
    let more_template = strings_table.resolve(strings::STATUS_PANEL_MORE);
    let more_line = LabelText::Owned(more_template.replace("{n}", &rest.to_string()));
    head.chain(core::iter::once(more_line)).collect()
}

fn offending_label(item: SketchItemId, sketch: &Sketch, strings_table: &StringTable) -> LabelText {
    let (kind_key, ordinal) = match item {
        SketchItemId::Relation(id) => (
            sketch
                .relations()
                .get(id)
                .map(|rel| relation_label_key(*rel)),
            relation_ordinal(sketch, id),
        ),
        SketchItemId::Dimension(id) => (
            sketch
                .dimensions()
                .get(id)
                .map(|dim| dimension_label_key(*dim)),
            dimension_ordinal(sketch, id),
        ),
        SketchItemId::Entity(id) => (
            sketch
                .entities()
                .get(id)
                .map(|entity| entity_label_key(entity.kind())),
            entity_ordinal(sketch, id),
        ),
    };
    let kind_str = strings_table.resolve(kind_key.unwrap_or(strings::STATUS_PANEL_KIND_UNKNOWN));
    match ordinal {
        Some(o) => LabelText::Owned(format!("{kind_str} #{}", o + 1)),
        None => LabelText::Owned(kind_str.to_owned()),
    }
}

fn relation_ordinal(sketch: &Sketch, id: SketchRelationId) -> Option<usize> {
    sketch.relation_order().iter().position(|x| *x == id)
}

fn dimension_ordinal(sketch: &Sketch, id: SketchDimensionId) -> Option<usize> {
    sketch.dimension_order().iter().position(|x| *x == id)
}

fn entity_ordinal(sketch: &Sketch, id: SketchEntityId) -> Option<usize> {
    sketch.entity_order().iter().position(|x| *x == id)
}

fn relation_label_key(rel: SketchRelation) -> StringKey {
    match rel {
        SketchRelation::Coincident(_, _) => strings::TOOL_COINCIDENT,
        SketchRelation::Horizontal(_) => strings::TOOL_HORIZONTAL,
        SketchRelation::Vertical(_) => strings::TOOL_VERTICAL,
        SketchRelation::Parallel(_, _) => strings::TOOL_PARALLEL,
        SketchRelation::Perpendicular(_, _) => strings::TOOL_PERPENDICULAR,
        SketchRelation::Tangent(_, _) => strings::TOOL_TANGENT,
        SketchRelation::Equal(_, _) => strings::TOOL_EQUAL,
        SketchRelation::Concentric(_, _) => strings::TOOL_CONCENTRIC,
        SketchRelation::Midpoint { .. } => strings::TOOL_MIDPOINT,
        SketchRelation::Symmetric { .. } => strings::TOOL_SYMMETRIC,
        SketchRelation::Fix(_) => strings::TOOL_FIX,
    }
}

fn dimension_label_key(dim: bone_document::SketchDimension) -> StringKey {
    use bone_document::SketchDimension as Dim;
    match dim {
        Dim::Linear { .. } => strings::STATUS_PANEL_KIND_LINEAR,
        Dim::Angular { .. } => strings::STATUS_PANEL_KIND_ANGULAR,
        Dim::Radius { .. } => strings::STATUS_PANEL_KIND_RADIUS,
        Dim::Diameter { .. } => strings::STATUS_PANEL_KIND_DIAMETER,
    }
}

fn entity_label_key(kind: SketchEntityKind) -> StringKey {
    match kind {
        SketchEntityKind::Point => strings::STATUS_PANEL_KIND_POINT,
        SketchEntityKind::Line => strings::STATUS_PANEL_KIND_LINE,
        SketchEntityKind::Arc => strings::STATUS_PANEL_KIND_ARC,
        SketchEntityKind::Circle => strings::STATUS_PANEL_KIND_CIRCLE,
    }
}

#[must_use]
pub fn status_badge_widget_id(parent: WidgetId) -> WidgetId {
    parent.child(WidgetKey::new("status.badge"))
}

#[must_use]
pub fn status_panel_widget_id(parent: WidgetId) -> WidgetId {
    parent.child(WidgetKey::new("status.panel"))
}

#[cfg(test)]
mod tests {
    use super::{SketchStatus, status_color, status_label_key};
    use crate::strings;
    use bone_ui::theme::Theme;

    #[test]
    fn each_status_maps_to_expected_label_key() {
        assert_eq!(
            status_label_key(SketchStatus::UnderDefined),
            strings::STATUS_SKETCH_UNDER_DEFINED
        );
        assert_eq!(
            status_label_key(SketchStatus::FullyDefined),
            strings::STATUS_SKETCH_FULLY_DEFINED
        );
        assert_eq!(
            status_label_key(SketchStatus::OverDefined),
            strings::STATUS_SKETCH_OVER_DEFINED
        );
        assert_eq!(
            status_label_key(SketchStatus::NoSolutionFound),
            strings::STATUS_SKETCH_NO_SOLUTION
        );
        assert_eq!(
            status_label_key(SketchStatus::InvalidSolutionFound),
            strings::STATUS_SKETCH_INVALID
        );
        assert_eq!(
            status_label_key(SketchStatus::Dangling),
            strings::STATUS_SKETCH_DANGLING
        );
    }

    #[test]
    fn over_no_solution_and_invalid_share_red_token() {
        let cad = Theme::light().cad;
        assert_eq!(
            status_color(SketchStatus::OverDefined, &cad),
            cad.sketch_over_defined
        );
        assert_eq!(
            status_color(SketchStatus::NoSolutionFound, &cad),
            cad.sketch_over_defined
        );
        assert_eq!(
            status_color(SketchStatus::InvalidSolutionFound, &cad),
            cad.sketch_over_defined
        );
        assert_eq!(
            status_color(SketchStatus::Dangling, &cad),
            cad.sketch_dangling
        );
        assert_eq!(
            status_color(SketchStatus::UnderDefined, &cad),
            cad.sketch_under_defined
        );
        assert_eq!(
            status_color(SketchStatus::FullyDefined, &cad),
            cad.sketch_fully_defined
        );
    }
}
