use std::cmp::max;
use std::f32::consts::PI;
use std::mem::swap;

use crate::rect_area::RectArea;
use crate::utils::{f32_to_i32, f32_to_u32};

pub trait ToIndex {
    fn to_index(&self) -> usize;
}

#[derive(Clone)]
pub struct Canvas<T: Copy + PartialEq + Default + ToIndex> {
    pub self_rect: RectArea,
    pub clip_rect: RectArea,
    pub camera_x: i32,
    pub camera_y: i32,
    pub alpha: f32,
    pub data: Vec<T>,

    should_write: fn(&Canvas<T>, i32, i32) -> bool,
}

impl<T: Copy + PartialEq + Default + ToIndex> Canvas<T> {
    pub fn new(width: u32, height: u32) -> Self {
        Self {
            self_rect: RectArea::new(0, 0, width, height),
            clip_rect: RectArea::new(0, 0, width, height),
            camera_x: 0,
            camera_y: 0,
            alpha: 1.0,
            data: vec![T::default(); (width * height) as usize],
            should_write: Self::should_write_always,
        }
    }

    pub const fn width(&self) -> u32 {
        self.self_rect.width()
    }

    pub const fn height(&self) -> u32 {
        self.self_rect.height()
    }

    pub fn data_ptr(&mut self) -> *mut T {
        self.data.as_mut_ptr()
    }

    pub fn clip(&mut self, x: f32, y: f32, width: f32, height: f32) {
        let x = f32_to_i32(x);
        let y = f32_to_i32(y);
        let width = f32_to_u32(width);
        let height = f32_to_u32(height);
        self.clip_rect = self
            .self_rect
            .intersects(RectArea::new(x, y, width, height));
    }

    pub fn clip0(&mut self) {
        self.clip_rect = self.self_rect;
    }

    pub fn camera(&mut self, x: f32, y: f32) {
        self.camera_x = f32_to_i32(x);
        self.camera_y = f32_to_i32(y);
    }

    pub fn camera0(&mut self) {
        self.camera_x = 0;
        self.camera_y = 0;
    }

    pub fn dither(&mut self, alpha: f32) {
        self.alpha = alpha;
        if alpha <= 0.0 {
            self.should_write = Self::should_write_never;
        } else if alpha >= 1.0 {
            self.should_write = Self::should_write_always;
        } else {
            self.should_write = Self::should_write_normal;
        }
    }

    pub fn cls(&mut self, value: T) {
        let width = self.width();
        let height = self.height();
        let alpha = self.alpha;
        self.dither(1.0);
        for y in 0..height {
            for x in 0..width {
                self.write_data(x as usize, y as usize, value);
            }
        }
        self.dither(alpha);
    }

    pub fn pget(&mut self, x: f32, y: f32) -> T {
        let x = f32_to_i32(x);
        let y = f32_to_i32(y);
        if self.clip_rect.contains(x, y) {
            self.read_data(x as usize, y as usize)
        } else {
            T::default()
        }
    }

    pub fn pset(&mut self, x: f32, y: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        self.write_data_with_clipping(x, y, value);
    }

