Documentation
use alloc::string::String;
use alloc::vec::Vec;
use alloc::btree_map::BTreeMap;

use core::str::{self, Utf8Error};
use core::ptr;
use core::ops::Drop;

use regex::Regex;

use gl;
use gl::types::*;

use super::{AttributeKind, UniformKind, GLAttribute, GLUniform};


pub struct GLProgram {
    id: GLuint,
    uniforms: BTreeMap<String, GLUniform>,
    attributes: BTreeMap<String, GLAttribute>,
}

impl GLProgram {

    #[inline]
    pub fn new(vertex: &str, fragment: &str) -> Self {
        let mut program = GLProgram {
            id: 0,
            uniforms: BTreeMap::new(),
            attributes: BTreeMap::new(),
        };
        program.set(vertex, fragment);
        program
    }
    #[inline]
    pub fn new_mutiple(vertex: &[&str], fragment: &[&str]) -> Self {
        let mut program = GLProgram {
            id: 0,
            uniforms: BTreeMap::new(),
            attributes: BTreeMap::new(),
        };
        program.set_mutiple(vertex, fragment);
        program
    }

    #[inline(always)]
    pub fn id(&self) -> GLuint { self.id }

    #[inline]
    pub fn bind(&self) -> &Self {
        unsafe { gl::UseProgram(self.id); }
        self
    }
    #[inline]
    pub fn unbind(&self) -> &Self {
        unsafe { gl::UseProgram(0); }
        self
    }

    #[inline(always)]
    pub fn has_uniform(&self, name: &str) -> bool {
        self.uniforms.contains_key(name)
    }
    #[inline(always)]
    pub fn get_uniform(&self, name: &str) -> &GLUniform {
        match self.uniforms.get(name) {
            Some(ref uniform) => uniform,
            None => panic!("No uniform named {:?} found", name),
        }
    }
    #[inline(always)]
    pub fn uniforms(&self) -> &BTreeMap<String, GLUniform> {&self.uniforms}
    #[inline(always)]
    pub fn uniforms_mut(&mut self) -> &mut BTreeMap<String, GLUniform> {&mut self.uniforms}

    #[inline(always)]
    pub fn has_attribute(&self, name: &str) -> bool {
        self.attributes.contains_key(name)
    }

    #[inline(always)]
    pub fn get_attribute(&self, name: &str) -> &GLAttribute {
        match self.attributes.get(name) {
            Some(ref attribute) => attribute,
            None => panic!("No attribute named {:?} found", name),
        }
    }
    #[inline(always)]
    pub fn attributes(&self) -> &BTreeMap<String, GLAttribute> {&self.attributes}
    #[inline(always)]
    pub fn attributes_mut(&mut self) -> &mut BTreeMap<String, GLAttribute> {&mut self.attributes}

    #[inline]
    pub fn set(&mut self, vertex: &str, fragment: &str) -> &mut Self {
        let vs = compile_shader(vertex, gl::VERTEX_SHADER);
        let fs = compile_shader(fragment, gl::FRAGMENT_SHADER);
        let id = link_program(vs, fs);
        self.set_program_id(id)
    }
    #[inline]
    pub fn set_mutiple(&mut self, vertex: &[&str], fragment: &[&str]) -> &mut Self {
        let vs = compile_shaders(vertex, gl::VERTEX_SHADER);
        let fs = compile_shaders(fragment, gl::FRAGMENT_SHADER);
        let id = link_program(vs, fs);
        self.set_program_id(id)
    }

    #[inline]
    fn set_program_id(&mut self, id: GLuint) -> &mut Self {
        {
            let ref mut uniforms = self.uniforms;
            let ref mut attributes = self.attributes;

            if self.id != 0 {
                uniforms.clear();
                attributes.clear();
                unsafe { gl::DeleteProgram(self.id); }
            }

            self.id = id;
            parse_uniforms(id, uniforms);
            parse_attributes(id, attributes);
        }
        self
    }
}

impl Drop for GLProgram {
    #[inline]
    fn drop(&mut self) {
        if self.id != 0 {
            unsafe { gl::DeleteProgram(self.id); }
        }
    }
}

#[inline]
fn parse_uniforms(program: GLuint, uniforms: &mut BTreeMap<String, GLUniform>) {
    let mut max_length = 0;
    let mut active_length = 0;

    unsafe {
        gl::GetProgramiv(program, gl::ACTIVE_UNIFORM_MAX_LENGTH, &mut max_length);
        gl::GetProgramiv(program, gl::ACTIVE_UNIFORMS, &mut active_length);
    }

    for i in 0..active_length {
        let mut length = 0;
        let mut count = 0;
        let mut kind = 0;

        let mut buf = Vec::with_capacity(max_length as usize);
        let location;

        unsafe {
            let buf_ptr = buf.as_mut_ptr() as *mut GLchar;
            gl::GetActiveUniform(program, i as GLuint, max_length, &mut length, &mut count, &mut kind, buf_ptr);
            buf.set_len(length as usize);
            location = gl::GetUniformLocation(program, buf_ptr);
        }

        let mut name = match string_from_utf8(&buf) {
            Ok(string) => string,
            Err(vec) => panic!("Could not convert uniform name from buffer: {:?}", vec)
        };

        if name.chars().nth(name.len() - 1).expect("Unexpected empty uniform name") == ']' {
            let new_name = match
                Regex::new(r"(.*)\[\d+\]")
                    .expect("regex failed to compile")
                    .captures(&name) {
                     Some(cap) => match cap.get(1) {
                        Some(name) => Some(String::from(name.as_str())),
                        None => None,
                    },
                    None => None,
                };

            if let Some(value) = new_name {
                name = value;
            }
        }

        uniforms.insert(name, GLUniform::new(UniformKind::from_gl(kind), count as usize, location as usize));
    }
}

