use glium::vertex::Vertex as GliumVertex;
use glium::{Blend, DrawParameters, IndexBuffer, Surface, VertexBuffer, uniform};
use glium::{Depth, DepthTest, implement_vertex};
use sge_color::Color;
use sge_config::{get_dithering, get_polygon_mode};
use sge_debugging::*;
use sge_math::transform::Transform2D;
use sge_programs::{
    CIRCLE_PROGRAM, FLAT_PROGRAM, RADIAL_PROGRAM, ROUNDED_PROGRAM, TEXTURED_PROGRAM,
};
use sge_shapes::d2::{QUAD_INDICES, Shape2D, UNIT_QUAD};
use sge_textures::TextureRef;
use sge_types::{
    CircleBatch, CircleInstance, RadialGradientBatch, RadialGradientInstance, RoundedBatch,
    RoundedInstance, ShapeBatch, Vertex2D,
};
use sge_vectors::{Mat4, Rect, Vec2, Vec3};
use sge_window::get_display;

use crate::scissor::current_scissor;

pub use queue::*;
pub use scene::*;

mod queue;
mod scene;

#[derive(Clone)]
pub enum DrawCommand {
    Shapes(ShapeBatch),
    Circles(CircleBatch),
    Rounded(RoundedBatch),
    Radial(RadialGradientBatch),
    Sprites(SpriteBatch),
}

#[derive(Clone)]
pub struct SpriteBatch {
    texture: TextureRef,
    vertices: Vec<SpriteVertex>,
    indices: Vec<u32>,
    scissor: Option<glium::Rect>,
}

implement_vertex!(SpriteVertex, position, tex_coords, color);
#[derive(Copy, Clone, Debug)]
pub struct SpriteVertex {
    pub position: [f32; 3],
    pub tex_coords: [f32; 2],
    pub color: [f32; 4],
}

#[derive(Clone, Copy)]
pub enum RendererType {
    Screen,
    World,
    Scene,
}

#[derive(Clone, Copy)]
pub struct Renderer2D {
    draws: *mut Vec<DrawCommand>,
    ty: RendererType,
}

impl Renderer2D {
    fn draws_mut(&mut self) -> &mut Vec<DrawCommand> {
        unsafe { &mut *self.draws }
    }

    fn draws(&self) -> &[DrawCommand] {
        unsafe { &*self.draws }
    }

    pub fn is_world(&self) -> bool {
        matches!(self.ty, RendererType::World)
    }

    pub fn current_shape_batch(&mut self) -> &mut ShapeBatch {
        let scissor = current_scissor();
        let needs_new = match self.draws().last() {
            Some(DrawCommand::Shapes(b)) => b.scissor != scissor,
            _ => true,
        };
        if needs_new {
            self.draws_mut()
                .push(DrawCommand::Shapes(ShapeBatch::new(scissor)));
        }
        match self.draws_mut().last_mut().unwrap() {
            DrawCommand::Shapes(b) => b,
            _ => unreachable!(),
        }
    }

    pub fn current_circle_batch(&mut self) -> &mut CircleBatch {
        let scissor = current_scissor();
        let needs_new = match self.draws().last() {
            Some(DrawCommand::Circles(b)) => b.scissor != scissor,
            _ => true,
        };
        if needs_new {
            self.draws_mut()
                .push(DrawCommand::Circles(CircleBatch::new(scissor)));
        }
        match self.draws_mut().last_mut().unwrap() {
            DrawCommand::Circles(b) => b,
            _ => unreachable!(),
        }
    }

    pub fn current_rounded_batch(&mut self) -> &mut RoundedBatch {
        let scissor = current_scissor();
        let needs_new = match self.draws().last() {
            Some(DrawCommand::Rounded(b)) => b.scissor != scissor,
            _ => true,
        };
        if needs_new {
            self.draws_mut()
                .push(DrawCommand::Rounded(RoundedBatch::new(scissor)));
        }
        match self.draws_mut().last_mut().unwrap() {
            DrawCommand::Rounded(b) => b,
            _ => unreachable!(),
        }
    }

    pub fn current_radial_batch(&mut self) -> &mut RadialGradientBatch {
        let scissor = current_scissor();
        let needs_new = match self.draws().last() {
            Some(DrawCommand::Radial(b)) => b.scissor != scissor,
            _ => true,
        };
        if needs_new {
            self.draws_mut()
                .push(DrawCommand::Radial(RadialGradientBatch::new(scissor)));
        }
        match self.draws_mut().last_mut().unwrap() {
            DrawCommand::Radial(b) => b,
            _ => unreachable!(),
        }
    }

    pub fn current_sprite_batch(&mut self, texture: TextureRef) -> &mut SpriteBatch {
        let scissor = current_scissor();

        let can_merge = match self.draws().last() {
            Some(DrawCommand::Sprites(b)) => b.texture == texture && b.scissor == scissor,
            _ => false,
        };

        if !can_merge {
            self.draws_mut().push(DrawCommand::Sprites(SpriteBatch {
                texture,
                vertices: Vec::new(),
                indices: Vec::new(),
                scissor,
            }));
        }

        match self.draws_mut().last_mut().unwrap() {
            DrawCommand::Sprites(b) => b,
            _ => unreachable!(),
        }
    }

