use crate::{
    core::{math::Matrix4Ext, math::Rect, scope_profile},
    renderer::framework::{
        error::FrameworkError,
        framebuffer::{CullFace, DrawParameters, FrameBuffer},
        gpu_program::{GpuProgram, UniformLocation},
        gpu_texture::GpuTexture,
        state::PipelineState,
    },
    renderer::{GeometryCache, RenderPassStatistics, TextureCache},
    scene::mesh::surface::SurfaceData,
    scene::{camera::Camera, graph::Graph, node::Node},
};
use std::{cell::RefCell, rc::Rc};

struct SpriteShader {
    program: GpuProgram,
    view_projection_matrix: UniformLocation,
    world_matrix: UniformLocation,
    camera_side_vector: UniformLocation,
    camera_up_vector: UniformLocation,
    color: UniformLocation,
    diffuse_texture: UniformLocation,
    size: UniformLocation,
    rotation: UniformLocation,
}

impl SpriteShader {
    pub fn new(state: &mut PipelineState) -> Result<Self, FrameworkError> {
        let fragment_source = include_str!("shaders/sprite_fs.glsl");
        let vertex_source = include_str!("shaders/sprite_vs.glsl");
        let program = GpuProgram::from_source(state, "FlatShader", vertex_source, fragment_source)?;
        Ok(Self {
            view_projection_matrix: program.uniform_location(state, "viewProjectionMatrix")?,
            world_matrix: program.uniform_location(state, "worldMatrix")?,
            camera_side_vector: program.uniform_location(state, "cameraSideVector")?,
            camera_up_vector: program.uniform_location(state, "cameraUpVector")?,
            size: program.uniform_location(state, "size")?,
            diffuse_texture: program.uniform_location(state, "diffuseTexture")?,
            color: program.uniform_location(state, "color")?,
            rotation: program.uniform_location(state, "rotation")?,
            program,
        })
    }
}

pub struct SpriteRenderer {
    shader: SpriteShader,
    surface: SurfaceData,
}

pub(in crate) struct SpriteRenderContext<'a, 'b, 'c> {
    pub state: &'a mut PipelineState,
    pub framebuffer: &'b mut FrameBuffer,
    pub graph: &'c Graph,
    pub camera: &'c Camera,
    pub white_dummy: Rc<RefCell<GpuTexture>>,
    pub viewport: Rect<i32>,
    pub textures: &'a mut TextureCache,
    pub geom_map: &'a mut GeometryCache,
}

impl SpriteRenderer {
    pub fn new(state: &mut PipelineState) -> Result<Self, FrameworkError> {
        let surface = SurfaceData::make_collapsed_xy_quad();

        Ok(Self {
            shader: SpriteShader::new(state)?,
            surface,
        })
    }

    #[must_use]
    pub(in crate) fn render(&mut self, args: SpriteRenderContext) -> RenderPassStatistics {
        scope_profile!();

        let mut statistics = RenderPassStatistics::default();

        let SpriteRenderContext {
            state,
            framebuffer,
            graph,
            camera,
            white_dummy,
            viewport,
            textures,
            geom_map,
        } = args;

        state.set_blend_func(glow::SRC_ALPHA, glow::ONE_MINUS_SRC_ALPHA);

        let initial_view_projection = camera.view_projection_matrix();

        let inv_view = camera.inv_view_matrix().unwrap();

        let camera_up = inv_view.up();
        let camera_side = inv_view.side();

        for sprite in graph.linear_iter().filter_map(|node| {
            if !node.global_visibility() {
                return None;
            }

            if let Node::Sprite(sprite) = node {
                Some(sprite)
            } else {
                None
            }
        }) {
            let view_projection = if sprite.depth_offset_factor() != 0.0 {
                let mut projection = camera.projection_matrix();
                projection[14] -= sprite.depth_offset_factor();
                projection * camera.view_matrix()
            } else {
                initial_view_projection
            };

            let diffuse_texture = if let Some(texture) = sprite.texture_ref() {
                if let Some(texture) = textures.get(state, texture) {
                    texture
                } else {
                    white_dummy.clone()
                }
            } else {
                white_dummy.clone()
            };

            statistics += framebuffer.draw(
                geom_map.get(state, &self.surface),
                state,
                viewport,
                &self.shader.program,
                &DrawParameters {
                    cull_face: CullFace::Back,
                    culling: true,
                    color_write: Default::default(),
                    depth_write: false,
                    stencil_test: false,
                    depth_test: true,
                    blend: true,
                },
                |program_binding| {
                    program_binding
                        .set_texture(&self.shader.diffuse_texture, &diffuse_texture)
                        .set_matrix4(&self.shader.view_projection_matrix, &view_projection)
                        .set_matrix4(&self.shader.world_matrix, &sprite.global_transform())
                        .set_vector3(&self.shader.camera_up_vector, &camera_up)
                        .set_vector3(&self.shader.camera_side_vector, &camera_side)
                        .set_float(&self.shader.size, sprite.size())
                        .set_color(&self.shader.color, &sprite.color())
                        .set_float(&self.shader.rotation, sprite.rotation());
                },
            );
        }

        statistics
    }
}