use uom::si::angle::{degree, radian, revolution};
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_REV: UnitCtor<Angle> = |v| Angle::new::<revolution>(v);

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

#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
pub enum AngleUnit {
    #[default]
    Degrees,
    Radians,
    Revolutions,
}

impl AngleUnit {
    #[must_use]
    pub fn make(self, value: f64) -> Angle {
        match self {
            Self::Degrees => Angle::new::<degree>(value),
            Self::Radians => Angle::new::<radian>(value),
            Self::Revolutions => Angle::new::<revolution>(value),
        }
    }

    #[must_use]
    pub fn measure(self, angle: Angle) -> f64 {
        match self {
            Self::Degrees => angle.get::<degree>(),
            Self::Radians => angle.get::<radian>(),
            Self::Revolutions => angle.get::<revolution>(),
        }
    }

    #[must_use]
    pub fn suffix(self) -> &'static str {
        match self {
            Self::Degrees => "deg",
            Self::Radians => "rad",
            Self::Revolutions => "rev",
        }
    }
}

#[must_use]
pub fn angle_text_has_unit(text: &str) -> bool {
    let lowered = text.trim().to_ascii_lowercase();
    ANGLE_UNITS
        .suffixes
        .iter()
        .any(|(suffix, _)| lowered.ends_with(suffix))
}

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();
            parse_finite(trimmed[..cut].trim()).map(ctor)
        });
    if let Some(value) = with_unit {
        return Ok(value);
    }
    if let Some(value) = parse_finite(trimmed) {
        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')
}

fn parse_finite(text: &str) -> Option<f64> {
    text.parse::<f64>().ok().filter(|value| value.is_finite())
}

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;
    use uom::si::f64::{Angle, Length};
    use uom::si::length::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::{ParsedValue, 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<T: ParsedValue>(state_text: &str) -> DimensionedInputResponse<T> {
        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::<T>::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 assert_parses<T: ParsedValue>(
        cases: &[(&str, f64)],
        measure: impl Fn(T) -> f64,
    ) -> Result<(), Box<dyn std::error::Error>> {
        cases.iter().try_for_each(
            |&(text, expected)| -> Result<(), Box<dyn std::error::Error>> {
                let value = run::<T>(text)
                    .value
                    .ok_or_else(|| format!("{text} parses"))?;
                let got = measure(value);
                assert!(
                    (got - expected).abs() < 1e-9,
                    "{text}: want {expected}, got {got}"
                );
                Ok(())
            },
        )?;
        Ok(())
    }

    #[test]
    fn parses_units_and_defaults_for_length_and_angle() -> Result<(), Box<dyn std::error::Error>> {
        assert_parses::<Length>(
            &[
                ("12.5", 12.5),
                ("3mm", 3.0),
                ("1in", 25.4),
                ("0.5\"", 12.7),
                ("2cm", 20.0),
                ("0.5m", 500.0),
                ("2ft", 609.6),
                ("3MM", 3.0),
                ("12In", 304.8),
                ("1.5e1mm", 15.0),
            ],
            |value| value.get::<millimeter>(),
        )?;
        assert_parses::<Angle>(
            &[
                ("90", 90.0),
                ("1.0rad", 1.0_f64.to_degrees()),
                ("45\u{00B0}", 45.0),
                ("0.25rev", 90.0),
                ("1.5RAD", 1.5_f64.to_degrees()),
            ],
            |value| value.get::<degree>(),
        )?;
        Ok(())
    }

    #[test]
    fn parse_reports_unknown_units_invalid_numbers_and_empties() {
        type Probe = fn(&str) -> (bool, Option<DimensionedParseError>);
        enum Want {
            Unknown(&'static str),
            Invalid,
            Silent,
        }
        let length: Probe = |text| {
            let response = run::<Length>(text);
            (response.value.is_none(), response.error)
        };
        let angle: Probe = |text| {
            let response = run::<Angle>(text);
            (response.value.is_none(), response.error)
        };
        [
            (length, "3parsec", Want::Unknown("parsec")),
            (length, "3min", Want::Unknown("min")),
            (length, "abcmm", Want::Invalid),
            (length, "abc", Want::Invalid),
            (length, "NaN", Want::Invalid),
            (length, "1e999", Want::Invalid),
            (angle, "inf", Want::Invalid),
            (length, "", Want::Silent),
        ]
        .into_iter()
        .for_each(|(probe, text, want)| {
            let (value_absent, error) = probe(text);
            match want {
                Want::Unknown(unit) => assert!(
                    matches!(&error, Some(DimensionedParseError::UnknownUnit(found)) if found == unit),
                    "{text} reports unknown unit {unit}, got {error:?}"
                ),
                Want::Invalid => {
                    assert!(value_absent, "{text} must not commit a value");
                    assert!(
                        matches!(&error, Some(DimensionedParseError::InvalidNumber(_))),
                        "{text} reports an invalid number, got {error:?}"
                    );
                }
                Want::Silent => {
                    assert!(value_absent, "{text} yields no value");
                    assert!(error.is_none(), "{text} yields no error, got {error:?}");
                }
            }
        });
    }

    #[test]
    fn angle_helpers_detect_suffixes_and_round_trip_their_measure() {
        use super::{AngleUnit, angle_text_has_unit};
        [
            ("1.5 rad", true),
            ("90deg", true),
            ("0.25 REV", true),
            ("45\u{00B0}", true),
            ("90", false),
            ("  0.5  ", false),
        ]
        .into_iter()
        .for_each(|(text, has_unit)| {
            assert_eq!(angle_text_has_unit(text), has_unit, "{text}");
        });

        [
            AngleUnit::Degrees,
            AngleUnit::Radians,
            AngleUnit::Revolutions,
        ]
        .into_iter()
        .for_each(|unit| {
            assert!((unit.measure(unit.make(0.75)) - 0.75).abs() < 1e-12);
        });
        assert!((AngleUnit::Revolutions.make(0.25).get::<degree>() - 90.0).abs() < 1e-9);
    }
}
