use bone_document::{
    BrepError, ExtrudeError, ProfileDefect, Sketch, SketchEntityKind, SketchRelation,
    SketchStatusReport, TruckGap,
};
use bone_types::{
    BrepSlot, RebuildError, 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,
    }
}

#[derive(Copy, Clone, Debug)]
pub enum ExtrudeStatus<'a> {
    Valid,
    Failed(&'a ExtrudeError),
}

impl<'a> ExtrudeStatus<'a> {
    #[must_use]
    pub fn error(self) -> Option<&'a ExtrudeError> {
        match self {
            Self::Valid => None,
            Self::Failed(error) => Some(error),
        }
    }
}

#[must_use]
pub fn extrude_badge_style(status: ExtrudeStatus<'_>, cad: &CadColors) -> (StringKey, Color) {
    match status {
        ExtrudeStatus::Valid => (strings::STATUS_EXTRUDE_VALID, cad.sketch_fully_defined),
        ExtrudeStatus::Failed(
            ExtrudeError::Kernel(BrepError::DanglingEdge { .. } | BrepError::DanglingVertex { .. })
            | ExtrudeError::PlaneUnresolved(RebuildError::DanglingReference(_)),
        ) => (strings::STATUS_EXTRUDE_DANGLING, cad.sketch_dangling),
        ExtrudeStatus::Failed(_) => (strings::STATUS_EXTRUDE_INVALID, cad.sketch_invalid),
    }
}

fn extrude_panel_line(error: &ExtrudeError, strings_table: &StringTable) -> LabelText {
    match error {
        ExtrudeError::UnsolvedSketch(_) => LabelText::Key(strings::EXTRUDE_PANEL_UNSOLVED_SKETCH),
        ExtrudeError::Kernel(kernel) => kernel_panel_line(kernel, strings_table),
        ExtrudeError::PlaneUnresolved(rebuild) => LabelText::Key(rebuild_error_key(*rebuild)),
    }
}

fn rebuild_error_key(error: RebuildError) -> StringKey {
    match error {
        RebuildError::DanglingReference(_) => strings::EXTRUDE_PANEL_DANGLING_REFERENCE,
        RebuildError::NonPlanarSketchTarget => strings::EXTRUDE_PANEL_NON_PLANAR_TARGET,
        RebuildError::UpstreamUnresolved => strings::EXTRUDE_PANEL_UPSTREAM_UNRESOLVED,
        RebuildError::Build(_) => strings::EXTRUDE_PANEL_INTERNAL,
    }
}

fn kernel_panel_line(error: &BrepError, strings_table: &StringTable) -> LabelText {
    match error {
        BrepError::InvalidProfile { reason } => LabelText::Key(profile_defect_key(*reason)),
        BrepError::EmptyExtrudeDepth => LabelText::Key(strings::EXTRUDE_PANEL_EMPTY_DEPTH),
        BrepError::ShellNotClosed => LabelText::Key(strings::EXTRUDE_PANEL_SHELL_OPEN),
        BrepError::DegenerateEdge { edge } => brep_entity_line(
            strings::EXTRUDE_PANEL_DEGENERATE_EDGE,
            strings_table,
            edge.slot(),
        ),
        BrepError::DanglingEdge { edge } => brep_entity_line(
            strings::EXTRUDE_PANEL_DANGLING_EDGE,
            strings_table,
            edge.slot(),
        ),
        BrepError::DanglingVertex { vertex } => brep_entity_line(
            strings::EXTRUDE_PANEL_DANGLING_VERTEX,
            strings_table,
            vertex.slot(),
        ),
        BrepError::TruckUnsupported { detail } => LabelText::Key(truck_gap_key(*detail)),
        BrepError::MissingLabel { .. }
        | BrepError::ReattachMismatch { .. }
        | BrepError::ReattachOrder
        | BrepError::BlobSerialize
        | BrepError::BlobParse
        | BrepError::StepSyntax
        | BrepError::StepNoData
        | BrepError::StepShellMalformed
        | BrepError::StepEmpty
        | BrepError::StepMultipleSolids { .. }
        | BrepError::StepUnsupported { .. }
        | BrepError::Canceled => LabelText::Key(strings::EXTRUDE_PANEL_INTERNAL),
    }
}

fn profile_defect_key(defect: ProfileDefect) -> StringKey {
    match defect {
        ProfileDefect::OpenLoop => strings::EXTRUDE_PANEL_PROFILE_OPEN_LOOP,
        ProfileDefect::BranchingVertex => strings::EXTRUDE_PANEL_PROFILE_BRANCHING,
        ProfileDefect::SelfIntersectingLoop => strings::EXTRUDE_PANEL_PROFILE_SELF_INTERSECTING,
        ProfileDefect::ZeroArea => strings::EXTRUDE_PANEL_PROFILE_ZERO_AREA,
        ProfileDefect::UncontainedLoop => strings::EXTRUDE_PANEL_PROFILE_UNCONTAINED,
        ProfileDefect::OverlappingLoops => strings::EXTRUDE_PANEL_PROFILE_OVERLAPPING,
    }
}

