//! Implements portable horizontal vector min/max reductions.

macro_rules! impl_reduction_min_max {
    ([$elem_ty:ident; $elem_count:expr]: $id:ident
     | $ielem_ty:ident | $test_tt:tt) => {
        impl $id {
            /// Largest vector element value.
            #[inline]
            pub fn max_element(self) -> $elem_ty {
                #[cfg(not(any(
                    target_arch = "aarch64",
                    target_arch = "arm",
                    target_arch = "powerpc64",
                    target_arch = "wasm32",
                )))]
                {
                    use crate::llvm::simd_reduce_max;
                    let v: $ielem_ty = unsafe { simd_reduce_max(self.0) };
                    v as $elem_ty
                }
                #[cfg(any(
                    target_arch = "aarch64",
                    target_arch = "arm",
                    target_arch = "powerpc64",
                    target_arch = "wasm32",
                ))]
                {
                    // FIXME: broken on AArch64
                    // https://github.com/rust-lang-nursery/packed_simd/issues/15
                    // FIXME: broken on WASM32
                    // https://github.com/rust-lang-nursery/packed_simd/issues/91
                    let mut x = self.extract(0);
                    for i in 1..$id::lanes() {
                        x = x.max(self.extract(i));
                    }
                    x
                }
            }

            /// Smallest vector element value.
            #[inline]
            pub fn min_element(self) -> $elem_ty {
                #[cfg(not(any(
                    target_arch = "aarch64",
                    target_arch = "arm",
                    all(target_arch = "x86", not(target_feature = "sse2")),
                    target_arch = "powerpc64",
                    target_arch = "wasm32",
                ),))]
                {
                    use crate::llvm::simd_reduce_min;
                    let v: $ielem_ty = unsafe { simd_reduce_min(self.0) };
                    v as $elem_ty
                }
                #[cfg(any(
                    target_arch = "aarch64",
                    target_arch = "arm",
                    all(target_arch = "x86", not(target_feature = "sse2")),
                    target_arch = "powerpc64",
                    target_arch = "wasm32",
                ))]
                {
                    // FIXME: broken on AArch64
                    // https://github.com/rust-lang-nursery/packed_simd/issues/15
                    // FIXME: broken on i586-unknown-linux-gnu
                    // https://github.com/rust-lang-nursery/packed_simd/issues/22
                    // FIXME: broken on WASM32
                    // https://github.com/rust-lang-nursery/packed_simd/issues/91
                    let mut x = self.extract(0);
                    for i in 1..$id::lanes() {
                        x = x.min(self.extract(i));
                    }
                    x
                }
            }
        }
        test_if! {$test_tt:
        paste::item! {
            // Comparisons use integer casts within mantissa^1 range.
            #[allow(clippy::float_cmp)]
            pub mod [<$id _reduction_min_max>] {
                use super::*;
                #[cfg_attr(not(target_arch = "wasm32"), test)]
                #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
                pub fn max_element() {
                    let v = $id::splat(0 as $elem_ty);
                    assert_eq!(v.max_element(), 0 as $elem_ty);
                    if $id::lanes() > 1 {
                        let v = v.replace(1, 1 as $elem_ty);
                        assert_eq!(v.max_element(), 1 as $elem_ty);
                    }
                    let v = v.replace(0, 2 as $elem_ty);
                    assert_eq!(v.max_element(), 2 as $elem_ty);
                }

                #[cfg_attr(not(target_arch = "wasm32"), test)]
                #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
                pub fn min_element() {
                    let v = $id::splat(0 as $elem_ty);
                    assert_eq!(v.min_element(), 0 as $elem_ty);
                    if $id::lanes() > 1 {
                        let v = v.replace(1, 1 as $elem_ty);
                        assert_eq!(v.min_element(), 0 as $elem_ty);
                    }
                    let v = $id::splat(1 as $elem_ty);
                    let v = v.replace(0, 2 as $elem_ty);
                    if $id::lanes() > 1 {
                        assert_eq!(v.min_element(), 1 as $elem_ty);
                    } else {
                        assert_eq!(v.min_element(), 2 as $elem_ty);
                    }
                    if $id::lanes() > 1 {
                        let v = $id::splat(2 as $elem_ty);
                        let v = v.replace(1, 1 as $elem_ty);
                        assert_eq!(v.min_element(), 1 as $elem_ty);
                    }
                }
            }
        }
        }
    };
}

