use super::Packer;
use crate::config::{GuillotineChoice, GuillotineSplit, PackerConfig};
use crate::free_space::{guillotine_score, guillotine_split, merge_adjacent, prune_contained};
use crate::geometry::{PackingContext, PlacementGeometry};
use crate::model::{Frame, Rect};

pub struct GuillotinePacker {
    config: PackerConfig,
    free: Vec<Rect>,
    used: Vec<Rect>,
    choice: GuillotineChoice,
    split: GuillotineSplit,
}

impl GuillotinePacker {
    pub fn new(config: PackerConfig, choice: GuillotineChoice, split: GuillotineSplit) -> Self {
        let border = PackingContext::new(&config).usable_area();
        Self {
            config,
            free: vec![border],
            used: Vec::new(),
            choice,
            split,
        }
    }

    fn choose(&self, w: u32, h: u32) -> Option<(usize, Rect, bool)> {
        let mut best_idx = None;
        let mut best_score = i128::MAX;
        let mut best_rect = Rect::new(0, 0, 0, 0);
        let mut best_rot = false;
        for (i, fr) in self.free.iter().enumerate() {
            if fr.w >= w && fr.h >= h {
                let s = guillotine_score(&self.choice, fr, w, h).0;
                if s < best_score {
                    best_score = s;
                    best_idx = Some(i);
                    best_rect = Rect::new(fr.x, fr.y, w, h);
                    best_rot = false;
                }
            }
            if self.config.allow_rotation && fr.w >= h && fr.h >= w {
                let s = guillotine_score(&self.choice, fr, h, w).0;
                if s < best_score {
                    best_score = s;
                    best_idx = Some(i);
                    best_rect = Rect::new(fr.x, fr.y, h, w);
                    best_rot = true;
                }
            }
        }
        best_idx.map(|idx| (idx, best_rect, best_rot))
    }

    fn place(&mut self, idx: usize, placed: &Rect) {
        let fr = self.free[idx];
        self.free.swap_remove(idx);
        let (a, b) = guillotine_split(&self.split, &fr, placed);
        if let Some(r) = a {
            self.free.push(r);
        }
        if let Some(r) = b {
            self.free.push(r);
        }
        self.prune_free_list();
        self.merge_free_list();
        self.used.push(*placed);
    }

    fn prune_free_list(&mut self) {
        prune_contained(&mut self.free);
    }

    fn merge_free_list(&mut self) {
        merge_adjacent(&mut self.free);
    }
}

impl<K: Clone> Packer<K> for GuillotinePacker {
    fn can_pack(&self, rect: &Rect) -> bool {
        let geometry = PlacementGeometry::new(rect, &self.config);
        self.choose(geometry.reserved_w, geometry.reserved_h)
            .is_some()
    }

    fn pack(&mut self, key: K, rect: &Rect) -> Option<Frame<K>> {
        let geometry = PlacementGeometry::new(rect, &self.config);
        if let Some((idx, place, rotated)) = self.choose(geometry.reserved_w, geometry.reserved_h) {
            self.place(idx, &place);
            Some(geometry.frame(key, *rect, &place, rotated))
        } else {
            None
        }
    }
}