    pub fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, value: T) {
        let x1 = f32_to_i32(x1) - self.camera_x;
        let y1 = f32_to_i32(y1) - self.camera_y;
        let x2 = f32_to_i32(x2) - self.camera_x;
        let y2 = f32_to_i32(y2) - self.camera_y;

        if x1 == x2 && y1 == y2 {
            self.write_data_with_clipping(x1, y1, value);
        } else if (x1 - x2).abs() > (y1 - y2).abs() {
            let (start_x, start_y, end_x, end_y) = if x1 < x2 {
                (x1, y1, x2, y2)
            } else {
                (x2, y2, x1, y1)
            };
            let length = end_x - start_x + 1;
            let alpha = (end_y - start_y) as f32 / (end_x - start_x) as f32;
            for xi in 0..length {
                self.write_data_with_clipping(
                    start_x + xi,
                    start_y + f32_to_i32(alpha * xi as f32),
                    value,
                );
            }
        } else {
            let (start_x, start_y, end_x, end_y) = if y1 < y2 {
                (x1, y1, x2, y2)
            } else {
                (x2, y2, x1, y1)
            };
            let length = end_y - start_y + 1;
            let alpha = (end_x - start_x) as f32 / (end_y - start_y) as f32;
            for yi in 0..length {
                self.write_data_with_clipping(
                    start_x + f32_to_i32(alpha * yi as f32),
                    start_y + yi,
                    value,
                );
            }
        }
    }

    pub fn rect(&mut self, x: f32, y: f32, width: f32, height: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let width = f32_to_u32(width);
        let height = f32_to_u32(height);
        let rect = RectArea::new(x, y, width, height).intersects(self.clip_rect);
        if rect.is_empty() {
            return;
        }

        let left = rect.left();
        let top = rect.top();
        let right = rect.right();
        let bottom = rect.bottom();
        for y in top..=bottom {
            for x in left..=right {
                self.write_data(x as usize, y as usize, value);
            }
        }
    }

    pub fn rectb(&mut self, x: f32, y: f32, width: f32, height: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let width = f32_to_u32(width);
        let height = f32_to_u32(height);
        let rect = RectArea::new(x, y, width, height);
        if rect.intersects(self.clip_rect).is_empty() {
            return;
        }

        let left = rect.left();
        let top = rect.top();
        let right = rect.right();
        let bottom = rect.bottom();
        for x in left..=right {
            self.write_data_with_clipping(x, top, value);
            self.write_data_with_clipping(x, bottom, value);
        }
        for y in top..=bottom {
            self.write_data_with_clipping(left, y, value);
            self.write_data_with_clipping(right, y, value);
        }
    }

    pub fn circ(&mut self, x: f32, y: f32, radius: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let radius = f32_to_u32(radius);

        for xi in 0..=radius as i32 {
            let (x1, y1, x2, y2) = Self::ellipse_area(0.0, 0.0, radius as f32, radius as f32, xi);
            for yi in y1..=y2 {
                self.write_data_with_clipping(x + x1, y + yi, value);
                self.write_data_with_clipping(x + x2, y + yi, value);
                self.write_data_with_clipping(x + yi, y + x1, value);
                self.write_data_with_clipping(x + yi, y + x2, value);
            }
        }
    }

    pub fn circb(&mut self, x: f32, y: f32, radius: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let radius = f32_to_u32(radius);

        for xi in 0..=radius as i32 {
            let (x1, y1, x2, y2) = Self::ellipse_area(0.0, 0.0, radius as f32, radius as f32, xi);
            self.write_data_with_clipping(x + x1, y + y1, value);
            self.write_data_with_clipping(x + x2, y + y1, value);
            self.write_data_with_clipping(x + x1, y + y2, value);
            self.write_data_with_clipping(x + x2, y + y2, value);

            self.write_data_with_clipping(x + y1, y + x1, value);
            self.write_data_with_clipping(x + y1, y + x2, value);
            self.write_data_with_clipping(x + y2, y + x1, value);
            self.write_data_with_clipping(x + y2, y + x2, value);
        }
    }

    pub fn elli(&mut self, x: f32, y: f32, width: f32, height: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let width = f32_to_u32(width);
        let height = f32_to_u32(height);
        let (ra, rb, cx, cy) = Self::ellipse_params(x, y, width, height);

        for xi in x..=(x + width as i32 / 2) {
            let (x1, y1, x2, y2) = Self::ellipse_area(cx, cy, ra, rb, xi);
            for yi in y1..=y2 {
                self.write_data_with_clipping(x1, yi, value);
                self.write_data_with_clipping(x2, yi, value);
            }
        }

        for yi in y..=(y + height as i32 / 2) {
            let (y1, x1, y2, x2) = Self::ellipse_area(cy, cx, rb, ra, yi);
            for xi in x1..=x2 {
                self.write_data_with_clipping(xi, y1, value);
                self.write_data_with_clipping(xi, y2, value);
            }
        }
    }

    pub fn ellib(&mut self, x: f32, y: f32, width: f32, height: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let width = f32_to_u32(width);
        let height = f32_to_u32(height);
        let (ra, rb, cx, cy) = Self::ellipse_params(x, y, width, height);

        for xi in x..=(x + width as i32 / 2) {
            let (x1, y1, x2, y2) = Self::ellipse_area(cx, cy, ra, rb, xi);
            self.write_data_with_clipping(x1, y1, value);
            self.write_data_with_clipping(x2, y1, value);
            self.write_data_with_clipping(x1, y2, value);
            self.write_data_with_clipping(x2, y2, value);
        }

        for yi in y..=(y + height as i32 / 2) {
            let (y1, x1, y2, x2) = Self::ellipse_area(cy, cx, rb, ra, yi);
            self.write_data_with_clipping(x1, y1, value);
            self.write_data_with_clipping(x2, y1, value);
            self.write_data_with_clipping(x1, y2, value);
            self.write_data_with_clipping(x2, y2, value);
        }
    }

    pub fn tri(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, value: T) {
        let mut x1 = f32_to_i32(x1) - self.camera_x;
        let mut y1 = f32_to_i32(y1) - self.camera_y;
        let mut x2 = f32_to_i32(x2) - self.camera_x;
        let mut y2 = f32_to_i32(y2) - self.camera_y;
        let mut x3 = f32_to_i32(x3) - self.camera_x;
        let mut y3 = f32_to_i32(y3) - self.camera_y;

        if y1 > y2 {
            swap(&mut y1, &mut y2);
            swap(&mut x1, &mut x2);
        }
        if y1 > y3 {
            swap(&mut y1, &mut y3);
            swap(&mut x1, &mut x3);
        }
        if y2 > y3 {
            swap(&mut y2, &mut y3);
            swap(&mut x2, &mut x3);
        }

        let alpha12 = if y2 == y1 {
            0.0
        } else {
            (x2 - x1) as f32 / (y2 - y1) as f32
        };
        let alpha13 = if y3 == y1 {
            0.0
        } else {
            (x3 - x1) as f32 / (y3 - y1) as f32
        };
        let alpha23 = if y3 == y2 {
            0.0
        } else {
            (x3 - x2) as f32 / (y3 - y2) as f32
        };
        let x_inter = f32_to_i32(x1 as f32 + alpha13 * (y2 - y1) as f32);

        for y in y1..=y2 {
            let (x_slider, x_end) = if x_inter < x2 {
                (
                    f32_to_i32(x_inter as f32 + alpha13 * (y - y2) as f32),
                    f32_to_i32(x2 as f32 + alpha12 * (y - y2) as f32),
                )
            } else {
                (
                    f32_to_i32(x2 as f32 + alpha12 * (y - y2) as f32),
                    f32_to_i32(x_inter as f32 + alpha13 * (y - y2) as f32),
                )
            };
            for x in x_slider..=x_end {
                self.write_data_with_clipping(x, y, value);
            }
        }

        for y in (y2 + 1)..=y3 {
            let (x_slider, x_end) = if x_inter < x2 {
                (
                    f32_to_i32(x_inter as f32 + alpha13 * (y - y2) as f32),
                    f32_to_i32(x2 as f32 + alpha23 * (y - y2) as f32),
                )
            } else {
                (
                    f32_to_i32(x2 as f32 + alpha23 * (y - y2) as f32),
                    f32_to_i32(x_inter as f32 + alpha13 * (y - y2) as f32),
                )
            };
            for x in x_slider..=x_end {
                self.write_data_with_clipping(x, y, value);
            }
        }
    }

    pub fn trib(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, value: T) {
        self.line(x1, y1, x2, y2, value);
        self.line(x1, y1, x3, y3, value);
        self.line(x2, y2, x3, y3, value);
    }

    pub fn fill(&mut self, x: f32, y: f32, value: T) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        if !self.clip_rect.contains(x, y) {
            return;
        }

        let dst_value = self.read_data(x as usize, y as usize);
        if value == dst_value {
            return;
        }

        let mut visit_stack = Vec::new();
        visit_stack.push((x, y));
        while let Some((x, y)) = visit_stack.pop() {
            if !self.clip_rect.contains(x, y) || self.read_data(x as usize, y as usize) != dst_value
            {
                continue;
            }

            let mut left = x;
            let mut right = x;
            while left > self.clip_rect.left()
                && self.read_data((left - 1) as usize, y as usize) == dst_value
            {
                left -= 1;
            }
            while right < self.clip_rect.right()
                && self.read_data((right + 1) as usize, y as usize) == dst_value
            {
                right += 1;
            }

            for xi in left..=right {
                self.write_data(xi as usize, y as usize, value);
            }

            for scan_y in [y - 1, y + 1] {
                if scan_y >= self.clip_rect.top() && scan_y <= self.clip_rect.bottom() {
                    let mut scan_x = left;
                    let mut in_segment = false;
                    while scan_x <= right {
                        let is_target =
                            self.read_data(scan_x as usize, scan_y as usize) == dst_value;
                        if is_target && !in_segment {
                            visit_stack.push((scan_x, scan_y));
                            in_segment = true;
                        } else if !is_target {
                            in_segment = false;
                        }
                        scan_x += 1;
                    }
                }
            }
        }
    }

    pub fn blt(
        &mut self,
        x: f32,
        y: f32,
        canvas: &Self,
        canvas_x: f32,
        canvas_y: f32,
        width: f32,
        height: f32,
        transparent: Option<T>,
        palette: Option<&[T]>,
    ) {
        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let canvas_x = f32_to_i32(canvas_x);
        let canvas_y = f32_to_i32(canvas_y);
        let width = f32_to_i32(width);
        let height = f32_to_i32(height);

        let CopyArea {
            dst_x,
            dst_y,
            src_x,
            src_y,
            sign_x,
            sign_y,
            offset_x,
            offset_y,
            width,
            height,
        } = CopyArea::new(
            x,
            y,
            self.clip_rect,
            canvas_x,
            canvas_y,
            canvas.self_rect,
            width,
            height,
        );
        if width == 0 || height == 0 {
            return;
        }

        for yi in 0..height {
            for xi in 0..width {
                let value_x = src_x + sign_x * xi + offset_x;
                let value_y = src_y + sign_y * yi + offset_y;
                let value = canvas.read_data(value_x as usize, value_y as usize);
                if let Some(transparent) = transparent {
                    if value == transparent {
                        continue;
                    }
                }
                let value = palette.map_or(value, |palette| palette[value.to_index()]);
                self.write_data((dst_x + xi) as usize, (dst_y + yi) as usize, value);
            }
        }
    }

    pub fn blt_transform(
        &mut self,
        x: f32,
        y: f32,
        canvas: &Self,
        canvas_x: f32,
        canvas_y: f32,
        width: f32,
        height: f32,
        transparent: Option<T>,
        palette: Option<&[T]>,
        rotate: f32,
        scale: f32,
        use_canvas_clip: bool,
    ) {
        if scale < f32::EPSILON {
            return;
        }

        let x = f32_to_i32(x) - self.camera_x;
        let y = f32_to_i32(y) - self.camera_y;
        let canvas_x = f32_to_i32(canvas_x);
        let canvas_y = f32_to_i32(canvas_y);
        let sign_x = if width < 0.0 { -1.0 } else { 1.0 };
        let sign_y = if height < 0.0 { -1.0 } else { 1.0 };
        let width = f32_to_i32(width).abs();
        let height = f32_to_i32(height).abs();

        let canvas_area = RectArea::new(canvas_x, canvas_y, width as u32, height as u32)
            .intersects(if use_canvas_clip {
                canvas.clip_rect
            } else {
                canvas.self_rect
            });
        if canvas_area.is_empty() {
            return;
        }

        let half_width = (width - 1) as f32 / 2.0;
        let half_height = (height - 1) as f32 / 2.0;
        let src_cx = canvas_x as f32 + half_width;
        let src_cy = canvas_y as f32 + half_height;
        let dst_cx = x as f32 + half_width;
        let dst_cy = y as f32 + half_height;

        let rotate = rotate * PI / 180.0;
        let sin = -f32::sin(rotate); // Clockwise
        let cos = f32::cos(rotate);
        let offset_x = (half_width * cos.abs() + half_height * sin.abs() + 1.0) * scale;
        let offset_y = (half_width * sin.abs() + half_height * cos.abs() + 1.0) * scale;
        let x1 = f32_to_i32(dst_cx - offset_x).max(self.clip_rect.left());
        let x2 = f32_to_i32(dst_cx + offset_x).min(self.clip_rect.right());
        let y1 = f32_to_i32(dst_cy - offset_y).max(self.clip_rect.top());
        let y2 = f32_to_i32(dst_cy + offset_y).min(self.clip_rect.bottom());

        for yi in y1..=y2 {
            for xi in x1..=x2 {
                let offset_x = (xi as f32 - dst_cx) * sign_x;
                let offset_y = (yi as f32 - dst_cy) * sign_y;
                let value_x = f32_to_i32(src_cx + (offset_x * cos - offset_y * sin) / scale);
                let value_y = f32_to_i32(src_cy + (offset_x * sin + offset_y * cos) / scale);
                if !canvas_area.contains(value_x, value_y) {
                    continue;
                }
                let value = canvas.read_data(value_x as usize, value_y as usize);
                if let Some(transparent) = transparent {
                    if value == transparent {
                        continue;
                    }
                }
                let value = palette.map_or(value, |palette| palette[value.to_index()]);
                self.write_data(xi as usize, yi as usize, value);
            }
        }
    }

    pub fn read_data(&self, x: usize, y: usize) -> T {
        let width = self.width() as usize;
        self.data[width * y + x]
    }

    pub fn write_data(&mut self, x: usize, y: usize, value: T) {
        if (self.should_write)(self, x as i32, y as i32) {
            let width = self.width() as usize;
            self.data[width * y + x] = value;
        }
    }

    fn write_data_with_clipping(&mut self, x: i32, y: i32, value: T) {
        if (self.should_write)(self, x, y) && self.clip_rect.contains(x, y) {
            let width = self.width() as usize;
            self.data[width * y as usize + x as usize] = value;
        }
    }

    fn ellipse_params(x: i32, y: i32, width: u32, height: u32) -> (f32, f32, f32, f32) {
        let ra = (width - 1) as f32 / 2.0;
        let rb = (height - 1) as f32 / 2.0;
        let cx = x as f32 + ra;
        let cy = y as f32 + rb;
        (ra, rb, cx, cy)
    }

    fn ellipse_area(cx: f32, cy: f32, ra: f32, rb: f32, x: i32) -> (i32, i32, i32, i32) {
        let dx = x as f32 - cx;
        let dy = if ra > 0.0 {
            rb * (1.0 - dx * dx / (ra * ra)).sqrt()
        } else {
            rb
        };

        let x1 = f32_to_i32(cx - dx - 0.01);
        let y1 = f32_to_i32(cy - dy - 0.01);
        let x2 = f32_to_i32(cx + dx + 0.01);
        let y2 = f32_to_i32(cy + dy + 0.01);

        (x1, y1, x2, y2)
    }

    fn should_write_always(&self, _x: i32, _y: i32) -> bool {
        true
    }

    fn should_write_never(&self, _x: i32, _y: i32) -> bool {
        false
    }

    fn should_write_normal(&self, x: i32, y: i32) -> bool {
        const DITHERING_MATRIX: [[f32; 4]; 4] = [
            [1.0 / 16.0, 9.0 / 16.0, 3.0 / 16.0, 11.0 / 16.0],
            [13.0 / 16.0, 5.0 / 16.0, 15.0 / 16.0, 7.0 / 16.0],
            [3.0 / 16.0, 11.0 / 16.0, 1.0 / 16.0, 9.0 / 16.0],
            [15.0 / 16.0, 7.0 / 16.0, 13.0 / 16.0, 5.0 / 16.0],
        ];

        self.alpha > DITHERING_MATRIX[y.rem_euclid(4) as usize][x.rem_euclid(4) as usize]
    }
}

