use std::collections::HashMap;
use std::mem::size_of;

use glow::{HasContext, PixelUnpackData};
use pyxel_platform::GLProfile;

use crate::font::SharedFont;
use crate::image::Color;
use crate::pyxel::Pyxel;
use crate::settings::{BACKGROUND_COLOR, MAX_COLORS, NUM_SCREEN_TYPES};

#[cfg(target_os = "macos")]
const GL_VERSION: &str = include_str!("shaders/gles_version.glsl");
#[cfg(not(target_os = "macos"))]
const GL_VERSION: &str = include_str!("shaders/gl_version.glsl");

const GLES_VERSION: &str = include_str!("shaders/gles_version.glsl");
const COMMON_VERT: &str = include_str!("shaders/common.vert");
const COMMON_FRAG: &str = include_str!("shaders/common.frag");
const SCREEN_FRAGS: [&str; NUM_SCREEN_TYPES as usize] = [
    include_str!("shaders/crisp.frag"),
    include_str!("shaders/smooth.frag"),
    include_str!("shaders/retro.frag"),
];

pub struct ScreenShader {
    shader_program: glow::Program,
    uniform_locations: HashMap<String, glow::UniformLocation>,
    vertex_array: glow::VertexArray,
}

pub struct Graphics {
    screen_shaders: Vec<ScreenShader>,
    screen_texture: glow::NativeTexture,
    colors_texture: glow::NativeTexture,
}

impl Graphics {
    pub fn new() -> Self {
        unsafe {
            let gl = pyxel_platform::gl_context();

            if pyxel_platform::gl_profile() != GLProfile::Gles {
                gl.disable(glow::FRAMEBUFFER_SRGB);
            }
            gl.disable(glow::BLEND);

            let screen_shaders = Self::create_screen_shaders(gl);
            let screen_texture = Self::create_screen_texture(gl);
            let colors_texture = Self::create_colors_texture(gl);

            Self {
                screen_shaders,
                screen_texture,
                colors_texture,
            }
        }
    }

    unsafe fn create_screen_shaders(gl: &mut glow::Context) -> Vec<ScreenShader> {
        let glsl_version = if pyxel_platform::gl_profile() == GLProfile::Gles {
            GLES_VERSION
        } else {
            GL_VERSION
        };

        let mut screen_shaders = Vec::new();
        for &screen_frag in &SCREEN_FRAGS {
            // Vertex shader
            let vertex_shader = gl.create_shader(glow::VERTEX_SHADER).unwrap();
            gl.shader_source(vertex_shader, &format!("{glsl_version}{COMMON_VERT}"));
            gl.compile_shader(vertex_shader);
            assert!(
                gl.get_shader_compile_status(vertex_shader),
                "\n[vertex shader]\n{}",
                gl.get_shader_info_log(vertex_shader)
            );

            // Fragment shader
            let fragment_shader = gl.create_shader(glow::FRAGMENT_SHADER).unwrap();
            gl.shader_source(
                fragment_shader,
                &format!("{glsl_version}{COMMON_FRAG}{screen_frag}"),
            );
            gl.compile_shader(fragment_shader);
            assert!(
                gl.get_shader_compile_status(fragment_shader),
                "\n[fragment shader]\n{}",
                gl.get_shader_info_log(fragment_shader)
            );

            // Shader program
            let shader_program = gl.create_program().unwrap();
            gl.attach_shader(shader_program, vertex_shader);
            gl.attach_shader(shader_program, fragment_shader);
            gl.link_program(shader_program);
            assert!(
                gl.get_program_link_status(shader_program),
                "{}",
                gl.get_program_info_log(shader_program)
            );
            gl.detach_shader(shader_program, vertex_shader);
            gl.delete_shader(vertex_shader);
            gl.detach_shader(shader_program, fragment_shader);
            gl.delete_shader(fragment_shader);

            // Uniform locations
            let mut uniform_locations: HashMap<String, glow::UniformLocation> = HashMap::new();
            let uniform_names = [
                "u_screenPos",
                "u_screenSize",
                "u_screenScale",
                "u_numColors",
                "u_backgroundColor",
                "u_screenTexture",
                "u_colorsTexture",
            ];

            for &uniform_name in &uniform_names {
                if let Some(location) = gl.get_uniform_location(shader_program, uniform_name) {
                    uniform_locations.insert(uniform_name.to_string(), location);
                }
            }

            // Vertex array
            let vertices: [f32; 8] = [-1.0, 1.0, -1.0, -1.0, 1.0, 1.0, 1.0, -1.0];
            let vertex_array = gl.create_vertex_array().unwrap();
            let vertex_buffer = gl.create_buffer().unwrap();

            gl.bind_vertex_array(Some(vertex_array));
            gl.bind_buffer(glow::ARRAY_BUFFER, Some(vertex_buffer));
            gl.buffer_data_u8_slice(
                glow::ARRAY_BUFFER,
                vertices.align_to::<u8>().1,
                glow::STATIC_DRAW,
            );

            let position = gl.get_attrib_location(shader_program, "position").unwrap();
            gl.vertex_attrib_pointer_f32(
                position,
                2,
                glow::FLOAT,
                false,
                2 * size_of::<f32>() as i32,
                0,
            );
            gl.enable_vertex_attrib_array(position);

            // Add screen shader
            screen_shaders.push(ScreenShader {
                shader_program,
                uniform_locations,
                vertex_array,
            });
        }

        screen_shaders
    }

