#![allow(dead_code)]

use std::cmp::Ordering;

use crate::{
    algorithm::{partition, quick_select},
    bbox::BBox,
    lerp::lerp_slice,
    math::{dot, Vector},
    sampling::uniform_sample_hemisphere,
};

const SAH_BIN_COUNT: usize = 13; // Prime numbers work best, for some reason
const SPLIT_PLANE_COUNT: usize = 5;

/// Takes a slice of boundable objects and partitions them based on the Surface
/// Area Heuristic, but using arbitrarily oriented planes.
///
/// Returns the index of the partition boundary and the axis that it split on
/// (0 = x, 1 = y, 2 = z).
pub fn free_sah_split<'a, T, F>(seed: u32, objects: &mut [T], bounder: &F) -> (usize, usize)
where
    F: Fn(&T) -> &'a [BBox],
{
    // Generate the planes for splitting
    let planes = {
        let mut planes = [Vector::new(0.0, 0.0, 0.0); SPLIT_PLANE_COUNT];
        let offset = seed * SPLIT_PLANE_COUNT as u32;
        for i in 0..SPLIT_PLANE_COUNT {
            let u = halton::sample(0, offset + i as u32);
            let v = halton::sample(1, offset + i as u32);
            planes[i] = uniform_sample_hemisphere(u, v).normalized();
        }
        planes
    };

    // Get the extents of the objects with respect to the split planes
    let extents = {
        let mut extents = [(std::f32::INFINITY, std::f32::NEG_INFINITY); SPLIT_PLANE_COUNT];
        for obj in &objects[..] {
            let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector();
            for i in 0..SPLIT_PLANE_COUNT {
                let dist = dot(centroid, planes[i]);
                extents[i].0 = extents[i].0.min(dist);
                extents[i].1 = extents[i].1.max(dist);
            }
        }
        extents
    };

    // Pre-calc SAH div distances
    let sah_divs = {
        let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT];
        for pi in 0..SPLIT_PLANE_COUNT {
            let extent = extents[pi].1 - extents[pi].0;
            for div in 0..(SAH_BIN_COUNT - 1) {
                let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32);
                sah_divs[pi][div] = extents[pi].0 + part;
            }
        }
        sah_divs
    };

    // Build SAH bins
    let sah_bins = {
        let mut sah_bins =
            [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; SPLIT_PLANE_COUNT];
        for obj in objects.iter() {
            let tb = lerp_slice(bounder(obj), 0.5);
            let centroid = tb.center().into_vector();

            for pi in 0..SPLIT_PLANE_COUNT {
                for div in 0..(SAH_BIN_COUNT - 1) {
                    let dist = dot(centroid, planes[pi]);
                    if dist <= sah_divs[pi][div] {
                        sah_bins[pi][div].0 |= tb;
                        sah_bins[pi][div].2 += 1;
                    } else {
                        sah_bins[pi][div].1 |= tb;
                        sah_bins[pi][div].3 += 1;
                    }
                }
            }
        }
        sah_bins
    };

    // Find best split axis and div point
    let (split_plane_i, div_n) = {
        let mut split_plane_i = 0;
        let mut div_n = 0.0;
        let mut smallest_cost = std::f32::INFINITY;

        for pi in 0..SPLIT_PLANE_COUNT {
            for div in 0..(SAH_BIN_COUNT - 1) {
                let left_cost = sah_bins[pi][div].0.surface_area() * sah_bins[pi][div].2 as f32;
                let right_cost = sah_bins[pi][div].1.surface_area() * sah_bins[pi][div].3 as f32;
                let tot_cost = left_cost + right_cost;
                if tot_cost < smallest_cost {
                    split_plane_i = pi;
                    div_n = sah_divs[pi][div];
                    smallest_cost = tot_cost;
                }
            }
        }

        (split_plane_i, div_n)
    };

    // Calculate the approximate axis-aligned split, along with flipping the split plane as
    // appropriate.
    let (plane, approx_axis, div) = {
        // Find axis with largest value
        let mut largest_axis = 0;
        let mut n = 0.0;
        for d in 0..3 {
            let m = planes[split_plane_i].get_n(d).abs();
            if n < m {
                largest_axis = d;
                n = m;
            }
        }

        // If it's negative, flip
        if planes[split_plane_i].get_n(largest_axis).is_sign_positive() {
            (planes[split_plane_i], largest_axis, div_n)
        } else {
            (planes[split_plane_i] * -1.0, largest_axis, div_n * -1.0)
        }
    };

    // Partition
    let mut split_i = partition(&mut objects[..], |obj| {
        let centroid = lerp_slice(bounder(obj), 0.5).center().into_vector();
        let dist = dot(centroid, plane);
        dist < div
    });

    if split_i < 1 {
        split_i = 1;
    } else if split_i >= objects.len() {
        split_i = objects.len() - 1;
    }

    (split_i, approx_axis)
}

