use core::str::FromStr;

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

pub type NumericInput<'state, T> = ParsedInput<'state, T>;
pub type NumericInputResponse<T> = ParsedInputResponse<T>;

macro_rules! impl_parsed_value_via_fromstr {
    ($($ty:ty),+ $(,)?) => {$(
        impl ParsedValue for $ty {
            type Error = <$ty as FromStr>::Err;

            fn parse(text: &str) -> Result<Self, Self::Error> {
                text.trim().parse::<$ty>()
            }
        }
    )+};
}

impl_parsed_value_via_fromstr!(i8, i16, i32, i64, i128, isize);
impl_parsed_value_via_fromstr!(u8, u16, u32, u64, u128, usize);

#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum NumericFloatParseError {
    #[error("not a finite number")]
    NotFinite,
    #[error("invalid float: {0}")]
    Parse(String),
}

macro_rules! impl_parsed_value_finite_float {
    ($($ty:ty),+ $(,)?) => {$(
        impl ParsedValue for $ty {
            type Error = NumericFloatParseError;

            fn parse(text: &str) -> Result<Self, Self::Error> {
                let trimmed = text.trim();
                let value: $ty = trimmed
                    .parse()
                    .map_err(|_| NumericFloatParseError::Parse(trimmed.to_owned()))?;
                if value.is_finite() {
                    Ok(value)
                } else {
                    Err(NumericFloatParseError::NotFinite)
                }
            }
        }
    )+};
}

impl_parsed_value_finite_float!(f32, f64);

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

    use super::{NumericInput, NumericInputResponse};
    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("numeric.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("numeric"))
    }

    fn run<T: ParsedValue + 'static>(state: &mut TextInputState) -> NumericInputResponse<T> {
        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);
        input.text_committed = String::new();
        let mut clipboard = MemoryClipboard::default();
        let widget = NumericInput::<T>::new(id_widget(), rect(), PLACEHOLDER, 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 parses_valid_integer() {
        let mut state = TextInputState::from_text("42");
        let response: NumericInputResponse<i32> = run(&mut state);
        assert_eq!(response.value, Some(42));
        assert!(response.error.is_none());
    }

    #[test]
    fn parses_valid_float() {
        let mut state = TextInputState::from_text("2.5");
        let response: NumericInputResponse<f64> = run(&mut state);
        assert_eq!(response.value, Some(2.5));
        assert!(response.error.is_none());
    }

    #[test]
    fn rejects_garbage_with_typed_error() {
        let mut state = TextInputState::from_text("not-a-number");
        let response: NumericInputResponse<u32> = run(&mut state);
        assert!(response.value.is_none());
        assert!(response.error.is_some());
    }

    #[test]
    fn empty_text_is_neither_value_nor_error() {
        let mut state = TextInputState::default();
        let response: NumericInputResponse<i64> = run(&mut state);
        assert!(response.value.is_none());
        assert!(response.error.is_none());
    }

    #[test]
    fn negative_integer_parses_when_signed() {
        let mut state = TextInputState::from_text("-7");
        let response: NumericInputResponse<i32> = run(&mut state);
        assert_eq!(response.value, Some(-7));
    }

    #[test]
    fn negative_integer_errors_when_unsigned() {
        let mut state = TextInputState::from_text("-7");
        let response: NumericInputResponse<u32> = run(&mut state);
        assert!(response.error.is_some());
    }

    #[test]
    fn float_rejects_nan() {
        use super::NumericFloatParseError;
        let mut state = TextInputState::from_text("NaN");
        let response: NumericInputResponse<f64> = run(&mut state);
        assert!(response.value.is_none());
        assert_eq!(response.error, Some(NumericFloatParseError::NotFinite));
    }

    #[test]
    fn float_rejects_infinity() {
        use super::NumericFloatParseError;
        let mut state = TextInputState::from_text("inf");
        let response: NumericInputResponse<f64> = run(&mut state);
        assert!(response.value.is_none());
        assert_eq!(response.error, Some(NumericFloatParseError::NotFinite));
    }

    #[test]
    fn float_accepts_finite_value() {
        let mut state = TextInputState::from_text("2.5");
        let response: NumericInputResponse<f64> = run(&mut state);
        assert_eq!(response.value, Some(2.5));
        assert!(response.error.is_none());
    }

    #[test]
    fn whitespace_around_value_parses() {
        let mut state = TextInputState::from_text("  12 ");
        let response: NumericInputResponse<i32> = run(&mut state);
        assert_eq!(response.value, Some(12));
    }

    #[test]
    fn typing_alone_does_not_commit() {
        let mut state = TextInputState::from_text("42");
        let response: NumericInputResponse<i32> = run(&mut state);
        assert_eq!(response.value, Some(42));
        assert!(response.committed.is_none());
    }

    #[test]
    fn enter_while_focused_commits_value() {
        use crate::input::{KeyCode, KeyEvent, ModifierMask, NamedKey};
        let mut state = TextInputState::from_text("42");
        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);
        input.keys_pressed.push(KeyEvent::new(
            KeyCode::Named(NamedKey::Enter),
            ModifierMask::NONE,
        ));
        let mut clipboard = MemoryClipboard::default();
        let widget = NumericInput::<i32>::new(id_widget(), rect(), PLACEHOLDER, &mut state);
        let response = {
            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)
        };
        assert_eq!(response.committed, Some(42));
        assert!(input.keys_pressed.is_empty(), "Enter drained on commit");
    }

    #[test]
    fn focus_loss_commits_value() {
        let mut state = TextInputState::from_text("42");
        state.was_focused = true;
        let theme = Arc::new(Theme::light());
        let mut focus = FocusManager::new();
        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 = NumericInput::<i32>::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,
        );
        let response = show_parsed_input(&mut ctx, widget, &mut clipboard);
        assert_eq!(response.committed, Some(42));
        assert!(
            !state.was_focused,
            "post-call state reflects current focus, not prior",
        );
    }

    #[test]
    fn two_frame_focus_loss_commits_value() {
        let mut state = TextInputState::from_text("42");
        let theme = Arc::new(Theme::light());
        let table = HotkeyTable::new();
        let mut clipboard = MemoryClipboard::default();

        let mut focus = FocusManager::new();
        focus.register_focusable(id_widget());
        focus.request_focus(id_widget());
        focus.end_frame();

        {
            let mut hits = HitFrame::new();
            let prev = HitState::new();
            let mut input = InputSnapshot::idle(FrameInstant::ZERO);
            let widget = NumericInput::<i32>::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.clone(),
                &mut input,
                &mut focus,
                &table,
                StringTable::empty(),
                &mut hits,
                &prev,
                &mut a11y,
                &mut shaper,
            );
            let response = show_parsed_input(&mut ctx, widget, &mut clipboard);
            assert!(
                response.committed.is_none(),
                "frame 1 still focused, no commit yet",
            );
        }

        let mut focus = FocusManager::new();
        let mut hits = HitFrame::new();
        let prev = HitState::new();
        let mut input = InputSnapshot::idle(FrameInstant::ZERO);
        let widget = NumericInput::<i32>::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,
        );
        let response = show_parsed_input(&mut ctx, widget, &mut clipboard);
        assert_eq!(
            response.committed,
            Some(42),
            "frame 2 unfocused after being focused frame 1: commit fires",
        );
    }
}
