use std::fmt;
use std::ops::{Add, Sub};

/// Unified result of a bin packing or partitioning algorithm.
///
/// - `capacity`: `Some(limit)` for bin packing (FFD/BFD), `None` for unconstrained partitioning (KK).
/// - `remaining[i]`: unused space in bin i; zero for partitioning results.
/// - `sums[i]`: total item weight assigned to bin/group i.
#[derive(Debug)]
pub struct BinPacking<T> {
    pub bins: Vec<Vec<T>>,
    pub sums: Vec<T>,
    pub remaining: Vec<T>,
    pub capacity: Option<T>,
}

impl<T> BinPacking<T>
where
    T: Ord + Clone + Sub<Output = T>,
{
    /// Number of bins / groups used.
    pub fn num_bins(&self) -> usize {
        self.bins.len()
    }

    /// Difference between the largest and smallest bin sum.
    pub fn imbalance(&self) -> T {
        let max = self.sums.iter().max().unwrap().clone();
        let min = self.sums.iter().min().unwrap().clone();
        max - min
    }

    /// Total wasted space across all bins (sum of `remaining`).
    /// For partitioning results this is always zero.
    pub fn waste(&self) -> T
    where
        T: Default + Add<Output = T>,
    {
        self.remaining
            .iter()
            .cloned()
            .fold(T::default(), |acc, r| acc + r)
    }
}

impl<T: fmt::Display> fmt::Display for BinPacking<T> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        for (i, ((bin, sum), rem)) in self
            .bins
            .iter()
            .zip(self.sums.iter())
            .zip(self.remaining.iter())
            .enumerate()
        {
            match &self.capacity {
                Some(_) => write!(f, "Bin {i} (sum={sum}, remaining={rem}): [")?,
                None => write!(f, "Bin {i} (sum={sum}): [")?,
            }
            for (j, item) in bin.iter().enumerate() {
                if j > 0 {
                    write!(f, ", ")?;
                }
                write!(f, "{item}")?;
            }
            writeln!(f, "]")?;
        }
        Ok(())
    }
}

/// First Fit Decreasing bin packing.
///
/// Sorts items descending, then for each item tries bins in order (0, 1, 2, ...)
/// and places it in the **first** bin with enough remaining capacity.
/// Opens a new bin only when no existing bin fits.
/// Capacity defaults to the sum of all items when not provided.
pub fn first_fit_decreasing<T>(mut items: Vec<T>, capacity: Option<T>) -> BinPacking<T>
where
    T: Ord + Add<Output = T> + Sub<Output = T> + Default + Clone,
{
    let capacity = capacity
        .unwrap_or_else(|| items.iter().cloned().fold(T::default(), |acc, x| acc + x));

    items.sort_unstable_by(|a, b| b.cmp(a));

    let mut bins: Vec<Vec<T>> = Vec::new();
    let mut sums: Vec<T> = Vec::new();
    let mut remaining: Vec<T> = Vec::new();

    for item in items {
        assert!(item <= capacity, "item exceeds bin capacity");

        // First bin with enough room.
        let target = bins
            .iter()
            .enumerate()
            .find(|(i, _)| remaining[*i] >= item)
            .map(|(i, _)| i);

        match target {
            Some(i) => {
                remaining[i] = remaining[i].clone() - item.clone();
                sums[i] = sums[i].clone() + item.clone();
                bins[i].push(item);
            }
            None => {
                remaining.push(capacity.clone() - item.clone());
                sums.push(item.clone());
                bins.push(vec![item]);
            }
        }
    }

    BinPacking {
        bins,
        sums,
        remaining,
        capacity: Some(capacity),
    }
}

/// Best Fit Decreasing bin packing.
///
/// Sorts items descending, then for each item finds the bin with the
/// **minimum remaining capacity** that still fits the item (tightest fit).
/// This leaves larger gaps available for larger future items.
/// Opens a new bin only when no existing bin fits.
/// Capacity defaults to the sum of all items when not provided.
pub fn best_fit_decreasing<T>(mut items: Vec<T>, capacity: Option<T>) -> BinPacking<T>
where
    T: Ord + Add<Output = T> + Sub<Output = T> + Default + Clone,
{
    let capacity = capacity
        .unwrap_or_else(|| items.iter().cloned().fold(T::default(), |acc, x| acc + x));

    items.sort_unstable_by(|a, b| b.cmp(a));

    let mut bins: Vec<Vec<T>> = Vec::new();
    let mut sums: Vec<T> = Vec::new();
    let mut remaining: Vec<T> = Vec::new();

    for item in items {
        assert!(item <= capacity, "item exceeds bin capacity");

        // Bin with the smallest remaining space that still fits (tightest fit).
        let best_idx = remaining
            .iter()
            .enumerate()
            .filter(|(_, r)| *r >= &item)
            .min_by(|(_, a), (_, b)| a.cmp(b))
            .map(|(i, _)| i);

        match best_idx {
            Some(i) => {
                remaining[i] = remaining[i].clone() - item.clone();
                sums[i] = sums[i].clone() + item.clone();
                bins[i].push(item);
            }
            None => {
                remaining.push(capacity.clone() - item.clone());
                sums.push(item.clone());
                bins.push(vec![item]);
            }
        }
    }

    BinPacking {
        bins,
        sums,
        remaining,
        capacity: Some(capacity),
    }
}