    pub fn add_shape(&mut self, shape: &impl Shape2D) {
        debugger_add_drawn_objects(1);
        let batch = self.current_shape_batch();
        let (mut indices, vertices) = shape.gen_mesh(batch.max_index);
        for vertex in &vertices {
            batch.vertices.push(vertex.to_3d(0.0));
        }
        batch.max_index += vertices.len() as u32;
        batch.indices.append(&mut indices);
    }

    pub fn add_circle(&mut self, center: Vec2, radius: Vec2, color: Color) {
        debugger_add_drawn_objects(1);
        self.current_circle_batch()
            .instances
            .push(CircleInstance::new(center, 0.0, radius, color));
    }

    pub fn add_circle_with_outline(
        &mut self,
        center: Vec2,
        radius: Vec2,
        fill_color: Color,
        outline_thickness: f32,
        outline_color: Color,
    ) {
        debugger_add_drawn_objects(1);
        self.current_circle_batch()
            .instances
            .push(CircleInstance::new_with_outline(
                center,
                0.0,
                radius,
                fill_color,
                outline_thickness,
                outline_color,
            ));
    }

    #[allow(clippy::too_many_arguments)]
    pub fn add_radial_gradient(
        &mut self,
        center: Vec2,
        radius: Vec2,
        inner_color: Color,
        outer_color: Color,
        outline_thickness: f32,
        outline_color: Color,
        gradient_offset: Vec2,
    ) {
        debugger_add_drawn_objects(1);
        self.current_radial_batch()
            .instances
            .push(RadialGradientInstance {
                center: [center.x, center.y, 0.0],
                radius: [radius.x, radius.y],
                outline_thickness,
                inner_color: inner_color.for_gpu(),
                outer_color: outer_color.for_gpu(),
                outline_color: outline_color.for_gpu(),
                gradient_offset: [gradient_offset.x, gradient_offset.y],
            });
    }

    pub fn add_rounded_rectangle(
        &mut self,
        center: Vec2,
        dimensions: Vec2,
        corner_radius: f32,
        fill_color: Color,
        outline_thickness: f32,
        outline_color: Color,
    ) {
        debugger_add_drawn_objects(1);
        let shortest_side = dimensions.x.min(dimensions.y);
        self.current_rounded_batch()
            .instances
            .push(RoundedInstance::new(
                dimensions,
                center,
                0.0,
                corner_radius.clamp(0.0, (0.5 * shortest_side).max(0.0)),
                fill_color,
                outline_thickness,
                outline_color,
            ));
    }

    pub fn add_sprite(
        &mut self,
        texture: TextureRef,
        mut transform: Transform2D,
        color: Color,
        region: Option<Rect>,
    ) {
        debugger_add_drawn_objects(1);

        let (tex_min_x, tex_min_y, tex_max_x, tex_max_y) = if let Some(region) = region {
            let tex = texture.get();
            let tex_width = tex.gl_texture.width() as f32;
            let tex_height = tex.gl_texture.height() as f32;
            (
                region.min.x / tex_width,
                region.min.y / tex_height,
                region.max.x / tex_width,
                region.max.y / tex_height,
            )
        } else {
            (0.0, 0.0, 1.0, 1.0)
        };

        let color_gpu = color.for_gpu();
        let mat = transform.matrix();

        let corners = [
            Vec3::new(0.0, 0.0, 0.0),
            Vec3::new(1.0, 0.0, 0.0),
            Vec3::new(1.0, 1.0, 0.0),
            Vec3::new(0.0, 1.0, 0.0),
        ];

        let tex_coords = [
            [tex_min_x, tex_min_y],
            [tex_max_x, tex_min_y],
            [tex_max_x, tex_max_y],
            [tex_min_x, tex_max_y],
        ];

        let batch = self.current_sprite_batch(texture);
        let base_index = batch.vertices.len() as u32;

        for i in 0..4 {
            let v = mat.transform_point3(corners[i]);
            batch.vertices.push(SpriteVertex {
                #[cfg(not(feature = "round_coords"))]
                position: [v.x, v.y, 0.0],
                #[cfg(feature = "round_coords")]
                position: [v.x.round(), v.y.round(), 0.0],
                tex_coords: tex_coords[i],
                color: color_gpu,
            });
        }

        batch.indices.extend_from_slice(&[
            base_index,
            base_index + 1,
            base_index + 2,
            base_index,
            base_index + 2,
            base_index + 3,
        ]);
    }

    pub fn add_mesh(&mut self, vertices: &[Vertex2D], indices: &[u32]) {
        let batch = self.current_shape_batch();
        let base_index = batch.max_index;
        for v in vertices {
            batch.vertices.push(v.to_3d(0.0));
        }
        batch.indices.extend(indices.iter().map(|i| i + base_index));
        batch.max_index += vertices.len() as u32;
    }

    pub fn add_scene(&mut self, scene: &Scene2D) {
        self.draws_mut().append(&mut scene.clone().draws)
    }
}
