use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::Path;

use array_macro::array;
use image::imageops::{self, FilterType};
use image::{Rgb, RgbImage};

use crate::canvas::{Canvas, CopyArea, ToIndex};
use crate::rectarea::RectArea;
use crate::resource::ResourceItem;
use crate::settings::{
    FONT_HEIGHT, FONT_WIDTH, MAX_FONT_CODE, MIN_FONT_CODE, NUM_COLORS, NUM_FONT_ROWS,
    RESOURCE_ARCHIVE_DIRNAME, TILE_SIZE,
};
use crate::tilemap::SharedTilemap;
use crate::types::Color;
use crate::utils::{add_file_extension, as_i32, as_u32, parse_hex_string, simplify_string};

impl ToIndex for Color {
    fn to_index(&self) -> usize {
        *self as usize
    }
}

pub struct Image {
    pub(crate) canvas: Canvas<Color>,
    pub(crate) palette: [Color; NUM_COLORS as usize],
}

pub type SharedImage = shared_type!(Image);

impl Image {
    pub fn new(width: u32, height: u32) -> SharedImage {
        new_shared_type!(Self {
            canvas: Canvas::new(width, height),
            palette: array![i => i as Color; NUM_COLORS as usize],
        })
    }

    pub fn from_image(filename: &str) -> SharedImage {
        let colors = crate::colors().lock();
        let image_file = image::open(&Path::new(&filename))
            .unwrap_or_else(|_| panic!("Unable to open file '{}'", filename))
            .to_rgb8();
        let (width, height) = image_file.dimensions();
        let image = Self::new(width, height);
        {
            let mut image = image.lock();
            let mut color_table = HashMap::<(u8, u8, u8), Color>::new();
            for y in 0..height {
                for x in 0..width {
                    let p = image_file.get_pixel(x, y);
                    let src_rgb = (p[0], p[1], p[2]);
                    if let Some(color) = color_table.get(&src_rgb) {
                        image.canvas.data[y as usize][x as usize] = *color;
                    } else {
                        let mut closest_color: Color = 0;
                        let mut closest_dist: f64 = f64::MAX;
                        for i in 0..NUM_COLORS {
                            let pal_color = colors[i as usize];
                            let pal_rgb = (
                                ((pal_color >> 16) & 0xff) as u8,
                                ((pal_color >> 8) & 0xff) as u8,
                                (pal_color & 0xff) as u8,
                            );
                            let dist = Self::color_dist(src_rgb, pal_rgb);
                            if dist < closest_dist {
                                closest_color = i as Color;
                                closest_dist = dist;
                            }
                        }
                        color_table.insert(src_rgb, closest_color);
                        image.canvas.data[y as usize][x as usize] = closest_color;
                    }
                }
            }
        }
        image
    }

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

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

    pub fn set(&mut self, x: i32, y: i32, data_str: &[&str]) {
        let width = simplify_string(data_str[0]).len() as u32;
        let height = data_str.len() as u32;
        let image = Self::new(width, height);
        {
            let mut image = image.lock();
            for y in 0..height {
                let src_data = simplify_string(data_str[y as usize]);
                for x in 0..width {
                    let color = parse_hex_string(&src_data[x as usize..x as usize + 1]).unwrap();
                    image.canvas.data[y as usize][x as usize] = color as Color;
                }
            }
        }
        self.blt(
            x as f64,
            y as f64,
            image,
            0.0,
            0.0,
            width as f64,
            height as f64,
            None,
        );
    }

    pub fn load(&mut self, x: i32, y: i32, filename: &str) {
        let image = Self::from_image(filename);
        let width = image.lock().width();
        let height = image.lock().height();
        self.blt(
            x as f64,
            y as f64,
            image,
            0.0,
            0.0,
            width as f64,
            height as f64,
            None,
        );
    }

    pub fn save(&self, filename: &str, scale: u32) {
        let colors = crate::colors().lock();
        let width = self.width();
        let height = self.height();
        let mut image = RgbImage::new(width, height);
        for y in 0..height {
            for x in 0..width {
                let rgb = colors[self.canvas.data[y as usize][x as usize] as usize];
                let r = ((rgb >> 16) & 0xff) as u8;
                let g = ((rgb >> 8) & 0xff) as u8;
                let b = (rgb & 0xff) as u8;
                image.put_pixel(x, y, Rgb([r, g, b]));
            }
        }
        let image = imageops::resize(&image, width * scale, height * scale, FilterType::Nearest);
        let filename = add_file_extension(filename, ".png");
        image
            .save(&filename)
            .unwrap_or_else(|_| panic!("Unable to open file '{}'", filename));
    }

