use floating_ui_utils::{Coords, ElementOrVirtual, Placement, Strategy};
use crate::compute_coords_from_placement::compute_coords_from_placement;
use crate::types::{
    ComputePositionConfig, ComputePositionReturn, Elements, GetElementRectsArgs, MiddlewareData,
    MiddlewareReturn, MiddlewareState, Reset, ResetRects,
};
pub fn compute_position<Element: Clone, Window: Clone>(
    reference: ElementOrVirtual<Element>,
    floating: &Element,
    config: ComputePositionConfig<Element, Window>,
) -> ComputePositionReturn {
    let placement = config.placement.unwrap_or(Placement::Bottom);
    let strategy = config.strategy.unwrap_or(Strategy::Absolute);
    let platform = config.platform;
    let middlewares = config.middleware.unwrap_or_default();
    let rtl = platform.is_rtl(floating);
    let mut rects = platform.get_element_rects(GetElementRectsArgs {
        reference: reference.clone(),
        floating,
        strategy,
    });
    let Coords { mut x, mut y } = compute_coords_from_placement(&rects, placement, rtl);
    let mut stateful_placement = placement;
    let mut middleware_data = MiddlewareData::default();
    let mut reset_count = 0;
    let mut i = 0;
    while i < middlewares.len() {
        let middleware = &middlewares[i];
        let MiddlewareReturn {
            x: next_x,
            y: next_y,
            data,
            reset,
        } = middleware.compute(MiddlewareState {
            x,
            y,
            initial_placement: placement,
            placement: stateful_placement,
            strategy,
            middleware_data: &middleware_data,
            rects: &rects,
            platform,
            elements: Elements {
                reference: reference.clone(),
                floating,
            },
        });
        x = next_x.unwrap_or(x);
        y = next_y.unwrap_or(y);
        if let Some(data) = data {
            let existing_data = middleware_data.get(middleware.name());
            let new_data = match existing_data {
                Some(existing_data) => {
                    let mut a = existing_data
                        .as_object()
                        .expect("Existing data should be an object.")
                        .to_owned();
                    let mut b = data
                        .as_object()
                        .expect("New data should be an object.")
                        .to_owned();
                    b.retain(|_, v| !v.is_null());
                    a.extend(b);
                    serde_json::Value::Object(a)
                }
                None => data,
            };
            middleware_data.set(middleware.name(), new_data);
        }
        if let Some(reset) = reset {
            if reset_count <= 50 {
                reset_count += 1;
                match reset {
                    Reset::True => {}
                    Reset::Value(value) => {
                        if let Some(reset_placement) = value.placement {
                            stateful_placement = reset_placement;
                        }
                        if let Some(reset_rects) = value.rects {
                            rects = match reset_rects {
                                ResetRects::True => {
                                    platform.get_element_rects(GetElementRectsArgs {
                                        reference: reference.clone(),
                                        floating,
                                        strategy,
                                    })
                                }
                                ResetRects::Value(element_rects) => element_rects,
                            }
                        }
                        let Coords {
                            x: next_x,
                            y: next_y,
                        } = compute_coords_from_placement(&rects, stateful_placement, rtl);
                        x = next_x;
                        y = next_y;
                    }
                }
                i = 0;
                continue;
            }
        }
        i += 1;
    }
    ComputePositionReturn {
        x,
        y,
        placement: stateful_placement,
        strategy,
        middleware_data,
    }
}
#[cfg(test)]
mod tests {
    use serde_json::json;
    use crate::test_utils::{FLOATING, PLATFORM, REFERENCE};
    use crate::types::Middleware;
    use super::*;
    #[test]
    fn test_returned_data() {
        #[derive(Clone)]
        struct CustomMiddleware {}
        impl<Element: Clone, Window: Clone> Middleware<Element, Window> for CustomMiddleware {
            fn name(&self) -> &'static str {
                "custom"
            }
            fn compute(&self, _state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
                MiddlewareReturn {
                    x: None,
                    y: None,
                    data: Some(json!({"property": true})),
                    reset: None,
                }
            }
        }
        let ComputePositionReturn {
            x,
            y,
            placement,
            strategy,
            middleware_data,
        } = compute_position(
            (&REFERENCE).into(),
            &FLOATING,
            ComputePositionConfig {
                platform: &PLATFORM,
                placement: Some(Placement::Top),
                strategy: None,
                middleware: Some(vec![Box::new(CustomMiddleware {})]),
            },
        );
        assert_eq!(x, 25.0);
        assert_eq!(y, -50.0);
        assert_eq!(placement, Placement::Top);
        assert_eq!(strategy, Strategy::Absolute);
        assert_eq!(
            middleware_data.get("custom"),
            Some(&json!({"property": true}))
        );
    }
    #[test]
    fn test_middleware() {
        #[derive(Clone)]
        struct TestMiddleware {}
        impl<Element: Clone, Window: Clone> Middleware<Element, Window> for TestMiddleware {
            fn name(&self) -> &'static str {
                "test"
            }
            fn compute(
                &self,
                MiddlewareState { x, y, .. }: MiddlewareState<Element, Window>,
            ) -> MiddlewareReturn {
                MiddlewareReturn {
                    x: Some(x + 1.0),
                    y: Some(y + 1.0),
                    data: None,
                    reset: None,
                }
            }
        }
        let ComputePositionReturn { x, y, .. } = compute_position(
            (&REFERENCE).into(),
            &FLOATING,
            ComputePositionConfig {
                platform: &PLATFORM,
                placement: None,
                strategy: None,
                middleware: None,
            },
        );
        let ComputePositionReturn { x: x2, y: y2, .. } = compute_position(
            (&REFERENCE).into(),
            &FLOATING,
            ComputePositionConfig {
                platform: &PLATFORM,
                placement: None,
                strategy: None,
                middleware: Some(vec![Box::new(TestMiddleware {})]),
            },
        );
        assert_eq!((x2, y2), (x + 1.0, y + 1.0));
    }
    #[test]
    fn test_middleware_data() {
        #[derive(Clone)]
        struct TestMiddleware {}
        impl<Element: Clone, Window: Clone> Middleware<Element, Window> for TestMiddleware {
            fn name(&self) -> &'static str {
                "test"
            }
            fn compute(&self, _state: MiddlewareState<Element, Window>) -> MiddlewareReturn {
                MiddlewareReturn {
                    x: None,
                    y: None,
                    data: Some(json!({"hello": true})),
                    reset: None,
                }
            }
        }
        let ComputePositionReturn {
            middleware_data, ..
        } = compute_position(
            (&REFERENCE).into(),
            &FLOATING,
            ComputePositionConfig {
                platform: &PLATFORM,
                placement: None,
                strategy: None,
                middleware: Some(vec![Box::new(TestMiddleware {})]),
            },
        );
        assert_eq!(middleware_data.get("test"), Some(&json!({"hello": true})));
    }
}