use crate::core::image::Image;
use crate::utils::Arena;
use shared::Float;
use shared::core::geometry::{Point2i, Vector2f, Vector2i};
use shared::utils::Ptr;
use shared::utils::sampling::{
    AliasTable, Bin, DevicePiecewiseConstant1D, DevicePiecewiseConstant2D, PiecewiseLinear2D,
};
use std::sync::Arc;

#[derive(Debug, Clone)]
pub struct PiecewiseConstant1D {
    func: Box<[Float]>,
    cdf: Box<[Float]>,
    pub device: DevicePiecewiseConstant1D,
}

impl PiecewiseConstant1D {
    // Constructors
    pub fn new(f: &[Float]) -> Self {
        Self::new_with_bounds(f.to_vec(), 0.0, 1.0)
    }

    pub fn to_shared(&self, arena: &mut Arena) -> DevicePiecewiseConstant1D {
        let (func_ptr, _) = arena.alloc_slice(&self.func);
        let (cdf_ptr, _) = arena.alloc_slice(&self.cdf);

        DevicePiecewiseConstant1D {
            func: func_ptr,
            cdf: cdf_ptr,
            func_integral: self.func_integral,
            n: self.func.len() as u32,
            min: self.min,
            max: self.max,
        }
    }

    pub fn from_func<F>(f: F, min: Float, max: Float, n: usize) -> Self
    where
        F: Fn(Float) -> Float,
    {
        let delta = (max - min) / n as Float;
        let values: Vec<Float> = (0..n)
            .map(|i| {
                let x = min + (i as Float + 0.5) * delta;
                f(x)
            })
            .collect();

        Self::new_with_bounds(values, min, max)
    }

    pub fn new_with_bounds(f: Vec<Float>, min: Float, max: Float) -> Self {
        let n = f.len();
        let mut cdf = Vec::with_capacity(n + 1);
        cdf.push(0.0);

        let delta = (max - min) / n as Float;
        for i in 0..n {
            cdf.push(cdf[i] + f[i] * delta);
        }

        let func_integral = cdf[n];

        if func_integral > 0.0 {
            for c in &mut cdf {
                *c /= func_integral;
            }
        }

        // Convert to boxed slices (no more reallocation possible)
        let func: Box<[Float]> = f.into_boxed_slice();
        let cdf: Box<[Float]> = cdf.into_boxed_slice();

        let device = DevicePiecewiseConstant1D {
            func: func.as_ptr().into(),
            cdf: cdf.as_ptr().into(),
            min,
            max,
            n: n as u32,
            func_integral,
        };

        Self { func, cdf, device }
    }

    // Accessors
    pub fn min(&self) -> Float {
        self.device.min
    }
    pub fn max(&self) -> Float {
        self.device.max
    }
    pub fn n(&self) -> usize {
        self.device.n as usize
    }
    pub fn integral(&self) -> Float {
        self.device.func_integral
    }

    pub fn func(&self) -> &[Float] {
        &self.func
    }
    pub fn cdf(&self) -> &[Float] {
        &self.cdf
    }
}

impl std::ops::Deref for PiecewiseConstant1D {
    type Target = DevicePiecewiseConstant1D;

    fn deref(&self) -> &Self::Target {
        &self.device
    }
}

pub struct PiecewiseConstant2D {
    conditionals: Vec<PiecewiseConstant1D>,
    marginal: PiecewiseConstant1D,
    conditional_devices: Box<[DevicePiecewiseConstant1D]>,
    pub device: DevicePiecewiseConstant2D,
}

impl PiecewiseConstant2D {
    pub fn new(data: &[Float], n_u: usize, n_v: usize) -> Self {
        assert_eq!(data.len(), n_u * n_v);

        // Build conditional distributions p(u|v) for each row
        let mut conditionals = Vec::with_capacity(n_v);
        let mut marginal_func = Vec::with_capacity(n_v);

        for v in 0..n_v {
            let row_start = v * n_u;
            let row: Vec<Float> = data[row_start..row_start + n_u].to_vec();

            let conditional = PiecewiseConstant1D::new_with_bounds(row, 0.0, 1.0);
            marginal_func.push(conditional.integral());
            conditionals.push(conditional);
        }

        // Build marginal distribution p(v)
        let marginal = PiecewiseConstant1D::new_with_bounds(marginal_func, 0.0, 1.0);

        // Create array of device structs
        let conditional_devices: Box<[DevicePiecewiseConstant1D]> = conditionals
            .iter()
            .map(|c| c.device)
            .collect::<Vec<_>>()
            .into_boxed_slice();

        let device = DevicePiecewiseConstant2D {
            conditionals: conditional_devices.as_ptr(),
            marginal: marginal.device,
            n_u: n_u as u32,
            n_v: n_v as u32,
        };

        Self {
            conditionals,
            marginal,
            conditional_devices,
            device,
        }
    }