    pub fn clip(&mut self, x: f64, y: f64, width: f64, height: f64) {
        self.canvas.clip(x, y, width, height);
    }

    pub fn clip0(&mut self) {
        self.canvas.clip0();
    }

    pub fn camera(&mut self, x: f64, y: f64) {
        self.canvas.camera(x, y);
    }

    pub fn camera0(&mut self) {
        self.canvas.camera0();
    }

    pub fn pal(&mut self, src_color: Color, dst_color: Color) {
        self.palette[src_color as usize] = dst_color;
    }

    pub fn pal0(&mut self) {
        for i in 0..NUM_COLORS {
            self.palette[i as usize] = i as Color;
        }
    }

    pub fn cls(&mut self, color: Color) {
        self.canvas.cls(self.palette[color as usize]);
    }

    pub fn pget(&mut self, x: f64, y: f64) -> Color {
        self.canvas.pget(x, y)
    }

    pub fn pset(&mut self, x: f64, y: f64, color: Color) {
        self.canvas.pset(x, y, self.palette[color as usize]);
    }

    pub fn line(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, color: Color) {
        self.canvas
            .line(x1, y1, x2, y2, self.palette[color as usize]);
    }

    pub fn rect(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
        self.canvas
            .rect(x, y, width, height, self.palette[color as usize]);
    }

    pub fn rectb(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
        self.canvas
            .rectb(x, y, width, height, self.palette[color as usize]);
    }

    pub fn circ(&mut self, x: f64, y: f64, radius: f64, color: Color) {
        self.canvas.circ(x, y, radius, self.palette[color as usize]);
    }

    pub fn circb(&mut self, x: f64, y: f64, radius: f64, color: Color) {
        self.canvas
            .circb(x, y, radius, self.palette[color as usize]);
    }

    pub fn elli(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
        self.canvas
            .elli(x, y, width, height, self.palette[color as usize]);
    }

    pub fn ellib(&mut self, x: f64, y: f64, width: f64, height: f64, color: Color) {
        self.canvas
            .ellib(x, y, width, height, self.palette[color as usize]);
    }

    pub fn tri(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, color: Color) {
        self.canvas
            .tri(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
    }

    pub fn trib(&mut self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64, color: Color) {
        self.canvas
            .trib(x1, y1, x2, y2, x3, y3, self.palette[color as usize]);
    }

    pub fn fill(&mut self, x: f64, y: f64, color: Color) {
        self.canvas.fill(x, y, self.palette[color as usize]);
    }

    pub fn blt(
        &mut self,
        x: f64,
        y: f64,
        image: shared_type!(Self),
        image_x: f64,
        image_y: f64,
        width: f64,
        height: f64,
        transparent: Option<Color>,
    ) {
        if let Some(image) = image.try_lock() {
            self.canvas.blt(
                x,
                y,
                &image.canvas,
                image_x,
                image_y,
                width,
                height,
                transparent,
                Some(&self.palette),
            );
        } else {
            let copy_width = as_u32(width.abs());
            let copy_height = as_u32(height.abs());
            let mut canvas = Canvas::new(copy_width, copy_height);
            canvas.blt(
                0.0,
                0.0,
                &self.canvas,
                image_x,
                image_y,
                copy_width as f64,
                copy_height as f64,
                None,
                None,
            );
            self.canvas.blt(
                x,
                y,
                &canvas,
                0.0,
                0.0,
                width,
                height,
                transparent,
                Some(&self.palette),
            );
        }
    }

    pub fn bltm(
        &mut self,
        x: f64,
        y: f64,
        tilemap: SharedTilemap,
        tilemap_x: f64,
        tilemap_y: f64,
        width: f64,
        height: f64,
        transparent: Option<Color>,
    ) {
        let x = as_i32(x) - self.canvas.camera_x;
        let y = as_i32(y) - self.canvas.camera_y;
        let tilemap_x = as_i32(tilemap_x);
        let tilemap_y = as_i32(tilemap_y);
        let width = as_i32(width);
        let height = as_i32(height);

        let tilemap = tilemap.lock();
        let tilemap_rect = RectArea::new(
            tilemap.canvas.self_rect.left() * TILE_SIZE as i32,
            tilemap.canvas.self_rect.top() * TILE_SIZE as i32,
            tilemap.canvas.self_rect.width() * TILE_SIZE,
            tilemap.canvas.self_rect.height() * TILE_SIZE,
        );

        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.canvas.clip_rect,
            tilemap_x,
            tilemap_y,
            tilemap_rect,
            width,
            height,
        );
        if width == 0 || height == 0 {
            return;
        }

        let image = tilemap.image.lock();
        for yi in 0..height {
            for xi in 0..width {
                let tilemap_x = src_x + sign_x * xi + offset_x;
                let tilemap_y = src_y + sign_y * yi + offset_y;

                let tile_x = tilemap_x / TILE_SIZE as i32;
                let tile_y = tilemap_y / TILE_SIZE as i32;
                let tile = tilemap.canvas.data[tile_y as usize][tile_x as usize];

                let value_x = tile.0 as i32 * TILE_SIZE as i32 + tilemap_x % TILE_SIZE as i32;
                let value_y = tile.1 as i32 * TILE_SIZE as i32 + tilemap_y % TILE_SIZE as i32;
                let value = image.canvas.data[value_y as usize][value_x as usize];

                if let Some(transparent) = transparent {
                    if value == transparent {
                        continue;
                    }
                }
                let value = self.palette[value.to_index()];
                self.canvas.data[(dst_y + yi) as usize][(dst_x + xi) as usize] = value;
            }
        }
    }