    unsafe fn create_screen_texture(gl: &mut glow::Context) -> glow::NativeTexture {
        let screen_texture = gl.create_texture().unwrap();
        gl.active_texture(glow::TEXTURE0);
        gl.bind_texture(glow::TEXTURE_2D, Some(screen_texture));

        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_MIN_FILTER,
            glow::NEAREST as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_MAG_FILTER,
            glow::NEAREST as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_WRAP_S,
            glow::CLAMP_TO_EDGE as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_WRAP_T,
            glow::CLAMP_TO_EDGE as i32,
        );

        screen_texture
    }

    unsafe fn create_colors_texture(gl: &mut glow::Context) -> glow::NativeTexture {
        let colors_texture = gl.create_texture().unwrap();
        gl.active_texture(glow::TEXTURE1);
        gl.bind_texture(glow::TEXTURE_2D, Some(colors_texture));

        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_MIN_FILTER,
            glow::NEAREST as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_MAG_FILTER,
            glow::NEAREST as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_WRAP_S,
            glow::CLAMP_TO_EDGE as i32,
        );
        gl.tex_parameter_i32(
            glow::TEXTURE_2D,
            glow::TEXTURE_WRAP_T,
            glow::CLAMP_TO_EDGE as i32,
        );

        colors_texture
    }
}

impl Pyxel {
    pub fn clip(&self, x: f32, y: f32, width: f32, height: f32) {
        self.screen.lock().clip(x, y, width, height);
    }

    pub fn clip0(&self) {
        self.screen.lock().clip0();
    }

    pub fn camera(&self, x: f32, y: f32) {
        self.screen.lock().camera(x, y);
    }

    pub fn camera0(&self) {
        self.screen.lock().camera0();
    }

    pub fn pal(&self, src_color: Color, dst_color: Color) {
        self.screen.lock().pal(src_color, dst_color);
    }

    pub fn pal0(&self) {
        self.screen.lock().pal0();
    }

    pub fn dither(&self, alpha: f32) {
        self.screen.lock().dither(alpha);
    }

    pub fn cls(&self, color: Color) {
        self.screen.lock().cls(color);
    }

    pub fn pget(&self, x: f32, y: f32) -> Color {
        self.screen.lock().pget(x, y)
    }

    pub fn pset(&self, x: f32, y: f32, color: Color) {
        self.screen.lock().pset(x, y, color);
    }

    pub fn line(&self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color) {
        self.screen.lock().line(x1, y1, x2, y2, color);
    }

    pub fn rect(&self, x: f32, y: f32, width: f32, height: f32, color: Color) {
        self.screen.lock().rect(x, y, width, height, color);
    }

    pub fn rectb(&self, x: f32, y: f32, width: f32, height: f32, color: Color) {
        self.screen.lock().rectb(x, y, width, height, color);
    }

    pub fn circ(&self, x: f32, y: f32, radius: f32, color: Color) {
        self.screen.lock().circ(x, y, radius, color);
    }

    pub fn circb(&self, x: f32, y: f32, radius: f32, color: Color) {
        self.screen.lock().circb(x, y, radius, color);
    }

    pub fn elli(&self, x: f32, y: f32, width: f32, height: f32, color: Color) {
        self.screen.lock().elli(x, y, width, height, color);
    }

    pub fn ellib(&self, x: f32, y: f32, width: f32, height: f32, color: Color) {
        self.screen.lock().ellib(x, y, width, height, color);
    }

    pub fn tri(&self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color) {
        self.screen.lock().tri(x1, y1, x2, y2, x3, y3, color);
    }

    pub fn trib(&self, x1: f32, y1: f32, x2: f32, y2: f32, x3: f32, y3: f32, color: Color) {
        self.screen.lock().trib(x1, y1, x2, y2, x3, y3, color);
    }

    pub fn fill(&self, x: f32, y: f32, color: Color) {
        self.screen.lock().fill(x, y, color);
    }

    pub fn blt(
        &self,
        x: f32,
        y: f32,
        image_index: u32,
        image_x: f32,
        image_y: f32,
        width: f32,
        height: f32,
        color_key: Option<Color>,
        rotate: Option<f32>,
        scale: Option<f32>,
    ) {
        self.screen.lock().blt(
            x,
            y,
            self.images.lock()[image_index as usize].clone(),
            image_x,
            image_y,
            width,
            height,
            color_key,
            rotate,
            scale,
        );
    }