/// Takes a slice of boundable objects and partitions them based on the Surface
/// Area Heuristic.
///
/// Returns the index of the partition boundary and the axis that it split on
/// (0 = x, 1 = y, 2 = z).
pub fn sah_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)
where
    F: Fn(&T) -> &'a [BBox],
{
    // Get combined object centroid extents
    let bounds = {
        let mut bb = BBox::new();
        for obj in &objects[..] {
            bb |= lerp_slice(bounder(obj), 0.5).center();
        }
        bb
    };

    // Pre-calc SAH div points
    let sah_divs = {
        let mut sah_divs = [[0.0f32; SAH_BIN_COUNT - 1]; 3];
        for d in 0..sah_divs.len() {
            let extent = bounds.max.get_n(d) - bounds.min.get_n(d);
            for div in 0..(SAH_BIN_COUNT - 1) {
                let part = extent * ((div + 1) as f32 / SAH_BIN_COUNT as f32);
                sah_divs[d][div] = bounds.min.get_n(d) + part;
            }
        }
        sah_divs
    };

    // Build SAH bins
    let sah_bins = {
        let mut sah_bins = [[(BBox::new(), BBox::new(), 0, 0); SAH_BIN_COUNT - 1]; 3];
        for obj in objects.iter() {
            let tb = lerp_slice(bounder(obj), 0.5);
            let centroid = (tb.min.into_vector() + tb.max.into_vector()) * 0.5;

            for d in 0..3 {
                for div in 0..(SAH_BIN_COUNT - 1) {
                    if centroid.get_n(d) <= sah_divs[d][div] {
                        sah_bins[d][div].0 |= tb;
                        sah_bins[d][div].2 += 1;
                    } else {
                        sah_bins[d][div].1 |= tb;
                        sah_bins[d][div].3 += 1;
                    }
                }
            }
        }
        sah_bins
    };

    // Find best split axis and div point
    let (split_axis, div) = {
        let mut dim = 0;
        let mut div_n = 0.0;
        let mut smallest_cost = std::f32::INFINITY;

        for d in 0..3 {
            for div in 0..(SAH_BIN_COUNT - 1) {
                let left_cost = sah_bins[d][div].0.surface_area() * sah_bins[d][div].2 as f32;
                let right_cost = sah_bins[d][div].1.surface_area() * sah_bins[d][div].3 as f32;
                let left_diag = sah_bins[d][div].0.diagonal();
                let right_diag = sah_bins[d][div].1.diagonal();
                let tot_cost = (left_cost * left_diag) + (right_cost * right_diag);
                if tot_cost < smallest_cost {
                    dim = d;
                    div_n = sah_divs[d][div];
                    smallest_cost = tot_cost;
                }
            }
        }

        (dim, div_n)
    };

    // Partition
    let mut split_i = partition(&mut objects[..], |obj| {
        let tb = lerp_slice(bounder(obj), 0.5);
        let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5;
        centroid < div
    });
    if split_i < 1 {
        split_i = 1;
    } else if split_i >= objects.len() {
        split_i = objects.len() - 1;
    }

    (split_i, split_axis)
}

/// Takes a slice of boundable objects and partitions them based on the bounds mean heuristic.
///
/// Returns the index of the partition boundary and the axis that it split on
/// (0 = x, 1 = y, 2 = z).
pub fn bounds_mean_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)
where
    F: Fn(&T) -> &'a [BBox],
{
    // Get combined object bounds
    let bounds = {
        let mut bb = BBox::new();
        for obj in &objects[..] {
            bb |= lerp_slice(bounder(obj), 0.5);
        }
        bb
    };

    let split_axis = {
        let mut axis = 0;
        let mut largest = std::f32::NEG_INFINITY;
        for i in 0..3 {
            let extent = bounds.max.get_n(i) - bounds.min.get_n(i);
            if extent > largest {
                largest = extent;
                axis = i;
            }
        }
        axis
    };

    let div = (bounds.min.get_n(split_axis) + bounds.max.get_n(split_axis)) * 0.5;

    // Partition
    let mut split_i = partition(&mut objects[..], |obj| {
        let tb = lerp_slice(bounder(obj), 0.5);
        let centroid = (tb.min.get_n(split_axis) + tb.max.get_n(split_axis)) * 0.5;
        centroid < div
    });
    if split_i < 1 {
        split_i = 1;
    } else if split_i >= objects.len() {
        split_i = objects.len() - 1;
    }

    (split_i, split_axis)
}

/// Takes a slice of boundable objects and partitions them based on the median heuristic.
///
/// Returns the index of the partition boundary and the axis that it split on
/// (0 = x, 1 = y, 2 = z).
pub fn median_split<'a, T, F>(objects: &mut [T], bounder: &F) -> (usize, usize)
where
    F: Fn(&T) -> &'a [BBox],
{
    // Get combined object bounds
    let bounds = {
        let mut bb = BBox::new();
        for obj in &objects[..] {
            bb |= lerp_slice(bounder(obj), 0.5);
        }
        bb
    };

    let split_axis = {
        let mut axis = 0;
        let mut largest = std::f32::NEG_INFINITY;
        for i in 0..3 {
            let extent = bounds.max.get_n(i) - bounds.min.get_n(i);
            if extent > largest {
                largest = extent;
                axis = i;
            }
        }
        axis
    };

    let place = {
        let place = objects.len() / 2;
        if place > 0 {
            place
        } else {
            1
        }
    };
    quick_select(objects, place, |a, b| {
        let tb_a = lerp_slice(bounder(a), 0.5);
        let tb_b = lerp_slice(bounder(b), 0.5);
        let centroid_a = (tb_a.min.get_n(split_axis) + tb_a.max.get_n(split_axis)) * 0.5;
        let centroid_b = (tb_b.min.get_n(split_axis) + tb_b.max.get_n(split_axis)) * 0.5;

        if centroid_a < centroid_b {
            Ordering::Less
        } else if centroid_a == centroid_b {
            Ordering::Equal
        } else {
            Ordering::Greater
        }
    });

    (place, split_axis)
}
