//! Public prop extraction helpers for widget authors.
//!
//! These functions provide a convenient API for reading typed values from
//! a props map ([`Props`]). Widget authors use these in their `render()`
//! and `prepare()` implementations instead of manually traversing
//! `serde_json::Value`.
//!
//! ```ignore
//! let props = &node.props;
//! let label = prop_str(props, "label").unwrap_or_default();
//! let size = prop_f32(props, "size").unwrap_or(14.0);
//! ```

use iced::Color;
use plushie_core::protocol::Props;
use plushie_core::types::PlushieType;
use serde_json::Value;

use crate::iced_convert::hex_to_iced_color;

// ---------------------------------------------------------------------------
// Props type alias (deprecated)
// ---------------------------------------------------------------------------

/// A borrowed reference to a JSON object's field map, or `None` when
/// the node has no props.
///
/// Deprecated: use `&Props` directly. Kept for backward compatibility
/// with external widget code that hasn't migrated yet.
#[deprecated(note = "use &Props directly")]
pub type JsonProps<'a> = Option<&'a serde_json::Map<String, Value>>;

/// Safely narrow an f64 to f32, clamping values outside f32's range
/// instead of silently producing infinity.
///
/// For UI property values (dimensions, colors, text sizes) the f64 values
/// from JSON are always within f32 range, so this is pure defense-in-depth.
#[inline]
pub fn f64_to_f32(v: f64) -> f32 {
    v.clamp(f32::MIN as f64, f32::MAX as f64) as f32
}

// ---------------------------------------------------------------------------
// Core prop helpers
// ---------------------------------------------------------------------------

/// Get a string prop value.
pub fn prop_str(props: &Props, key: &str) -> Option<String> {
    props.get_str(key).map(|s| s.to_string())
}

/// Get an f32 prop value. Accepts numeric props or numeric strings.
pub fn prop_f32(props: &Props, key: &str) -> Option<f32> {
    if let Some(f) = props.get_f64(key) {
        return Some(f64_to_f32(f));
    }
    let s = props.get_str(key)?;
    match s.parse::<f32>().ok().filter(|f| f.is_finite()) {
        Some(f) => Some(f),
        None => {
            log::trace!("prop '{}': string not parseable as f32: {:?}", key, s);
            None
        }
    }
}

/// Get an f64 prop value. Accepts numeric props or numeric strings.
pub fn prop_f64(props: &Props, key: &str) -> Option<f64> {
    if let Some(f) = props.get_f64(key) {
        return Some(f);
    }
    let s = props.get_str(key)?;
    match s.parse::<f64>().ok().filter(|f| f.is_finite()) {
        Some(f) => Some(f),
        None => {
            log::trace!("prop '{}': string not parseable as f64: {:?}", key, s);
            None
        }
    }
}

/// Get a u32 prop value. Accepts numeric props or numeric strings.
pub fn prop_u32(props: &Props, key: &str) -> Option<u32> {
    if let Some(u) = props.get_u64(key).and_then(|v| u32::try_from(v).ok()) {
        return Some(u);
    }
    let s = props.get_str(key)?;
    match s.parse::<u32>() {
        Ok(u) => Some(u),
        Err(_) => {
            log::trace!("prop '{}': string not parseable as u32: {:?}", key, s);
            None
        }
    }
}

/// Get a u64 prop value. Accepts numeric props or numeric strings.
pub fn prop_u64(props: &Props, key: &str) -> Option<u64> {
    if let Some(u) = props.get_u64(key) {
        return Some(u);
    }
    let s = props.get_str(key)?;
    match s.parse::<u64>() {
        Ok(u) => Some(u),
        Err(_) => {
            log::trace!("prop '{}': string not parseable as u64: {:?}", key, s);
            None
        }
    }
}

/// Get a usize prop value. Accepts numeric props or numeric strings.
pub fn prop_usize(props: &Props, key: &str) -> Option<usize> {
    prop_u64(props, key).and_then(|v| usize::try_from(v).ok())
}