    pub fn bltm(
        &self,
        x: f32,
        y: f32,
        tilemap_index: u32,
        tilemap_x: f32,
        tilemap_y: f32,
        width: f32,
        height: f32,
        color_key: Option<Color>,
        rotate: Option<f32>,
        scale: Option<f32>,
    ) {
        self.screen.lock().bltm(
            x,
            y,
            self.tilemaps.lock()[tilemap_index as usize].clone(),
            tilemap_x,
            tilemap_y,
            width,
            height,
            color_key,
            rotate,
            scale,
        );
    }

    pub fn text(&self, x: f32, y: f32, string: &str, color: Color, font: Option<SharedFont>) {
        self.screen.lock().text(x, y, string, color, font);
    }

    pub(crate) fn render_screen(&mut self) {
        unsafe {
            let gl = pyxel_platform::gl_context();
            self.set_viewport(gl);
            self.use_screen_shader(gl);
            self.bind_screen_texture(gl);
            self.bind_colors_texture(gl);
            gl.draw_arrays(glow::TRIANGLE_STRIP, 0, 4);
        }
    }

    unsafe fn set_viewport(&self, gl: &mut glow::Context) {
        let (window_width, window_height) = pyxel_platform::window_size();
        gl.viewport(0, 0, window_width as i32, window_height as i32);
    }

    unsafe fn use_screen_shader(&self, gl: &mut glow::Context) {
        let shader = &self.graphics.screen_shaders[self.system.screen_mode as usize];
        gl.use_program(Some(shader.shader_program));
        let uniform_locations = &shader.uniform_locations;

        if let Some(location) = uniform_locations.get("u_screenPos") {
            let (_, window_height) = pyxel_platform::window_size();
            gl.uniform_2_f32(
                Some(location),
                self.system.screen_x as f32,
                (window_height as i32
                    - self.system.screen_y
                    - (self.height as f32 * self.system.screen_scale) as i32)
                    as f32,
            );
        }

        if let Some(location) = uniform_locations.get("u_screenSize") {
            gl.uniform_2_f32(
                Some(location),
                self.width as f32 * self.system.screen_scale,
                self.height as f32 * self.system.screen_scale,
            );
        }

        if let Some(location) = uniform_locations.get("u_screenScale") {
            gl.uniform_1_f32(Some(location), self.system.screen_scale);
        }

        if let Some(location) = uniform_locations.get("u_numColors") {
            gl.uniform_1_i32(Some(location), self.colors.lock().len() as i32);
        }

        if let Some(location) = uniform_locations.get("u_backgroundColor") {
            gl.uniform_3_f32(
                Some(location),
                ((BACKGROUND_COLOR >> 16) as u8) as f32 / 255.0,
                ((BACKGROUND_COLOR >> 8) as u8) as f32 / 255.0,
                (BACKGROUND_COLOR as u8) as f32 / 255.0,
            );
        }

        if let Some(location) = uniform_locations.get("u_screenTexture") {
            gl.uniform_1_i32(Some(location), 0);
        }

        if let Some(location) = uniform_locations.get("u_colorsTexture") {
            gl.uniform_1_i32(Some(location), 1);
        }

        gl.bind_vertex_array(Some(shader.vertex_array));
    }

    unsafe fn bind_screen_texture(&self, gl: &mut glow::Context) {
        gl.active_texture(glow::TEXTURE0);
        gl.bind_texture(glow::TEXTURE_2D, Some(self.graphics.screen_texture));
        gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 1);

        let (internal_format, format) = if pyxel_platform::gl_profile() == GLProfile::Gles {
            (glow::LUMINANCE as i32, glow::LUMINANCE)
        } else {
            (glow::R8 as i32, glow::RED)
        };

        gl.tex_image_2d(
            glow::TEXTURE_2D,
            0,
            internal_format,
            self.width as i32,
            self.height as i32,
            0,
            format,
            glow::UNSIGNED_BYTE,
            PixelUnpackData::Slice(Some(&self.screen.lock().canvas.data)),
        );
    }

    #[allow(clippy::uninlined_format_args)]
    unsafe fn bind_colors_texture(&self, gl: &mut glow::Context) {
        gl.active_texture(glow::TEXTURE1);
        gl.bind_texture(glow::TEXTURE_2D, Some(self.graphics.colors_texture));
        gl.pixel_store_i32(glow::UNPACK_ALIGNMENT, 4);

        let colors = self.colors.lock();
        assert!(
            !colors.is_empty() && colors.len() <= MAX_COLORS as usize,
            "Number of colors must be between 1 to {}",
            MAX_COLORS
        );

        let mut pixels: Vec<u8> = Vec::with_capacity(colors.len() * 3);
        for color in &*colors {
            pixels.push((color >> 16) as u8);
            pixels.push((color >> 8) as u8);
            pixels.push(*color as u8);
        }

        gl.tex_image_2d(
            glow::TEXTURE_2D,
            0,
            glow::RGB as i32,
            colors.len() as i32,
            1,
            0,
            glow::RGB,
            glow::UNSIGNED_BYTE,
            PixelUnpackData::Slice(Some(&pixels)),
        );
    }
}