    pub fn from_image(image: &Image) -> Self {
        let res = image.resolution();
        let n_u = res.x() as usize;
        let n_v = res.y() as usize;

        let mut data = Vec::with_capacity(n_u * n_v);

        for v in 0..n_v {
            for u in 0..n_u {
                let p = Point2i::new(u as i32, v as i32);
                let luminance = image.get_channels(p).average();
                data.push(luminance);
            }
        }

        Self::new(&data, n_u, n_v)
    }

    pub fn integral(&self) -> Float {
        self.marginal.integral()
    }
}

struct PiecewiseLinear2DStorage<const N: usize> {
    data: Vec<Float>,
    marginal_cdf: Vec<Float>,
    conditional_cdf: Vec<Float>,
    param_values: [Vec<Float>; N],
}

pub struct PiecewiseLinear2DHost<const N: usize> {
    pub view: PiecewiseLinear2D<N>,
    _storage: Arc<PiecewiseLinear2DStorage<N>>,
}

impl<const N: usize> PiecewiseLinear2DHost<N> {
    pub fn new(
        data: &[Float],
        x_size: i32,
        y_size: i32,
        param_res: [usize; N],
        param_values: [&[Float]; N],
        normalize: bool,
        build_cdf: bool,
    ) -> Self {
        if build_cdf && !normalize {
            panic!("PiecewiseLinear2D::new: build_cdf implies normalize=true");
        }

        let size = Vector2i::new(x_size, y_size);
        let inv_patch_size = Vector2f::new(1. / (x_size - 1) as Float, 1. / (y_size - 1) as Float);

        let mut param_size = [0u32; N];
        let mut param_strides = [0u32; N];
        let param_values = std::array::from_fn(|i| param_values[i].to_vec());

        let mut slices: u32 = 1;
        for i in (0..N).rev() {
            if param_res[i] < 1 {
                panic!("PiecewiseLinear2D::new: parameter resolution must be >= 1!");
            }
            param_size[i] = param_res[i] as u32;
            param_strides[i] = if param_res[i] > 1 { slices } else { 0 };
            slices *= param_size[i];
        }

        let n_values = (x_size * y_size) as usize;
        let mut new_data = vec![0.0; slices as usize * n_values];
        let mut marginal_cdf = if build_cdf {
            vec![0.0; slices as usize * y_size as usize]
        } else {
            Vec::new()
        };
        let mut conditional_cdf = if build_cdf {
            vec![0.0; slices as usize * n_values]
        } else {
            Vec::new()
        };

        let mut data_offset = 0;
        for slice in 0..slices as usize {
            let slice_offset = slice * n_values;
            let current_data = &data[data_offset..data_offset + n_values];
            let mut sum = 0.;

            // Construct conditional CDF
            if normalize {
                for y in 0..(y_size - 1) {
                    for x in 0..(x_size - 1) {
                        let i = (y * x_size + x) as usize;
                        let v00 = current_data[i] as f64;
                        let v10 = current_data[i + 1] as f64;
                        let v01 = current_data[i + x_size as usize] as f64;
                        let v11 = current_data[i + 1 + x_size as usize] as f64;
                        sum += 0.25 * (v00 + v10 + v01 + v11);
                    }
                }
            }

            let normalization = if normalize && sum > 0.0 {
                1.0 / sum as Float
            } else {
                1.0
            };
            for k in 0..n_values {
                new_data[slice_offset + k] = current_data[k] * normalization;
            }

            if build_cdf {
                let marginal_slice_offset = slice * y_size as usize;
                // Construct marginal CDF
                for y in 0..y_size as usize {
                    let mut cdf_sum = 0.0;
                    let i_base = y * x_size as usize;
                    conditional_cdf[slice_offset + i_base] = 0.0;
                    for x in 0..(x_size - 1) as usize {
                        let i = i_base + x;
                        cdf_sum +=
                            0.5 * (new_data[slice_offset + i] + new_data[slice_offset + i + 1]);
                        conditional_cdf[slice_offset + i + 1] = cdf_sum;
                    }
                }
                // Construct marginal CDF
                marginal_cdf[marginal_slice_offset] = 0.0;
                let mut marginal_sum = 0.0;
                for y in 0..(y_size - 1) as usize {
                    let cdf1 = conditional_cdf[slice_offset + (y + 1) * x_size as usize - 1];
                    let cdf2 = conditional_cdf[slice_offset + (y + 2) * x_size as usize - 1];
                    marginal_sum += 0.5 * (cdf1 + cdf2);
                    marginal_cdf[marginal_slice_offset + y + 1] = marginal_sum;
                }
            }
            data_offset += n_values;
        }

        let view = PiecewiseLinear2D {
            size,
            inv_patch_size,
            param_size,
            param_strides,
            param_values: param_values.each_ref().map(|x| x.as_ptr().into()),
            data: Ptr::null(),
            marginal_cdf: marginal_cdf.as_ptr().into(),
            conditional_cdf: conditional_cdf.as_ptr().into(),
        };

        let storage = Arc::new(PiecewiseLinear2DStorage {
            data: new_data,
            marginal_cdf,
            conditional_cdf,
            param_values,
        });

        let mut final_view = view;
        final_view.data = storage.data.as_ptr().into();
        final_view.marginal_cdf = storage.marginal_cdf.as_ptr().into();
        final_view.conditional_cdf = storage.conditional_cdf.as_ptr().into();
        for i in 0..N {
            final_view.param_values[i] = storage.param_values[i].as_ptr().into();
        }

        Self {
            view: final_view,
            _storage: storage,
        }
    }
}