/// Get an i32 prop value. Accepts numeric props or numeric strings.
pub fn prop_i32(props: &Props, key: &str) -> Option<i32> {
    if let Some(i) = props.get_i64(key).and_then(|v| i32::try_from(v).ok()) {
        return Some(i);
    }
    let s = props.get_str(key)?;
    match s.parse::<i32>() {
        Ok(i) => Some(i),
        Err(_) => {
            log::trace!("prop '{}': string not parseable as i32: {:?}", key, s);
            None
        }
    }
}

/// Get an i64 prop value. Accepts numeric props or numeric strings.
pub fn prop_i64(props: &Props, key: &str) -> Option<i64> {
    if let Some(i) = props.get_i64(key) {
        return Some(i);
    }
    let s = props.get_str(key)?;
    match s.parse::<i64>() {
        Ok(i) => Some(i),
        Err(_) => {
            log::trace!("prop '{}': string not parseable as i64: {:?}", key, s);
            None
        }
    }
}

/// Get a boolean prop value.
pub fn prop_bool(props: &Props, key: &str) -> Option<bool> {
    props.get_bool(key)
}

/// Get a boolean prop value with a default.
pub fn prop_bool_default(props: &Props, key: &str, default: bool) -> bool {
    prop_bool(props, key).unwrap_or(default)
}

/// Parse a "range" prop as `[min, max]` into an inclusive `f32` range.
pub fn prop_range_f32(props: &Props) -> std::ops::RangeInclusive<f32> {
    props
        .get_value("range")
        .as_ref()
        .and_then(|v| v.as_array())
        .and_then(|arr| {
            let mut min = f64_to_f32(arr.first()?.as_f64()?);
            let mut max = f64_to_f32(arr.get(1)?.as_f64()?);
            if min > max {
                log::warn!("prop 'range': min ({min}) > max ({max}), swapping");
                std::mem::swap(&mut min, &mut max);
            }
            Some(min..=max)
        })
        .unwrap_or(0.0..=100.0)
}

/// Parse a "range" prop as `[min, max]` into an inclusive `f64` range.
pub fn prop_range_f64(props: &Props) -> std::ops::RangeInclusive<f64> {
    props
        .get_value("range")
        .as_ref()
        .and_then(|v| v.as_array())
        .and_then(|arr| {
            let mut min = arr.first()?.as_f64()?;
            let mut max = arr.get(1)?.as_f64()?;
            if min > max {
                log::warn!("prop 'range': min ({min}) > max ({max}), swapping");
                std::mem::swap(&mut min, &mut max);
            }
            Some(min..=max)
        })
        .unwrap_or(0.0..=100.0)
}

/// Get an array of f32 values from a prop.
/// Non-numeric elements are silently dropped with a warning.
pub fn prop_f32_array(props: &Props, key: &str) -> Option<Vec<f32>> {
    let val = props.get_value(key)?;
    match val.as_array() {
        Some(arr) => {
            let mut result = Vec::with_capacity(arr.len());
            for (i, v) in arr.iter().enumerate() {
                match v.as_f64() {
                    Some(f) => result.push(f64_to_f32(f)),
                    None => {
                        log::warn!(
                            "prop '{}': dropping non-numeric element at index {}: {:?}",
                            key,
                            i,
                            v
                        );
                    }
                }
            }
            Some(result)
        }
        None => {
            log::trace!("prop '{}': expected array, got {:?}", key, val);
            None
        }
    }
}

/// Get an f64 prop value. Accepts JSON numbers and numeric strings.
/// Non-numeric elements are silently dropped with a warning.
pub fn prop_f64_array(props: &Props, key: &str) -> Option<Vec<f64>> {
    let val = props.get_value(key)?;
    match val.as_array() {
        Some(arr) => {
            let mut result = Vec::with_capacity(arr.len());
            for (i, v) in arr.iter().enumerate() {
                match v.as_f64() {
                    Some(f) => result.push(f),
                    None => {
                        log::warn!(
                            "prop '{}': dropping non-numeric element at index {}: {:?}",
                            key,
                            i,
                            v
                        );
                    }
                }
            }
            Some(result)
        }
        None => {
            log::trace!("prop '{}': expected array, got {:?}", key, val);
            None
        }
    }
}

