use jxl_image::ImageHeader;
use jxl_modular::ChannelShift;

#[derive(Debug, Copy, Clone, PartialEq, Eq, Default, Hash)]
pub struct Region {
    pub left: i32,
    pub top: i32,
    pub width: u32,
    pub height: u32,
}

impl Region {
    #[inline]
    pub fn empty() -> Self {
        Self::default()
    }

    #[inline]
    pub fn with_size(width: u32, height: u32) -> Self {
        Self {
            left: 0,
            top: 0,
            width,
            height,
        }
    }

    #[inline]
    pub fn is_empty(self) -> bool {
        self.width == 0 || self.height == 0
    }

    #[inline]
    pub fn right(self) -> i32 {
        self.left.saturating_add_unsigned(self.width)
    }

    #[inline]
    pub fn bottom(self) -> i32 {
        self.top.saturating_add_unsigned(self.height)
    }

    pub fn contains(self, target: Region) -> bool {
        if target.is_empty() {
            return true;
        }

        self.left <= target.left
            && self.top <= target.top
            && self.right() >= target.right()
            && self.bottom() >= target.bottom()
    }

    pub fn translate(self, x: i32, y: i32) -> Self {
        Self {
            left: self.left + x,
            top: self.top + y,
            ..self
        }
    }

    pub fn intersection(self, rhs: Region) -> Self {
        if self.width == 0 || rhs.width == 0 || self.height == 0 || rhs.height == 0 {
            return Self {
                left: 0,
                top: 0,
                width: 0,
                height: 0,
            };
        }

        let mut ax = (self.left, self.right());
        let mut ay = (self.top, self.bottom());
        let mut bx = (rhs.left, rhs.right());
        let mut by = (rhs.top, rhs.bottom());
        if ax.0 > bx.0 {
            std::mem::swap(&mut ax, &mut bx);
        }
        if ay.0 > by.0 {
            std::mem::swap(&mut ay, &mut by);
        }

        if ax.1 <= bx.0 || ay.1 <= by.0 {
            Self {
                left: 0,
                top: 0,
                width: 0,
                height: 0,
            }
        } else {
            Self {
                left: bx.0,
                top: by.0,
                width: std::cmp::min(ax.1, bx.1).abs_diff(bx.0),
                height: std::cmp::min(ay.1, by.1).abs_diff(by.0),
            }
        }
    }

    #[inline]
    pub fn merge(self, other: Self) -> Self {
        if other.is_empty() {
            return self;
        }
        if self.is_empty() {
            return other;
        }

        let left = self.left.min(other.left);
        let top = self.top.min(other.top);
        let right = self
            .left
            .wrapping_add_unsigned(self.width)
            .max(other.left.wrapping_add_unsigned(other.width));
        let bottom = self
            .top
            .wrapping_add_unsigned(self.height)
            .max(other.top.wrapping_add_unsigned(other.height));
        let width = right.abs_diff(left);
        let height = bottom.abs_diff(top);

        Self {
            left,
            top,
            width,
            height,
        }
    }

    #[inline]
    pub fn pad(self, size: u32) -> Self {
        Self {
            left: self.left.saturating_sub_unsigned(size),
            top: self.top.saturating_sub_unsigned(size),
            width: self.width + size * 2,
            height: self.height + size * 2,
        }
    }

    #[inline]
    pub fn downsample(self, factor: u32) -> Self {
        if factor == 0 {
            return self;
        }

        let add = (1u32 << factor) - 1;
        let new_left = self.left >> factor;
        let new_top = self.top >> factor;
        let adj_width = self.width + self.left.abs_diff(new_left << factor);
        let adj_height = self.height + self.top.abs_diff(new_top << factor);
        Self {
            left: new_left,
            top: new_top,
            width: (adj_width + add) >> factor,
            height: (adj_height + add) >> factor,
        }
    }

    #[inline]
    pub fn downsample_separate(self, factor_x: u32, factor_y: u32) -> Self {
        if factor_x == 0 && factor_y == 0 {
            return self;
        }

        let add_x = (1u32 << factor_x) - 1;
        let new_left = self.left >> factor_x;
        let adj_width = self.width + self.left.abs_diff(new_left << factor_x);
        let add_y = (1u32 << factor_y) - 1;
        let new_top = self.top >> factor_y;
        let adj_height = self.height + self.top.abs_diff(new_top << factor_y);
        Self {
            left: new_left,
            top: new_top,
            width: (adj_width + add_x) >> factor_x,
            height: (adj_height + add_y) >> factor_y,
        }
    }

    #[inline]
    pub fn downsample_with_shift(self, shift: ChannelShift) -> Self {
        let factor_x = shift.hshift();
        let factor_y = shift.vshift();

        let left = self.left >> factor_x;
        let top = self.top >> factor_y;
        let (width, height) = shift.shift_size((self.width, self.height));

        Self {
            left,
            top,
            width,
            height,
        }
    }

    #[inline]
    pub fn upsample(self, factor: u32) -> Self {
        self.upsample_separate(factor, factor)
    }

    #[inline]
    pub fn upsample_separate(self, factor_x: u32, factor_y: u32) -> Self {
        Self {
            left: self.left << factor_x,
            top: self.top << factor_y,
            width: self.width << factor_x,
            height: self.height << factor_y,
        }
    }

    pub(crate) fn container_aligned(self, grid_dim: u32) -> Self {
        debug_assert!(grid_dim.is_power_of_two());
        let add = grid_dim - 1;
        let mask = !add;
        let new_left = ((self.left as u32) & mask) as i32;
        let new_top = ((self.top as u32) & mask) as i32;
        let x_diff = self.left.abs_diff(new_left);
        let y_diff = self.top.abs_diff(new_top);
        Self {
            left: new_left,
            top: new_top,
            width: (self.width + x_diff + add) & mask,
            height: (self.height + y_diff + add) & mask,
        }
    }

    pub fn apply_orientation(self, image_header: &ImageHeader) -> Self {
        let image_width = image_header.width_with_orientation();
        let image_height = image_header.height_with_orientation();
        let (_, _, mut left, mut top) = image_header.metadata.apply_orientation(
            image_width,
            image_height,
            self.left,
            self.top,
            true,
        );
        let (_, _, mut right, mut bottom) = image_header.metadata.apply_orientation(
            image_width,
            image_height,
            self.left + self.width as i32 - 1,
            self.top + self.height as i32 - 1,
            true,
        );

        if left > right {
            std::mem::swap(&mut left, &mut right);
        }
        if top > bottom {
            std::mem::swap(&mut top, &mut bottom);
        }
        let width = right.abs_diff(left) + 1;
        let height = bottom.abs_diff(top) + 1;
        Self {
            left,
            top,
            width,
            height,
        }
    }
}
