use glow::HasContext;

use super::shader::Shader;
use super::snippets::{CodeType, Snippet};
use crate::graphics::transforms::{viewport::Viewport, Transform};

#[derive(Debug)]
pub struct Program {
    version: &'static str,
    vertex: Shader,
    fragment: Shader,

    geometry: Option<Shader>,
    pub native_program: Option<<glow::Context as HasContext>::Program>,
}

impl Program {
    pub fn new(
        vertex: Shader,
        fragment: Shader,
        geometry: Option<Shader>,
        version: &'static str,
    ) -> Self {
        let res = Self {
            version,
            vertex,
            fragment,
            geometry,
            native_program: None,
        };

        res
    }

    pub fn vertex(&self) -> &Shader {
        &self.vertex
    }

    pub fn fragment(&self) -> &Shader {
        &self.fragment
    }

    pub fn set_hook(&mut self, hook: &str, code: &Snippet) {
        self.vertex.set_hook(hook, code);
        self.fragment.set_hook(hook, code);
    }

    pub fn set_transform<T: Transform>(&mut self, value: &T) {
        self.set_hook("transform", value.snippet());
    }

    pub fn set_viewport(&mut self, viewport: &Viewport) {
        self.set_hook("viewport", viewport.snippet());
    }

    pub fn compile(&mut self, gl: &glow::Context) -> crate::errors::Result<()> {
        use crate::errors::Error;
        unsafe {
            // let vertex_array = gl
            //     .create_vertex_array()
            //     .expect("Cannot create vertex array");

            let program = gl.create_program().map_err(|e| Error::InvalidProgram(e))?;

            let vertex_shader =
                self.create_shader(gl, &self.vertex.to_string(),self.version, glow::VERTEX_SHADER);
            let fragment_shader =
                self.create_shader(gl, &self.fragment.to_string(),self.version, glow::FRAGMENT_SHADER);

            gl.link_program(program);
            if !gl.get_program_link_status(program) {
                return Err(Error::InvalidProgram(gl.get_program_info_log(program)));
            }

            gl.detach_shader(program, vertex_shader);
            gl.detach_shader(program, fragment_shader);

            self.native_program = Some(program);
        }

        Ok(())
    }

    fn create_shader(
        &self,
        gl: &glow::Context,
        code: &str,
        version: &'static str,
        shader_type: u32,
    ) -> <glow::Context as HasContext>::Shader {
        let shader = unsafe {
            let shader = gl
                .create_shader(glow::VERTEX_SHADER)
                .expect("Cannot create shader");
            gl.shader_source(
                shader,
                &format!("{}\n{}", &format!("#version {}", self.version), code),
            );
            gl.compile_shader(shader);
            if !gl.get_shader_compile_status(shader) {
                panic!("{}", gl.get_shader_info_log(shader))
            }
            shader
        };
        shader
    }

    pub fn destroy(&self, gl: &glow::Context) {
        unsafe {
            self.native_program.map(|p| gl.delete_program(p));
        }
    }

    pub fn render(&self, gl: &glow::Context) {
        unsafe {
            gl.clear_color(0.05, 0.05, 0.1, 1.0);
            gl.clear(glow::COLOR_BUFFER_BIT);
            gl.use_program(self.native_program);
        }
    }
}

mod test {
    use super::*;

    #[test]
    fn test_program() {
        let vertex = Shader::new(
            glow::VERTEX_SHADER,
            CodeType::<&'static str>::Path("agg-fast-path.vert".into()),
        )
        .unwrap();

        let fragment = Shader::new(
            glow::FRAGMENT_SHADER,
            CodeType::<&'static str>::Path("agg-fast-path.frag".into()),
        )
        .unwrap();

        let program = Program::new(vertex, fragment, None, "330 core");
    }
}