#[inline]
fn string_from_utf8(vec: &Vec<u8>) -> Result<String, Utf8Error> {
    match str::from_utf8(vec) {
        Ok(s) => Ok(String::from(s)),
        Err(e) => Err(e),
    }
}

#[inline]
fn parse_attributes(program: GLuint, attributes: &mut BTreeMap<String, GLAttribute>) {
    let mut max_length = 0;
    let mut active_length = 0;

    unsafe {
        gl::GetProgramiv(program, gl::ACTIVE_ATTRIBUTE_MAX_LENGTH, &mut max_length);
        gl::GetProgramiv(program, gl::ACTIVE_ATTRIBUTES, &mut active_length);
    }

    for i in 0..active_length {
        let mut length = 0;
        let mut count = 0;
        let mut kind = 0;

        let mut buf = Vec::with_capacity(max_length as usize);
        let buf_ptr = buf.as_mut_ptr() as *mut GLchar;
        let location;

        unsafe {
            gl::GetActiveAttrib(program, i as u32, max_length, &mut length, &mut count, &mut kind, buf_ptr);
            buf.set_len(length as usize);
            location = gl::GetAttribLocation(program, buf_ptr);
        }

        let name = match string_from_utf8(&buf) {
            Ok(string) => string,
            Err(vec) => panic!("Could not convert attribute name from buffer: {:?}", vec)
        };

        attributes.insert(name, GLAttribute::new(AttributeKind::from_gl(kind), count as usize, location as usize));
    }
}

#[inline]
pub fn link_program(vertex_shader: GLuint, fragment_shader: GLuint) -> GLuint {
    let program = unsafe { gl::CreateProgram() };

    unsafe {
        gl::AttachShader(program, vertex_shader);
        gl::DeleteShader(vertex_shader);

        gl::AttachShader(program, fragment_shader);
        gl::DeleteShader(fragment_shader);

        gl::LinkProgram(program);
        gl::ValidateProgram(program);
        gl::UseProgram(program);
    }
    check_program_status(program)
}

#[inline]
pub fn check_program_status(program: GLuint) -> GLuint {
    let mut status = 0;
    unsafe { gl::GetProgramiv(program, gl::LINK_STATUS, &mut status) };
    if status != (gl::TRUE as GLint) {
        let mut len: GLint = 0;
        unsafe { gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut len); }
        let mut buf = Vec::with_capacity(len as usize);
        unsafe {
            buf.set_len(len as usize);
            gl::GetProgramInfoLog(program, len, ptr::null_mut(), buf.as_mut_ptr() as *mut GLchar);
        }
        panic!("{}", str::from_utf8(&buf).ok().expect("ProgramInfoLog not valid utf8"));
    }
    program
}

#[inline]
pub fn compile_shader(source: &str, kind: GLenum) -> GLuint {
    let shader = unsafe { gl::CreateShader(kind) };

    unsafe {
        let ptr: *const GLchar = source.as_bytes().as_ptr() as *const GLchar;
        let len = source.len() as GLint;
        gl::ShaderSource(shader, 1, &ptr, &len);
        gl::CompileShader(shader);
    }
    check_shader_status(shader)
}

#[inline]
pub fn compile_shaders(sources: &[&str], kind: GLenum) -> GLuint {
    let shader = unsafe { gl::CreateShader(kind) };

    unsafe {
        for source in sources.iter() {
            let ptr: *const GLchar = source.as_bytes().as_ptr() as *const GLchar;
            let len = source.len() as GLint;
            gl::ShaderSource(shader, 1, &ptr, &len);
            gl::CompileShader(shader);
        }
    }
    check_shader_status(shader)
}

#[inline]
pub fn check_shader_status(shader: GLuint) -> GLuint {
    let mut status = 0;
    unsafe { gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status) };
    if status != (gl::TRUE as GLint) {
        let mut len = 0;
        unsafe { gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len); }
        let mut buf = Vec::with_capacity(len as usize);
        unsafe {
            buf.set_len(len as usize);
            gl::GetShaderInfoLog(shader, len, ptr::null_mut(), buf.as_mut_ptr() as *mut GLchar);
        }
        panic!("{}", str::from_utf8(&buf).ok().expect("ShaderInfoLog not valid utf8"));
    }
    shader
}