use uom::si::angle::{degree, radian};
use uom::si::f64::{Angle, Length};
use uom::si::length::{centimeter, foot, inch, meter, millimeter};

use super::parsed_input::{ParsedInput, ParsedInputResponse, ParsedValue};

pub type DimensionedInput<'state, Q> = ParsedInput<'state, Q>;
pub type DimensionedInputResponse<Q> = ParsedInputResponse<Q>;

#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DimensionedParseError {
    #[error("empty input")]
    Empty,
    #[error("unknown unit suffix: {0}")]
    UnknownUnit(String),
    #[error("invalid number: {0}")]
    InvalidNumber(String),
}

type UnitCtor<Q> = fn(f64) -> Q;
type UnitEntry<Q> = (&'static str, UnitCtor<Q>);

struct UnitTable<Q: 'static> {
    default: UnitCtor<Q>,
    suffixes: &'static [UnitEntry<Q>],
}

const LENGTH_MM: UnitCtor<Length> = |v| Length::new::<millimeter>(v);
const LENGTH_CM: UnitCtor<Length> = |v| Length::new::<centimeter>(v);
const LENGTH_M: UnitCtor<Length> = |v| Length::new::<meter>(v);
const LENGTH_IN: UnitCtor<Length> = |v| Length::new::<inch>(v);
const LENGTH_FT: UnitCtor<Length> = |v| Length::new::<foot>(v);

const LENGTH_UNITS: UnitTable<Length> = UnitTable {
    default: LENGTH_MM,
    suffixes: &[
        ("mm", LENGTH_MM),
        ("cm", LENGTH_CM),
        ("m", LENGTH_M),
        ("in", LENGTH_IN),
        ("\"", LENGTH_IN),
        ("ft", LENGTH_FT),
        ("'", LENGTH_FT),
    ],
};

const ANGLE_DEG: UnitCtor<Angle> = |v| Angle::new::<degree>(v);
const ANGLE_RAD: UnitCtor<Angle> = |v| Angle::new::<radian>(v);

const ANGLE_UNITS: UnitTable<Angle> = UnitTable {
    default: ANGLE_DEG,
    suffixes: &[
        ("deg", ANGLE_DEG),
        ("\u{00B0}", ANGLE_DEG),
        ("rad", ANGLE_RAD),
    ],
};

fn parse_dimensioned<Q>(text: &str, units: &UnitTable<Q>) -> Result<Q, DimensionedParseError> {
    let trimmed = text.trim();
    if trimmed.is_empty() {
        return Err(DimensionedParseError::Empty);
    }
    let lowered = trimmed.to_ascii_lowercase();
    let with_unit = units
        .suffixes
        .iter()
        .filter(|(suffix, _)| lowered.ends_with(suffix))
        .max_by_key(|(suffix, _)| suffix.len())
        .and_then(|(suffix, ctor)| {
            let cut = trimmed.len() - suffix.len();
            trimmed[..cut].trim().parse::<f64>().ok().map(ctor)
        });
    if let Some(value) = with_unit {
        return Ok(value);
    }
    if let Ok(value) = trimmed.parse::<f64>() {
        return Ok((units.default)(value));
    }
    let suffix_start = trimmed
        .char_indices()
        .find(|(_, c)| !is_numeric_part(*c))
        .map(|(i, _)| i);
    match suffix_start {
        Some(start) if start > 0 => Err(DimensionedParseError::UnknownUnit(
            trimmed[start..].trim().to_owned(),
        )),
        _ => Err(DimensionedParseError::InvalidNumber(trimmed.to_owned())),
    }
}

fn is_numeric_part(c: char) -> bool {
    c.is_ascii_digit() || matches!(c, '.' | '-' | '+' | 'e' | 'E')
}

impl ParsedValue for Length {
    type Error = DimensionedParseError;

    fn parse(text: &str) -> Result<Self, Self::Error> {
        parse_dimensioned(text, &LENGTH_UNITS)
    }
}

impl ParsedValue for Angle {
    type Error = DimensionedParseError;