/// Get an array of string values from a prop.
pub fn prop_str_array(props: &Props, key: &str) -> Option<Vec<String>> {
    let val = props.get_value(key)?;
    match val.as_array() {
        Some(arr) => Some(
            arr.iter()
                .filter_map(|v| v.as_str().map(str::to_owned))
                .collect(),
        ),
        None => {
            log::trace!("prop '{}': expected array, got {:?}", key, val);
            None
        }
    }
}

// ---------------------------------------------------------------------------
// Animated prop helpers
// ---------------------------------------------------------------------------
// These check the interpolated_props cache (populated by the TransitionManager)
// before falling back to the tree's props. Use these in widget render functions
// for props that can be animated.

/// Get an f32 prop value, checking the animation cache first.
///
/// If the TransitionManager is actively interpolating this prop, returns
/// the current animated value. Otherwise falls back to the tree prop.
pub fn prop_animated_f32(
    interpolated: &std::collections::HashMap<String, serde_json::Map<String, Value>>,
    node_id: &str,
    props: &Props,
    key: &str,
) -> Option<f32> {
    // Check interpolated cache first
    if let Some(overrides) = interpolated.get(node_id)
        && let Some(val) = overrides.get(key)
    {
        return val.as_f64().map(f64_to_f32);
    }
    // Fall back to tree props (skip descriptor maps)
    if let Some(plushie_core::protocol::PropValue::Object(_)) = props.get(key) {
        // This is likely an animation descriptor; the renderer hasn't
        // started animating yet (first frame) or it just completed.
        // Return None so the widget uses its default.
        return None;
    }
    prop_f32(props, key)
}

/// Get a color prop value, checking the animation cache first.
pub fn prop_animated_color(
    interpolated: &std::collections::HashMap<String, serde_json::Map<String, Value>>,
    node_id: &str,
    props: &Props,
    key: &str,
) -> Option<Color> {
    if let Some(overrides) = interpolated.get(node_id)
        && let Some(val) = overrides.get(key)
    {
        return val.as_str().and_then(hex_to_iced_color);
    }
    if let Some(plushie_core::protocol::PropValue::Object(_)) = props.get(key) {
        return None;
    }
    plushie_core::types::Color::extract(props, key).map(|c| crate::iced_convert::color(&c))
}

// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::json;

    fn make_props(val: Value) -> Props {
        Props::from_json(val)
    }

    #[test]
    fn test_prop_str() {
        let p = make_props(json!({"label": "hello"}));
        assert_eq!(prop_str(&p, "label"), Some("hello".to_string()));
        assert_eq!(prop_str(&p, "missing"), None);
    }

    #[test]
    fn test_prop_str_non_string() {
        let p = make_props(json!({"num": 42}));
        assert_eq!(prop_str(&p, "num"), None);
    }

    #[test]
    fn test_prop_f32_number() {
        let p = make_props(json!({"size": 14.5}));
        let v = prop_f32(&p, "size").unwrap();
        assert!((v - 14.5).abs() < 0.001);
    }

    #[test]
    fn test_prop_f32_string() {
        let p = make_props(json!({"size": "14.5"}));
        let v = prop_f32(&p, "size").unwrap();
        assert!((v - 14.5).abs() < 0.001);
    }

    #[test]
    fn test_prop_f32_missing() {
        let p = make_props(json!({}));
        assert!(prop_f32(&p, "size").is_none());
    }

    #[test]
    fn test_prop_f32_wrong_type() {
        let p = make_props(json!({"size": true}));
        assert!(prop_f32(&p, "size").is_none());
    }

    #[test]
    fn test_prop_f64_number() {
        let p = make_props(json!({"value": 99.9}));
        let v = prop_f64(&p, "value").unwrap();
        assert!((v - 99.9).abs() < 0.0001);
    }

    #[test]
    fn test_prop_f64_string() {
        let p = make_props(json!({"value": "99.9"}));
        let v = prop_f64(&p, "value").unwrap();
        assert!((v - 99.9).abs() < 0.0001);
    }

    // -- prop_u32 --

    #[test]
    fn test_prop_u32_number() {
        let p = make_props(json!({"count": 42}));
        assert_eq!(prop_u32(&p, "count"), Some(42));
    }

    #[test]
    fn test_prop_u32_string() {
        let p = make_props(json!({"count": "123"}));
        assert_eq!(prop_u32(&p, "count"), Some(123));
    }

    #[test]
    fn test_prop_u32_missing() {
        let p = make_props(json!({}));
        assert_eq!(prop_u32(&p, "count"), None);
    }

    #[test]
    fn test_prop_u32_negative() {
        let p = make_props(json!({"count": -1}));
        assert_eq!(prop_u32(&p, "count"), None);
    }

    #[test]
    fn test_prop_u32_overflow() {
        let p = make_props(json!({"count": 5_000_000_000u64}));
        assert_eq!(prop_u32(&p, "count"), None);
    }

    // -- prop_u64 --

    #[test]
    fn test_prop_u64_number() {
        let p = make_props(json!({"big": 9_000_000_000u64}));
        assert_eq!(prop_u64(&p, "big"), Some(9_000_000_000));
    }

    #[test]
    fn test_prop_u64_string() {
        let p = make_props(json!({"big": "999"}));
        assert_eq!(prop_u64(&p, "big"), Some(999));
    }

    #[test]
    fn test_prop_u64_missing() {
        let p = make_props(json!({}));
        assert_eq!(prop_u64(&p, "big"), None);
    }

    #[test]
    fn test_prop_u64_negative() {
        let p = make_props(json!({"big": -1}));
        assert_eq!(prop_u64(&p, "big"), None);
    }

    // -- prop_usize --

    #[test]
    fn test_prop_usize_number() {
        let p = make_props(json!({"idx": 7}));
        assert_eq!(prop_usize(&p, "idx"), Some(7));
    }

    #[test]
    fn test_prop_usize_string() {
        let p = make_props(json!({"idx": "42"}));
        assert_eq!(prop_usize(&p, "idx"), Some(42));
    }

    #[test]
    fn test_prop_usize_missing() {
        let p = make_props(json!({}));
        assert_eq!(prop_usize(&p, "idx"), None);
    }

    // -- prop_i32 --

    #[test]
    fn test_prop_i32_positive() {
        let p = make_props(json!({"x": 42}));
        assert_eq!(prop_i32(&p, "x"), Some(42));
    }

    #[test]
    fn test_prop_i32_negative() {
        let p = make_props(json!({"x": -100}));
        assert_eq!(prop_i32(&p, "x"), Some(-100));
    }

    #[test]
    fn test_prop_i32_string() {
        let p = make_props(json!({"x": "-7"}));
        assert_eq!(prop_i32(&p, "x"), Some(-7));
    }

    #[test]
    fn test_prop_i32_missing() {
        let p = make_props(json!({}));
        assert_eq!(prop_i32(&p, "x"), None);
    }

    #[test]
    fn test_prop_i32_overflow() {
        let p = make_props(json!({"x": 5_000_000_000i64}));
        assert_eq!(prop_i32(&p, "x"), None);
    }

    // -- prop_i64 --

    #[test]
    fn test_prop_i64_positive() {
        let p = make_props(json!({"offset": 100}));
        assert_eq!(prop_i64(&p, "offset"), Some(100));
    }

    #[test]
    fn test_prop_i64_negative() {
        let p = make_props(json!({"offset": -50}));
        assert_eq!(prop_i64(&p, "offset"), Some(-50));
    }

    #[test]
    fn test_prop_i64_string() {
        let p = make_props(json!({"offset": "-99"}));
        assert_eq!(prop_i64(&p, "offset"), Some(-99));
    }

    #[test]
    fn test_prop_i64_missing() {
        let p = make_props(json!({}));
        assert_eq!(prop_i64(&p, "offset"), None);
    }

    #[test]
    fn test_prop_bool() {
        let p = make_props(json!({"disabled": true}));
        assert_eq!(prop_bool(&p, "disabled"), Some(true));
        assert_eq!(prop_bool(&p, "missing"), None);
    }

    #[test]
    fn test_prop_bool_default() {
        let p = make_props(json!({"disabled": true}));
        assert!(prop_bool_default(&p, "disabled", false));
        assert!(!prop_bool_default(&p, "missing", false));
        assert!(prop_bool_default(&p, "missing", true));
    }

    #[test]
    fn test_prop_bool_wrong_type() {
        let p = make_props(json!({"disabled": "yes"}));
        assert_eq!(prop_bool(&p, "disabled"), None);
    }

    #[test]
    fn test_prop_range_f32_present() {
        let p = make_props(json!({"range": [10.0, 50.0]}));
        let r = prop_range_f32(&p);
        assert_eq!(*r.start(), 10.0);
        assert_eq!(*r.end(), 50.0);
    }

    #[test]
    fn test_prop_range_f32_default() {
        let p = make_props(json!({}));
        let r = prop_range_f32(&p);
        assert_eq!(*r.start(), 0.0);
        assert_eq!(*r.end(), 100.0);
    }

    #[test]
    fn test_prop_range_f64_present() {
        let p = make_props(json!({"range": [1.0, 2.0]}));
        let r = prop_range_f64(&p);
        assert_eq!(*r.start(), 1.0);
        assert_eq!(*r.end(), 2.0);
    }

    #[test]
    fn test_prop_f32_array() {
        let p = make_props(json!({"data": [1.0, 2.5, 3.0]}));
        let arr = prop_f32_array(&p, "data").unwrap();
        assert_eq!(arr.len(), 3);
        assert!((arr[0] - 1.0).abs() < 0.001);
        assert!((arr[1] - 2.5).abs() < 0.001);
        assert!((arr[2] - 3.0).abs() < 0.001);
    }

    #[test]
    fn test_prop_f32_array_empty() {
        let p = make_props(json!({"data": []}));
        let arr = prop_f32_array(&p, "data").unwrap();
        assert!(arr.is_empty());
    }

    #[test]
    fn test_prop_f32_array_missing() {
        let p = make_props(json!({}));
        assert!(prop_f32_array(&p, "data").is_none());
    }

    #[test]
    fn test_prop_f32_array_not_array() {
        let p = make_props(json!({"data": "nope"}));
        assert!(prop_f32_array(&p, "data").is_none());
    }

    #[test]
    fn test_prop_f32_string_nan() {
        // "NaN" strings are rejected: non-finite values return None.
        let p = make_props(json!({"size": "NaN"}));
        assert!(prop_f32(&p, "size").is_none());
    }

    #[test]
    fn test_prop_f32_string_infinity() {
        // "Infinity" strings are rejected: non-finite values return None.
        let p = make_props(json!({"size": "Infinity"}));
        assert!(prop_f32(&p, "size").is_none());
    }

    #[test]
    fn test_prop_f32_empty_string() {
        let p = make_props(json!({"size": ""}));
        assert!(prop_f32(&p, "size").is_none());
    }

    #[test]
    fn test_prop_u32_non_numeric_string() {
        let p = make_props(json!({"count": "not_a_number"}));
        assert_eq!(prop_u32(&p, "count"), None);
    }

    #[test]
    fn test_empty_props() {
        let p = Props::default();
        assert!(prop_str(&p, "anything").is_none());
        assert!(prop_f32(&p, "anything").is_none());
        assert!(prop_bool(&p, "anything").is_none());
    }

    // -- prop_f64_array --

    #[test]
    fn test_prop_f64_array() {
        let p = make_props(json!({"data": [1.0, 2.5, 3.0]}));
        let arr = prop_f64_array(&p, "data").unwrap();
        assert_eq!(arr.len(), 3);
        assert!((arr[0] - 1.0).abs() < 0.0001);
        assert!((arr[1] - 2.5).abs() < 0.0001);
        assert!((arr[2] - 3.0).abs() < 0.0001);
    }

    #[test]
    fn test_prop_f64_array_skips_non_numeric() {
        let p = make_props(json!({"data": [1.0, "nope", 3.0]}));
        let arr = prop_f64_array(&p, "data").unwrap();
        assert_eq!(arr.len(), 2);
        assert!((arr[0] - 1.0).abs() < 0.0001);
        assert!((arr[1] - 3.0).abs() < 0.0001);
    }

    #[test]
    fn test_prop_f64_array_missing() {
        let p = make_props(json!({}));
        assert!(prop_f64_array(&p, "data").is_none());
    }

    // -- prop_str_array --

    #[test]
    fn test_prop_str_array() {
        let p = make_props(json!({"tags": ["a", "b", "c"]}));
        let arr = prop_str_array(&p, "tags").unwrap();
        assert_eq!(arr, vec!["a", "b", "c"]);
    }

    #[test]
    fn test_prop_str_array_skips_non_string() {
        let p = make_props(json!({"tags": ["a", 42, "c"]}));
        let arr = prop_str_array(&p, "tags").unwrap();
        assert_eq!(arr, vec!["a", "c"]);
    }

    #[test]
    fn test_prop_str_array_missing() {
        let p = make_props(json!({}));
        assert!(prop_str_array(&p, "tags").is_none());
    }

    // -- prop_object --

    // -- Property-based tests -------------------------------------------------

    mod proptest_prop_helpers {
        use super::*;
        use proptest::prelude::*;

        proptest! {
            /// prop_f32 should never panic for any f64 value and should
            /// return Some for all finite inputs.
            #[test]
            fn prop_f32_never_panics(val: f64) {
                let p = make_props(json!({"v": val}));
                let result = prop_f32(&p, "v");
                if val.is_finite() {
                    prop_assert!(result.is_some(), "expected Some for finite {val}");
                    let f = result.unwrap();
                    prop_assert!(f.is_finite(), "expected finite f32 for finite input {val}");
                } else {
                    // NaN and Infinity become JSON null via serde_json,
                    // so prop_f32 returns None, which is correct.
                }
            }
        }
    }

    // -- Typed variant tests -------------------------------------------------

    fn make_typed_props() -> Props {
        let mut map = plushie_core::protocol::PropMap::new();
        map.insert("label", "hello");
        map.insert("size", 14.5f64);
        map.insert("count", 42i64);
        map.insert("visible", true);
        map.insert("color", "#ff0000");
        Props::from(map)
    }

    #[test]
    fn typed_prop_str() {
        let p = make_typed_props();
        assert_eq!(prop_str(&p, "label"), Some("hello".to_string()));
        assert_eq!(prop_str(&p, "missing"), None);
    }

    #[test]
    fn typed_prop_f32() {
        let p = make_typed_props();
        let v = prop_f32(&p, "size").unwrap();
        assert!((v - 14.5).abs() < 0.001);
    }

    #[test]
    fn typed_prop_f64() {
        let p = make_typed_props();
        assert_eq!(prop_f64(&p, "size"), Some(14.5));
    }

    #[test]
    fn typed_prop_bool() {
        let p = make_typed_props();
        assert_eq!(prop_bool(&p, "visible"), Some(true));
        assert!(prop_bool_default(&p, "visible", false));
        assert!(!prop_bool_default(&p, "missing", false));
    }

    #[test]
    fn typed_prop_i64() {
        let p = make_typed_props();
        assert_eq!(prop_i64(&p, "count"), Some(42));
    }

    #[test]
    fn typed_prop_u32() {
        let p = make_typed_props();
        assert_eq!(prop_u32(&p, "count"), Some(42));
    }

    #[test]
    fn typed_and_wire_produce_same_results() {
        let typed = make_typed_props();
        let wire = make_props(json!({
            "label": "hello",
            "size": 14.5,
            "count": 42,
            "visible": true,
            "color": "#ff0000",
        }));

        assert_eq!(prop_str(&typed, "label"), prop_str(&wire, "label"));
        assert_eq!(prop_f32(&typed, "size"), prop_f32(&wire, "size"));
        assert_eq!(prop_f64(&typed, "size"), prop_f64(&wire, "size"));
        assert_eq!(prop_bool(&typed, "visible"), prop_bool(&wire, "visible"));
        assert_eq!(prop_i64(&typed, "count"), prop_i64(&wire, "count"));
        assert_eq!(prop_u64(&typed, "count"), prop_u64(&wire, "count"));
    }
}
