use gl::*;
use gl::types::*;
use glam::{Mat4, Vec3};

use std::ptr;
use std::ffi::CStr;

#[derive(Debug, Eq, PartialEq, Copy, Clone)]
pub struct Shader {
    pub id: u32,
}

impl Shader {
    pub fn new(shader_type: GLenum, code: &str) -> Self {
        unsafe {
            let shader = CreateShader(shader_type);
            let cstr = std::ffi::CString::new(code.as_bytes()).unwrap();
            ShaderSource(shader, 1, &cstr.as_ptr(), std::ptr::null());
            CompileShader(shader);
            check_shader_error(shader);

            let id = CreateProgram();
            AttachShader(id, shader);
            LinkProgram(id);
            check_shader_link_error(id);

            DeleteShader(shader);

            Self { id }
        }
    }

    pub fn new_pipeline(vs_code: &str, fs_code: &str) -> Self {
        unsafe {
            let vs = CreateShader(VERTEX_SHADER);
            let cstr = std::ffi::CString::new(vs_code.as_bytes()).unwrap();
            ShaderSource(vs, 1, &cstr.as_ptr(), std::ptr::null());
            CompileShader(vs);
            check_shader_error(vs);

            let fs = CreateShader(FRAGMENT_SHADER);
            let cstr = std::ffi::CString::new(fs_code.as_bytes()).unwrap();
            ShaderSource(fs, 1, &cstr.as_ptr(), std::ptr::null());
            CompileShader(fs);
            check_shader_error(fs);

            let id = CreateProgram();
            AttachShader(id, vs);
            AttachShader(id, fs);
            LinkProgram(id);
            check_shader_link_error(id);

            Self { id }
        }
    }

    pub unsafe fn use_shader(&self) {
        UseProgram(self.id);
    }

    pub unsafe fn stop_shader(&self) {

    }

    pub unsafe fn uniform_1f(&self, name: &CStr, val: f32) {
        Uniform1f(GetUniformLocation(self.id, name.as_ptr()), val);
    }

    pub unsafe fn uniform_mat4fv(&self, name: &CStr, mat: &Mat4) {
        UniformMatrix4fv(
            GetUniformLocation(self.id, name.as_ptr()), 
            1, 
            FALSE, 
            mat.as_ref() as *const f32
        );
    }

    pub unsafe fn uniform_vec3f(&self, name: &CStr, vec: &Vec3) {
        Uniform3f(
            GetUniformLocation(self.id, name.as_ptr()),
            vec.x, vec.y, vec.z
        );
    }
}

pub unsafe fn check_shader_error(shader: u32) {
    let mut success = gl::FALSE as GLint;
    let mut info_log = Vec::with_capacity(512);
    info_log.set_len(54); // skip the trailing null char
    GetShaderiv(shader, COMPILE_STATUS, &mut success);
    if success != gl::TRUE as GLint {
        GetShaderInfoLog(
            shader,
            512,
            ptr::null_mut(),
            info_log.as_mut_ptr() as *mut GLchar,
        );
        println!(
            "ERROR::SHADER::COMPILATION::FAILED\n{}",
            std::str::from_utf8(&info_log).unwrap()
        );
    }
}

pub unsafe fn check_shader_link_error(program: u32) {
    let mut success = gl::FALSE as GLint;
    let mut info_log = Vec::with_capacity(512);
    gl::GetProgramiv(program, gl::LINK_STATUS, &mut success);
    if success != gl::TRUE as GLint {
        gl::GetProgramInfoLog(
            program,
            512,
            ptr::null_mut(),
            info_log.as_mut_ptr() as *mut GLchar,
        );
        println!(
            "ERROR::SHADER::PROGRAM::COMPILATION_FAILED\n{}",
            std::str::from_utf8(&info_log).unwrap()
        );
    }
}

#[macro_export]
macro_rules! cstr {
    ($s:expr) => {
        CString::new($s).expect("CString conversion failed")
            .as_c_str()
    };
}