    fn parse(text: &str) -> Result<Self, Self::Error> {
        parse_dimensioned(text, &ANGLE_UNITS)
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use uom::si::angle::{degree, radian};
    use uom::si::f64::{Angle, Length};
    use uom::si::length::{centimeter, foot, inch, meter, millimeter};

    use super::{DimensionedInput, DimensionedInputResponse, DimensionedParseError};
    use crate::focus::FocusManager;
    use crate::frame::FrameCtx;
    use crate::hit_test::{HitFrame, HitState};
    use crate::hotkey::HotkeyTable;
    use crate::input::{FrameInstant, InputSnapshot};
    use crate::layout::{LayoutPos, LayoutPx, LayoutRect, LayoutSize};
    use crate::strings::StringKey;
    use crate::strings::StringTable;
    use crate::theme::Theme;
    use crate::widget_id::{WidgetId, WidgetKey};
    use crate::widgets::parsed_input::show_parsed_input;
    use crate::widgets::{MemoryClipboard, TextInputState};

    const PLACEHOLDER: StringKey = StringKey::new("dim.placeholder");

    fn rect() -> LayoutRect {
        LayoutRect::new(
            LayoutPos::new(LayoutPx::ZERO, LayoutPx::ZERO),
            LayoutSize::new(LayoutPx::new(120.0), LayoutPx::new(24.0)),
        )
    }

    fn id_widget() -> WidgetId {
        WidgetId::ROOT.child(WidgetKey::new("dim"))
    }

    fn run_length(state_text: &str) -> DimensionedInputResponse<Length> {
        let mut state = TextInputState::from_text(state_text);
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut clipboard = MemoryClipboard::default();
        let widget = DimensionedInput::<Length>::new(id_widget(), rect(), PLACEHOLDER, &mut state);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = crate::a11y::AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        show_parsed_input(&mut ctx, widget, &mut clipboard)
    }

    fn run_angle(state_text: &str) -> DimensionedInputResponse<Angle> {
        let mut state = TextInputState::from_text(state_text);
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();
        let table = HotkeyTable::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let mut clipboard = MemoryClipboard::default();
        let widget = DimensionedInput::<Angle>::new(id_widget(), rect(), PLACEHOLDER, &mut state);
        let mut shaper = bone_text::Shaper::new();
        let mut a11y = crate::a11y::AccessTreeBuilder::new();
        let mut ctx = FrameCtx::new(
            theme,
            &mut input,
            &mut focus,
            &table,
            StringTable::empty(),
            &mut hits,
            &prev,
            &mut a11y,
            &mut shaper,
        );
        show_parsed_input(&mut ctx, widget, &mut clipboard)
    }

    #[test]
    fn length_default_unit_is_mm() {
        let response = run_length("12.5");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<millimeter>() - 12.5).abs() < 1e-12);
    }

    #[test]
    fn length_with_explicit_mm_suffix() {
        let response = run_length("3mm");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<millimeter>() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn length_inch_suffix_converts_to_mm() {
        let response = run_length("1in");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<inch>() - 1.0).abs() < 1e-12);
    }

    #[test]
    fn length_double_quote_inch_suffix() {
        let response = run_length("0.5\"");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<inch>() - 0.5).abs() < 1e-12);
    }

    #[test]
    fn length_centimeter_suffix() {
        let response = run_length("2cm");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<centimeter>() - 2.0).abs() < 1e-12);
    }

    #[test]
    fn length_meter_suffix() {
        let response = run_length("0.5m");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<meter>() - 0.5).abs() < 1e-12);
    }

    #[test]
    fn length_foot_suffix() {
        let response = run_length("2ft");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<foot>() - 2.0).abs() < 1e-12);
    }

    #[test]
    fn length_unknown_suffix_errors() {
        let response = run_length("3parsec");
        match response.error {
            Some(DimensionedParseError::UnknownUnit(s)) => assert_eq!(s, "parsec"),
            other => panic!("expected unknown-unit, got {other:?}"),
        }
    }

    #[test]
    fn length_partial_suffix_match_falls_through_to_unknown_unit() {
        let response = run_length("3min");
        match response.error {
            Some(DimensionedParseError::UnknownUnit(s)) => assert_eq!(s, "min"),
            other => panic!("expected unknown-unit, got {other:?}"),
        }
    }

    #[test]
    fn length_invalid_number_errors() {
        let response = run_length("abcmm");
        assert!(matches!(
            response.error,
            Some(DimensionedParseError::InvalidNumber(_))
        ));
    }

    #[test]
    fn angle_default_unit_is_deg() {
        let response = run_angle("90");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<degree>() - 90.0).abs() < 1e-12);
    }

    #[test]
    fn angle_radian_suffix() {
        let response = run_angle("1.0rad");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<radian>() - 1.0).abs() < 1e-12);
    }

    #[test]
    fn angle_degree_glyph_suffix() {
        let response = run_angle("45\u{00B0}");
        let Some(value) = response.value else {
            panic!("value parses")
        };
        assert!((value.get::<degree>() - 45.0).abs() < 1e-12);
    }

    #[test]
    fn empty_input_neither_value_nor_error_for_length() {
        let response = run_length("");
        assert!(response.value.is_none());
        assert!(response.error.is_none());
    }

    #[test]
    fn pure_garbage_classifies_as_invalid_number_not_unknown_unit() {
        let response = run_length("abc");
        assert!(matches!(
            response.error,
            Some(DimensionedParseError::InvalidNumber(_))
        ));
    }

    #[test]
    fn scientific_notation_with_unit_parses() {
        let response = run_length("1.5e1mm");
        let Some(value) = response.value else {
            panic!("scientific notation parses")
        };
        assert!((value.get::<millimeter>() - 15.0).abs() < 1e-12);
    }

    #[test]
    fn length_uppercase_suffix_accepted() {
        let response = run_length("3MM");
        let Some(value) = response.value else {
            panic!("uppercase mm parses")
        };
        assert!((value.get::<millimeter>() - 3.0).abs() < 1e-12);
    }

    #[test]
    fn length_mixed_case_suffix_accepted() {
        let response = run_length("12In");
        let Some(value) = response.value else {
            panic!("mixed-case in parses")
        };
        assert!((value.get::<inch>() - 12.0).abs() < 1e-12);
    }

    #[test]
    fn angle_uppercase_suffix() {
        let response = run_angle("1.5RAD");
        let Some(value) = response.value else {
            panic!("uppercase rad parses")
        };
        assert!((value.get::<radian>() - 1.5).abs() < 1e-12);
    }
}
