use glow::{HasContext, NativeUniformLocation};

use super::shader::Shader;
use super::snippets::{CodeType, Snippet};
use crate::components::CodeComponent;
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);
        self.geometry.as_mut().map(|g| g.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;

        self.vertex.define("_GLUMPY__VERTEX_SHADER__");
        self.fragment.define("_GLUMPY__FRAGMENT_SHADER__");

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

            let vertex_shader = self.vertex.compile(&self.version, gl);
            let fragment_shader = self.fragment.compile(&self.version, gl);

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

            // Geom Shader
            let geom_shader = if let Some(geometry) = self.geometry.as_mut() {
                geometry.define("_GLUMPY__GEOMETRY_SHADER__");
                let geometry_shader = geometry.compile(&self.version, gl);

                gl.attach_shader(program, geometry_shader);

                Some(geometry_shader)
            } else {
                None
            };

            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);
            if let Some(_) = self.geometry.as_ref() {
                gl.detach_shader(program, geom_shader.unwrap());
                gl.delete_shader(geom_shader.unwrap());
            }

            gl.delete_shader(vertex_shader);
            gl.delete_shader(fragment_shader);

            gl.use_program(Some(program));

            self.native_program = Some(program);
        }

        Ok(())
    }

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

    pub fn get_uniform_location(
        &self,
        gl: &glow::Context,
        name: &str,
    ) -> Option<NativeUniformLocation> {
        self.native_program
            .as_ref()
            .map(|p| unsafe {
                let location = gl.get_uniform_location(*p, name);
                location
            })
            .flatten()
    }
}

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");
    }
}