#[derive(Copy, Debug, Clone)]
pub struct AliasTableHost {
    pub view: AliasTable,
    _storage: Vec<Bin>,
}

impl AliasTableHost {
    pub fn new(weights: &[Float]) -> Self {
        let n = weights.len();
        if n == 0 {
            return Self {
                view: AliasTable {
                    bins: Ptr::null(),
                    size: 0,
                },
                _storage: Vec::new(),
            };
        }

        let sum: f64 = weights.iter().map(|&w| w as f64).sum();
        assert!(sum > 0.0, "Sum of weights must be positive");
        let mut bins = Vec::with_capacity(n);
        for &w in weights {
            bins.push(Bin {
                p: (w as f64 / sum) as Float,
                q: 0.0,
                alias: 0,
            });
        }

        struct Outcome {
            p_hat: f64,
            index: usize,
        }

        let mut under = Vec::with_capacity(n);
        let mut over = Vec::with_capacity(n);

        for (i, bin) in bins.iter().enumerate() {
            let p_hat = (bin.p as f64) * (n as f64);
            if p_hat < 1.0 {
                under.push(Outcome { p_hat, index: i });
            } else {
                over.push(Outcome { p_hat, index: i });
            }
        }

        while !under.is_empty() && !over.is_empty() {
            let un = under.pop().unwrap();
            let ov = over.pop().unwrap();

            bins[un.index].q = un.p_hat as Float;
            bins[un.index].alias = ov.index as u32;

            let p_excess = un.p_hat + ov.p_hat - 1.0;

            if p_excess < 1.0 {
                under.push(Outcome {
                    p_hat: p_excess,
                    index: ov.index,
                });
            } else {
                over.push(Outcome {
                    p_hat: p_excess,
                    index: ov.index,
                });
            }
        }

        while let Some(ov) = over.pop() {
            bins[ov.index].q = 1.0;
            bins[ov.index].alias = ov.index as u32;
        }

        while let Some(un) = under.pop() {
            bins[un.index].q = 1.0;
            bins[un.index].alias = un.index as u32;
        }

        let view = AliasTable {
            bins: bins.as_ptr().into(),
            size: bins.len() as u32,
        };

        Self {
            view,
            _storage: bins,
        }
    }
}
