use crate::config::{PackerConfig, SortOrder, TransparentPolicy};
use crate::model::Rect;
use crate::pipeline::{InputImage, LayoutItem};
use image::RgbaImage;

pub(crate) struct PreparedItem<T> {
    pub(crate) key: String,
    pub(crate) payload: T,
    pub(crate) rect: Rect,
    pub(crate) trimmed: bool,
    pub(crate) source: Rect,
    pub(crate) orig_size: (u32, u32),
}

pub(crate) fn prepare_images(
    inputs: &[InputImage],
    cfg: &PackerConfig,
) -> Vec<PreparedItem<RgbaImage>> {
    let mut out = Vec::with_capacity(inputs.len());
    for input in inputs {
        let rgba = input.image.to_rgba8();
        if let Some(item) = prepare_image(input.key.clone(), rgba, cfg) {
            out.push(item);
        }
    }
    sort_prepared(&mut out, cfg);
    out
}

fn prepare_image(
    key: String,
    rgba: RgbaImage,
    cfg: &PackerConfig,
) -> Option<PreparedItem<RgbaImage>> {
    let (width, height) = rgba.dimensions();
    let (rect, trimmed, source) = if cfg.trim {
        let (trim_rect_opt, source_rect) = compute_trim_rect(&rgba, cfg.trim_threshold);
        match trim_rect_opt {
            Some(rect) => (Rect::new(0, 0, rect.w, rect.h), true, source_rect),
            None => match cfg.transparent_policy {
                TransparentPolicy::Keep => (
                    Rect::new(0, 0, width, height),
                    false,
                    Rect::new(0, 0, width, height),
                ),
                TransparentPolicy::OneByOne => (Rect::new(0, 0, 1, 1), true, Rect::new(0, 0, 1, 1)),
                TransparentPolicy::Skip => return None,
            },
        }
    } else {
        (
            Rect::new(0, 0, width, height),
            false,
            Rect::new(0, 0, width, height),
        )
    };

    Some(PreparedItem {
        key,
        payload: rgba,
        rect,
        trimmed,
        source,
        orig_size: (width, height),
    })
}

pub(crate) fn prepare_layout<K: Into<String>>(
    inputs: Vec<(K, u32, u32)>,
    cfg: &PackerConfig,
) -> Vec<PreparedItem<()>> {
    let mut prepared = inputs
        .into_iter()
        .map(|(key, width, height)| {
            let key = key.into();
            let rect = Rect::new(0, 0, width, height);
            PreparedItem {
                key,
                payload: (),
                rect,
                trimmed: false,
                source: rect,
                orig_size: (width, height),
            }
        })
        .collect::<Vec<_>>();
    sort_prepared(&mut prepared, cfg);
    prepared
}

pub(crate) fn prepare_layout_items<K: Into<String>>(
    items: Vec<LayoutItem<K>>,
    cfg: &PackerConfig,
) -> Vec<PreparedItem<()>> {
    let mut prepared = items
        .into_iter()
        .map(|item| {
            let key = item.key.into();
            let rect = Rect::new(0, 0, item.w, item.h);
            let source = item.source.unwrap_or(rect);
            let orig_size = item.source_size.unwrap_or((item.w, item.h));
            PreparedItem {
                key,
                payload: (),
                rect,
                trimmed: item.trimmed,
                source,
                orig_size,
            }
        })
        .collect::<Vec<_>>();
    sort_prepared(&mut prepared, cfg);
    prepared
}

pub(crate) fn sort_prepared<T>(prepared: &mut [PreparedItem<T>], cfg: &PackerConfig) {
    match cfg.sort_order {
        SortOrder::None => {}
        SortOrder::NameAsc => {
            prepared.sort_by(|a, b| a.key.cmp(&b.key));
        }
        SortOrder::AreaDesc => {
            prepared.sort_by(|a, b| {
                (b.rect.w * b.rect.h)
                    .cmp(&(a.rect.w * a.rect.h))
                    .then_with(|| a.key.cmp(&b.key))
            });
        }
        SortOrder::MaxSideDesc => {
            prepared.sort_by(|a, b| {
                b.rect
                    .w
                    .max(b.rect.h)
                    .cmp(&a.rect.w.max(a.rect.h))
                    .then_with(|| a.key.cmp(&b.key))
            });
        }
        SortOrder::HeightDesc => {
            prepared.sort_by(|a, b| b.rect.h.cmp(&a.rect.h).then_with(|| a.key.cmp(&b.key)));
        }
        SortOrder::WidthDesc => {
            prepared.sort_by(|a, b| b.rect.w.cmp(&a.rect.w).then_with(|| a.key.cmp(&b.key)));
        }
    }
}

pub fn compute_trim_rect(rgba: &RgbaImage, threshold: u8) -> (Option<Rect>, Rect) {
    let (width, height) = rgba.dimensions();
    let mut x1 = 0;
    let mut y1 = 0;
    let mut x2 = width.saturating_sub(1);
    let mut y2 = height.saturating_sub(1);

    while x1 < width {
        let mut all_transparent = true;
        for y in 0..height {
            if rgba.get_pixel(x1, y)[3] > threshold {
                all_transparent = false;
                break;
            }
        }
        if all_transparent {
            x1 += 1;
        } else {
            break;
        }
    }

    if x1 >= width {
        return (None, Rect::new(0, 0, width, height));
    }

    while x2 > x1 {
        let mut all_transparent = true;
        for y in 0..height {
            if rgba.get_pixel(x2, y)[3] > threshold {
                all_transparent = false;
                break;
            }
        }
        if all_transparent {
            x2 -= 1;
        } else {
            break;
        }
    }

    while y1 < height {
        let mut all_transparent = true;
        for x in x1..=x2 {
            if rgba.get_pixel(x, y1)[3] > threshold {
                all_transparent = false;
                break;
            }
        }
        if all_transparent {
            y1 += 1;
        } else {
            break;
        }
    }

    while y2 > y1 {
        let mut all_transparent = true;
        for x in x1..=x2 {
            if rgba.get_pixel(x, y2)[3] > threshold {
                all_transparent = false;
                break;
            }
        }
        if all_transparent {
            y2 -= 1;
        } else {
            break;
        }
    }

    let trimmed_width = x2 - x1 + 1;
    let trimmed_height = y2 - y1 + 1;
    (
        Some(Rect::new(0, 0, trimmed_width, trimmed_height)),
        Rect::new(x1, y1, trimmed_width, trimmed_height),
    )
}