pub struct CopyArea {
    pub dst_x: i32,
    pub dst_y: i32,
    pub src_x: i32,
    pub src_y: i32,
    pub sign_x: i32,
    pub sign_y: i32,
    pub offset_x: i32,
    pub offset_y: i32,
    pub width: i32,
    pub height: i32,
}

impl CopyArea {
    pub fn new(
        dst_x: i32,
        dst_y: i32,
        dst_rect: RectArea,
        src_x: i32,
        src_y: i32,
        src_rect: RectArea,
        width: i32,
        height: i32,
    ) -> Self {
        let flip_x = width < 0;
        let flip_y = height < 0;
        let width = width.abs();
        let height = height.abs();

        let left_cut = max(max(src_rect.left() - src_x, dst_rect.left() - dst_x), 0);
        let top_cut = max(max(src_rect.top() - src_y, dst_rect.top() - dst_y), 0);
        let right_cut = max(
            max(
                src_x + width - 1 - src_rect.right(),
                dst_x + width - 1 - dst_rect.right(),
            ),
            0,
        );
        let bottom_cut = max(
            max(
                src_y + height - 1 - src_rect.bottom(),
                dst_y + height - 1 - dst_rect.bottom(),
            ),
            0,
        );

        let width = max(width - left_cut - right_cut, 0);
        let height = max(height - top_cut - bottom_cut, 0);
        let (sign_x, offset_x) = if flip_x { (-1, width - 1) } else { (1, 0) };
        let (sign_y, offset_y) = if flip_y { (-1, height - 1) } else { (1, 0) };

        Self {
            dst_x: dst_x + left_cut,
            dst_y: dst_y + top_cut,
            src_x: src_x + if flip_x { right_cut } else { left_cut },
            src_y: src_y + if flip_y { bottom_cut } else { top_cut },
            sign_x,
            sign_y,
            offset_x,
            offset_y,
            width,
            height,
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Canvas;

    #[test]
    fn fill_doesnt_overflow_stack() {
        let mut canvas = Canvas::<u8>::new(256, 256);
        canvas.fill(0.0, 0.0, 8);
        // this assertion won't even be reached if the above line overflows the stack
        assert_eq!(canvas.read_data(128, 128), 8);
    }
}
