use crate::binding::UniformLocation;
use crate::error::FilterChainError;
use crate::gl::CompileProgram;
use crate::util;
use glow::HasContext;
use librashader_cache::Cacheable;
use librashader_reflect::back::glsl::CrossGlslContext;
use librashader_reflect::back::ShaderCompilerOutput;
use spirv_cross2::reflect::ResourceType;
use spirv_cross2::spirv::Decoration;

pub struct Gl4CompileProgram;

struct GlProgramBinary(Option<glow::ProgramBinary>);

impl Cacheable for GlProgramBinary {
    fn from_bytes(cached: &[u8]) -> Option<Self>
    where
        Self: Sized,
    {
        let mut cached = Vec::from(cached);
        let format = cached.split_off(cached.len() - std::mem::size_of::<u32>());
        let format: Option<&u32> = bytemuck::try_from_bytes(&format).ok();
        let Some(format) = format else {
            return None;
        };

        Some(GlProgramBinary(Some(glow::ProgramBinary {
            buffer: cached,
            format: *format,
        })))
    }

    fn to_bytes(&self) -> Option<Vec<u8>> {
        let Some(binary) = &self.0 else { return None };

        let mut slice = binary.buffer.clone();
        slice.extend(bytemuck::bytes_of(&binary.format));
        Some(slice)
    }
}

impl CompileProgram for Gl4CompileProgram {
    fn compile_program(
        context: &glow::Context,
        glsl: ShaderCompilerOutput<String, CrossGlslContext>,
        cache: bool,
    ) -> crate::error::Result<(glow::Program, UniformLocation<Option<u32>>)> {
        fn compile_shader(
            context: &glow::Context,
            resources: &CrossGlslContext,
            vertex: &str,
            fragment: &str,
        ) -> crate::error::Result<glow::Program> {
            unsafe {
                let vertex_resources = resources.artifact.vertex.shader_resources()?;
                let vertex = util::gl_compile_shader(context, glow::VERTEX_SHADER, vertex)?;
                let fragment = util::gl_compile_shader(context, glow::FRAGMENT_SHADER, fragment)?;

                let program = context
                    .create_program()
                    .map_err(|e| FilterChainError::GlProgramError(e))?;

                context.attach_shader(program, vertex);
                context.attach_shader(program, fragment);

                for res in vertex_resources.resources_for_type(ResourceType::StageInput)? {
                    let Some(loc) = resources
                        .artifact
                        .vertex
                        .decoration(res.id, Decoration::Location)?
                        .and_then(|d| d.as_literal())
                    else {
                        continue;
                    };

                    context.bind_attrib_location(program, loc, &res.name);
                }
                context.program_binary_retrievable_hint(program, true);
                context.link_program(program);
                context.delete_shader(vertex);
                context.delete_shader(fragment);

                if !context.get_program_link_status(program) {
                    let log = context.get_program_info_log(program);
                    return Err(FilterChainError::GLLinkError(log));
                }
                Ok(program)
            }
        }

        let program = librashader_cache::cache_shader_object(
            "opengl4",
            &[glsl.vertex.as_str(), glsl.fragment.as_str()],
            |&[vertex, fragment]| unsafe {
                let program = compile_shader(context, &glsl.context, vertex, fragment)?;
                let program_binary = context.get_program_binary(program);
                context.delete_program(program);
                Ok(GlProgramBinary(program_binary))
            },
            |binary| unsafe {
                let program = context
                    .create_program()
                    .map_err(|e| FilterChainError::GlProgramError(e))?;

                if let Some(binary) = &binary.0 {
                    context.program_binary(program, binary);
                }

                if !context.get_program_link_status(program) {
                    context.delete_program(program);
                    return compile_shader(
                        context,
                        &glsl.context,
                        glsl.vertex.as_str(),
                        glsl.fragment.as_str(),
                    );
                }

                return Ok(program);
            },
            !cache,
        )?;

        let ubo_location = unsafe {
            for (name, binding) in &glsl.context.sampler_bindings {
                let location = context.get_uniform_location(program, name.as_str());
                if let Some(location) = location {
                    context.program_uniform_1_i32(program, Some(&location), *binding as i32);
                }
            }

            UniformLocation {
                vertex: context.get_uniform_block_index(program, "LIBRA_UBO_VERTEX"),
                fragment: context.get_uniform_block_index(program, "LIBRA_UBO_FRAGMENT"),
            }
        };

        Ok((program, ubo_location))
    }
}