    pub fn text(&mut self, x: f64, y: f64, string: &str, color: Color) {
        let mut x = as_i32(x); // No need to reflect camera_x
        let mut y = as_i32(y); // No need to reflect camera_y
        let color = self.palette[color as usize];
        let palette1 = self.palette[1];
        self.pal(1, color);
        let start_x = x;
        for c in string.chars() {
            if c == '\n' {
                x = start_x;
                y += FONT_HEIGHT as i32;
                continue;
            }
            if c < MIN_FONT_CODE || c > MAX_FONT_CODE {
                continue;
            }
            let code = c as i32 - MIN_FONT_CODE as i32;
            let src_x = (code % NUM_FONT_ROWS as i32) * FONT_WIDTH as i32;
            let src_y = (code / NUM_FONT_ROWS as i32) * FONT_HEIGHT as i32;
            self.blt(
                x as f64,
                y as f64,
                crate::font(),
                src_x as f64,
                src_y as f64,
                FONT_WIDTH as f64,
                FONT_HEIGHT as f64,
                Some(0),
            );
            x += FONT_WIDTH as i32;
        }
        self.pal(1, palette1);
    }

    fn color_dist(rgb1: (u8, u8, u8), rgb2: (u8, u8, u8)) -> f64 {
        let (r1, g1, b1) = rgb1;
        let (r2, g2, b2) = rgb2;
        let dx = (r1 as f64 - r2 as f64) * 0.30;
        let dy = (g1 as f64 - g2 as f64) * 0.59;
        let dz = (b1 as f64 - b2 as f64) * 0.11;
        dx * dx + dy * dy + dz * dz
    }
}

impl ResourceItem for Image {
    fn resource_name(item_no: u32) -> String {
        RESOURCE_ARCHIVE_DIRNAME.to_string() + "image" + &item_no.to_string()
    }

    fn is_modified(&self) -> bool {
        for y in 0..self.height() {
            for x in 0..self.width() {
                if self.canvas.data[y as usize][x as usize] != 0 {
                    return true;
                }
            }
        }
        false
    }

    fn clear(&mut self) {
        self.cls(0);
    }

    fn serialize(&self) -> String {
        let mut output = String::new();
        for y in 0..self.height() {
            for x in 0..self.width() {
                let _ = write!(output, "{:1x}", self.canvas.data[y as usize][x as usize]);
            }
            output += "\n";
        }
        output
    }

    fn deserialize(&mut self, _version: u32, input: &str) {
        for (i, line) in input.lines().enumerate() {
            string_loop!(j, color, line, 1, {
                self.canvas.data[i][j] = parse_hex_string(&color).unwrap() as Color;
            });
        }
    }
}