macro_rules! test_reduction_float_min_max {
    ([$elem_ty:ident; $elem_count:expr]: $id:ident | $test_tt:tt) => {
        test_if! {
            $test_tt:
            paste::item! {
                // Comparisons use integer casts within mantissa^1 range.
                #[allow(clippy::float_cmp)]
                pub mod [<$id _reduction_min_max_nan>] {
                    use super::*;
                    #[cfg_attr(not(target_arch = "wasm32"), test)]
                    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
                    fn min_element_test() {
                        let n = crate::$elem_ty::NAN;

                        assert_eq!(n.min(-3.), -3.);
                        assert_eq!((-3. as $elem_ty).min(n), -3.);

                        let v0 = $id::splat(-3.);

                        let target_with_broken_last_lane_nan = !cfg!(any(
                            target_arch = "arm", target_arch = "aarch64",
                            all(target_arch = "x86",
                                not(target_feature = "sse2")
                            ),
                            target_arch = "powerpc64",
                            target_arch = "wasm32",
                        ));

                        // The vector is initialized to `-3.`s: [-3, -3, -3, -3]
                        for i in 0..$id::lanes() {
                            // We replace the i-th element of the vector with
                            // `NaN`: [-3, -3, -3, NaN]
                            let mut v = v0.replace(i, n);

                            // If the NaN is in the last place, the LLVM
                            // implementation of these methods is broken on some
                            // targets:
                            if i == $id::lanes() - 1 &&
                                target_with_broken_last_lane_nan {
                                    assert_eq!(v.min_element(), -3.,
                                            "[A]: nan at {} => {} | {:?}",
                                            i, v.min_element(), v);

                                // If we replace all the elements in the vector
                                // up-to the `i-th` lane with `NaN`s, the result
                                // is still always `-3.` unless all elements of
                                // the vector are `NaN`s:
                                for j in 0..i {
                                    v = v.replace(j, n);
                                    if j == i-1 {
                                        assert!(v.min_element().is_nan(),
                                            "[B]: nan at {} => {} | {:?}",
                                            i, v.min_element(), v);
                                    } else {
                                        assert_eq!(v.min_element(), -3.,
                                            "[B]: nan at {} => {} | {:?}",
                                            i, v.min_element(), v);
                                    }
                                }

                                // We are done here, since we were in the last
                                // lane which is the last iteration of the loop.
                                break
                            }

                            // We are not in the last lane, and there is only
                            // one `NaN` in the vector.

                            // If the vector has one lane, the result is `NaN`:
                            if $id::lanes() == 1 {
                                assert!(v.min_element().is_nan(),
                                        "[C]: all nans | v={:?} | min={} | \
is_nan: {}",
                                        v, v.min_element(),
                                        v.min_element().is_nan()
                                );

                                // And we are done, since the vector only has
                                // one lane anyways.
                                break;
                            }

                            // The vector has more than one lane, since there is
                            // only one `NaN` in the vector, the result is
                            // always `-3`.
                            assert_eq!(v.min_element(), -3.,
                                       "[D]: nan at {} => {} | {:?}",
                                       i, v.min_element(), v);

                            // If we replace all the elements in the vector
                            // up-to the `i-th` lane with `NaN`s, the result is
                            // still always `-3.` unless all elements of the
                            // vector are `NaN`s:
                            for j in 0..i {
                                v = v.replace(j, n);

                                if i == $id::lanes() - 1 && j == i - 1 {
                                    // All elements of the vector are `NaN`s,
                                    // therefore the result is NaN as well.
                                    //
                                    // Note: the #lanes of the vector is > 1, so
                                    // "i - 1" does not overflow.
                                    assert!(v.min_element().is_nan(),
                                            "[E]: all nans | v={:?} | min={} | \
is_nan: {}",
                                            v, v.min_element(),
                                            v.min_element().is_nan());
                                } else {
                                    // There are non-`NaN` elements in the
                                    // vector, therefore the result is `-3.`:
                                    assert_eq!(v.min_element(), -3.,
                                               "[F]: nan at {} => {} | {:?}",
                                               i, v.min_element(), v);
                                }
                            }
                        }

                        // If the vector contains all NaNs the result is NaN:
                        assert!($id::splat(n).min_element().is_nan(),
                                "all nans | v={:?} | min={} | is_nan: {}",
                                $id::splat(n), $id::splat(n).min_element(),
                                $id::splat(n).min_element().is_nan());
                    }
                    #[cfg_attr(not(target_arch = "wasm32"), test)]
                    #[cfg_attr(target_arch = "wasm32", wasm_bindgen_test)]
                    fn max_element_test() {
                        let n = crate::$elem_ty::NAN;

                        assert_eq!(n.max(-3.), -3.);
                        assert_eq!((-3. as $elem_ty).max(n), -3.);

                        let v0 = $id::splat(-3.);

                        let target_with_broken_last_lane_nan = !cfg!(any(
                            target_arch = "arm", target_arch = "aarch64",
                            target_arch = "powerpc64", target_arch = "wasm32",
                        ));

                        // The vector is initialized to `-3.`s: [-3, -3, -3, -3]
                        for i in 0..$id::lanes() {
                            // We replace the i-th element of the vector with
                            // `NaN`: [-3, -3, -3, NaN]
                            let mut v = v0.replace(i, n);

                            // If the NaN is in the last place, the LLVM
                            // implementation of these methods is broken on some
                            // targets:
                            if i == $id::lanes() - 1 &&
                              target_with_broken_last_lane_nan {
                                assert_eq!(v.max_element(), -3.,
                                        "[A]: nan at {} => {} | {:?}",
                                        i, v.max_element(), v);

                                // If we replace all the elements in the vector
                                // up-to the `i-th` lane with `NaN`s, the result
                                // is still always `-3.` unless all elements of
                                // the vector are `NaN`s:
                                for j in 0..i {
                                    v = v.replace(j, n);
                                    if j == i-1 {
                                        assert!(v.min_element().is_nan(),
                                        "[B]: nan at {} => {} | {:?}",
                                        i, v.min_element(), v);
                                    } else {
                                        assert_eq!(v.max_element(), -3.,
                                            "[B]: nan at {} => {} | {:?}",
                                            i, v.max_element(), v);
                                    }
                                }

                                // We are done here, since we were in the last
                                // lane which is the last iteration of the loop.
                                break
                            }

                            // We are not in the last lane, and there is only
                            // one `NaN` in the vector.

                            // If the vector has one lane, the result is `NaN`:
                            if $id::lanes() == 1 {
                                assert!(v.max_element().is_nan(),
                                        "[C]: all nans | v={:?} | min={} | \
is_nan: {}",
                                        v, v.max_element(),
                                        v.max_element().is_nan());

                                // And we are done, since the vector only has
                                // one lane anyways.
                                break;
                            }

                            // The vector has more than one lane, since there is
                            // only one `NaN` in the vector, the result is
                            // always `-3`.
                            assert_eq!(v.max_element(), -3.,
                                       "[D]: nan at {} => {} | {:?}",
                                       i, v.max_element(), v);

                            // If we replace all the elements in the vector
                            // up-to the `i-th` lane with `NaN`s, the result is
                            // still always `-3.` unless all elements of the
                            // vector are `NaN`s:
                            for j in 0..i {
                                v = v.replace(j, n);

                                if i == $id::lanes() - 1 && j == i - 1 {
                                    // All elements of the vector are `NaN`s,
                                    // therefore the result is NaN as well.
                                    //
                                    // Note: the #lanes of the vector is > 1, so
                                    // "i - 1" does not overflow.
                                    assert!(v.max_element().is_nan(),
                                            "[E]: all nans | v={:?} | max={} | \
is_nan: {}",
                                            v, v.max_element(),
                                            v.max_element().is_nan());
                                } else {
                                    // There are non-`NaN` elements in the
                                    // vector, therefore the result is `-3.`:
                                    assert_eq!(v.max_element(), -3.,
                                               "[F]: nan at {} => {} | {:?}",
                                               i, v.max_element(), v);
                                }
                            }
                        }

                        // If the vector contains all NaNs the result is NaN:
                        assert!($id::splat(n).max_element().is_nan(),
                                "all nans | v={:?} | max={} | is_nan: {}",
                                $id::splat(n), $id::splat(n).max_element(),
                                $id::splat(n).max_element().is_nan());
                    }
                }
            }
        }
    };
}
