use std::sync::Weak;

pub enum Atlas<T> {
    Split(Area, Box<[Atlas<T>; 4]>),
    Vacant(Area),
    Occupied(Area, T),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Area {
    pub left: usize,
    pub top: usize,
    pub right: usize,
    pub bottom: usize,
}

impl<T> Atlas<T> {
    pub fn new(size: usize) -> Self {
        Atlas::Vacant(Area {
            left: 0,
            top: 0,
            right: size,
            bottom: size,
        })
    }

    pub fn size(&self) -> usize {
        match self {
            Atlas::Split(area, _) | Atlas::Vacant(area) | Atlas::Occupied(area, _) => area.right - area.left,
        }
    }

    pub fn area(&self) -> Area {
        match self {
            Atlas::Split(area, _) | Atlas::Vacant(area) | Atlas::Occupied(area, _) => area.clone(),
        }
    }

    pub fn insert(&mut self, mut val: T, size: usize) -> Result<Area, T> {
        let size = size.next_power_of_two();
        if size > self.size() {
            panic!(
                "Texture does not fit in configured atlas size! {} > {}",
                size,
                self.size()
            );
        }

        match self {
            &mut Atlas::Split(_, ref mut children) => {
                if size <= children[0].size() {
                    for child in children.iter_mut() {
                        val = match child.insert(val, size) {
                            Ok(area) => return Ok(area),
                            Err(val) => val,
                        };
                    }
                }
                Err(val)
            }

            &mut Atlas::Occupied(_, _) => Err(val),

            vacant => {
                let area = vacant.area();
                if size < area.right - area.left {
                    *vacant = Atlas::Split(
                        area.clone(),
                        Box::new([
                            Atlas::Vacant(Area {
                                left: area.left,
                                top: area.top,
                                right: (area.left + area.right) / 2,
                                bottom: (area.top + area.bottom) / 2,
                            }),
                            Atlas::Vacant(Area {
                                left: (area.left + area.right) / 2,
                                top: area.top,
                                right: area.right,
                                bottom: (area.top + area.bottom) / 2,
                            }),
                            Atlas::Vacant(Area {
                                left: area.left,
                                top: (area.top + area.bottom) / 2,
                                right: (area.left + area.right) / 2,
                                bottom: area.bottom,
                            }),
                            Atlas::Vacant(Area {
                                left: (area.left + area.right) / 2,
                                top: (area.top + area.bottom) / 2,
                                right: area.right,
                                bottom: area.bottom,
                            }),
                        ]),
                    );

                    vacant.insert(val, size)
                } else {
                    *vacant = Atlas::Occupied(area.clone(), val);
                    Ok(area)
                }
            }
        }
    }
}

impl<T> Atlas<Weak<T>> {
    pub fn remove_expired(&mut self) -> bool {
        let (area, empty) = match self {
            Atlas::Split(area, children) => (
                area.clone(),
                children
                    .iter_mut()
                    .fold(true, |empty, child| child.remove_expired() && empty),
            ),
            Atlas::Vacant(area) => (area.clone(), true),
            Atlas::Occupied(area, content) => {
                if content.strong_count() == 0 {
                    (area.clone(), true)
                } else {
                    (area.clone(), false)
                }
            }
        };

        if empty {
            *self = Atlas::Vacant(area);
            true
        } else {
            false
        }
    }
}