fn truck_gap_key(gap: TruckGap) -> StringKey {
    match gap {
        TruckGap::ReverseNormal | TruckGap::AxisDirection | TruckGap::ReferenceDirection => {
            strings::EXTRUDE_PANEL_UNSUPPORTED_DIRECTION
        }
        TruckGap::ThroughAll
        | TruckGap::UpToNext
        | TruckGap::UpToVertex
        | TruckGap::UpToSurface
        | TruckGap::OffsetFromSurface
        | TruckGap::UpToBody => strings::EXTRUDE_PANEL_UNSUPPORTED_END,
        TruckGap::Draft => strings::EXTRUDE_PANEL_UNSUPPORTED_DRAFT,
        TruckGap::ThinWall => strings::EXTRUDE_PANEL_UNSUPPORTED_THIN,
    }
}

fn brep_entity_line(key: StringKey, strings_table: &StringTable, slot: BrepSlot) -> LabelText {
    LabelText::Owned(strings_table.resolve(key).replace("{n}", &slot.to_string()))
}

#[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);
    render_diagnostics_panel(
        ctx,
        panel_id,
        panel_state,
        status_bar_rect,
        strings::STATUS_PANEL_TITLE,
        lines,
        paints,
    );
}

pub fn render_extrude_panel(
    ctx: &mut FrameCtx<'_>,
    panel_id: WidgetId,
    panel_state: &mut PanelState,
    status_bar_rect: LayoutRect,
    error: &ExtrudeError,
    paints: &mut Vec<WidgetPaint>,
) {
    let lines = vec![extrude_panel_line(error, ctx.strings)];
    render_diagnostics_panel(
        ctx,
        panel_id,
        panel_state,
        status_bar_rect,
        strings::EXTRUDE_PANEL_TITLE,
        lines,
        paints,
    );
}

fn render_diagnostics_panel(
    ctx: &mut FrameCtx<'_>,
    panel_id: WidgetId,
    panel_state: &mut PanelState,
    status_bar_rect: LayoutRect,
    title: StringKey,
    lines: Vec<LabelText>,
    paints: &mut Vec<WidgetPaint>,
) {
    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: 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"))
}

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

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

#[cfg(test)]
mod tests {
    use super::{
        BrepError, ExtrudeError, ExtrudeStatus, SketchStatus, TruckGap, extrude_badge_style,
        extrude_panel_line, status_color, status_label_key,
    };
    use crate::strings;
    use bone_types::BrepEdgeId;
    use bone_ui::strings::Locale;
    use bone_ui::theme::Theme;

    #[test]
    fn extrude_badge_maps_states_to_keys_and_colors() {
        let cad = Theme::light().cad;
        let dangling = ExtrudeError::Kernel(BrepError::DanglingEdge {
            edge: BrepEdgeId::default(),
        });
        let invalid = ExtrudeError::Kernel(BrepError::EmptyExtrudeDepth);
        assert_eq!(
            extrude_badge_style(ExtrudeStatus::Valid, &cad),
            (strings::STATUS_EXTRUDE_VALID, cad.sketch_fully_defined)
        );
        assert_eq!(
            extrude_badge_style(ExtrudeStatus::Failed(&dangling), &cad),
            (strings::STATUS_EXTRUDE_DANGLING, cad.sketch_dangling)
        );
        assert_eq!(
            extrude_badge_style(ExtrudeStatus::Failed(&invalid), &cad),
            (strings::STATUS_EXTRUDE_INVALID, cad.sketch_invalid)
        );
    }

    #[test]
    fn invalid_and_dangling_tokens_differ() {
        let cad = Theme::light().cad;
        assert_ne!(cad.sketch_invalid, cad.sketch_dangling);
        assert_ne!(cad.sketch_invalid, cad.sketch_over_defined);
    }

    #[test]
    fn extrude_panel_lines_render_through_string_table() {
        let table = strings::make_strings(Locale::EnUs);
        let depth = ExtrudeError::Kernel(BrepError::EmptyExtrudeDepth);
        assert_eq!(
            extrude_panel_line(&depth, &table).resolve(&table),
            "Extrude depth is zero"
        );
        let dangling = ExtrudeError::Kernel(BrepError::DanglingEdge {
            edge: BrepEdgeId::default(),
        });
        let text = extrude_panel_line(&dangling, &table)
            .resolve(&table)
            .to_owned();
        assert!(text.starts_with("Edge "));
        assert!(!text.contains("{n}"));
        let draft = ExtrudeError::Kernel(BrepError::TruckUnsupported {
            detail: TruckGap::Draft,
        });
        assert_eq!(
            extrude_panel_line(&draft, &table).resolve(&table),
            "Draft is not supported yet"
        );
    }

    #[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
        );
    }
}
