mod gl {
    pub use gl::types::*;
    pub use gl::*;
}
use errors::Error;
use std::{ffi,mem,ptr};

//

pub struct Window {
    events: glutin::event_loop::EventLoop<()>,
    gl_window: glutin::WindowedContext<glutin::PossiblyCurrent>,
}

impl Window {
    pub fn create() -> Result<Window, Error> {
        let events = glutin::event_loop::EventLoop::new();
        let wb = glutin::window::WindowBuilder::new();
        let context = glutin::ContextBuilder::new();
        let gl_window = context.build_windowed(wb, &events)?;

        let gl_window = unsafe { gl_window.make_current() }.unwrap();

        gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _);

        Ok(Window { events, gl_window })
    }

    pub fn run<F: 'static>(self, mut cb: F) -> Result<(), Error>
        where F: for<'a> FnMut(glutin::event::Event<'a, ()>) -> glutin::event_loop::ControlFlow
    {
        let Window { mut events, gl_window } = self;
        let mut result = Ok(());

        events.run(move |ev, _, tgt| {
            let r = cb(ev);

            match gl_window.swap_buffers() {
                Ok(()) => *tgt = r,
                Err(ctx) => {
                    result = Err(Error::GlutinContext(ctx));
                    *tgt = glutin::event_loop::ControlFlow::Exit
                }
            }
        })
    }
}

pub struct Shader {
    pub n: gl::GLuint,
}

impl Drop for Shader {
    fn drop(&mut self) {
        unsafe { gl::DeleteShader(self.n); }
    }
}

#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ShaderType {
    Fragment,
    Vertex,
}

impl ShaderType {
    fn to_glenum(&self) -> gl::GLenum {
        match *self {
            ShaderType::Fragment => gl::FRAGMENT_SHADER,
            ShaderType::Vertex => gl::VERTEX_SHADER,
        }
    }
}

impl Shader {
    pub fn compile(ty: ShaderType, src: &str) -> Result<Shader, Error> {
        let shader;
        unsafe {
            shader = gl::CreateShader(ty.to_glenum());
            let c_str = ffi::CString::new(src.as_bytes())?;
            gl::ShaderSource(shader, 1, &c_str.as_ptr(), ptr::null());
            gl::CompileShader(shader);

            let mut status = gl::FALSE as gl::GLint;
            gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status);

            if status != (gl::TRUE as gl::GLint) {
                let mut len = 0;
                gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len);
                let mut buf = Vec::with_capacity(len as usize);
                buf.set_len((len as usize) - 1);
                gl::GetShaderInfoLog(
                    shader,
                    len,
                    ptr::null_mut(),
                    buf.as_mut_ptr() as *mut gl::GLchar
                );
                return Err(Error::CompileError(String::from_utf8(buf)?));
            }
        }
        Ok(Shader { n: shader })
    }
}


//

pub struct Program {
    pub p: gl::GLuint,
    pub shaders: Vec<Shader>,
}

impl Drop for Program {
    fn drop(&mut self) {
        self.shaders = vec![];
        unsafe {
            gl::DeleteProgram(self.p)
        }
    }
}

impl Program {
    pub fn link(shaders: Vec<Shader>) -> Result<Program, Error> {
        unsafe {
            let program = gl::CreateProgram();
            for s in shaders.iter() {
                gl::AttachShader(program, s.n);
            }
            gl::LinkProgram(program);

            let mut status = gl::FALSE as gl::GLint;
            gl::GetProgramiv(program, gl::LINK_STATUS, &mut status);

            // Fail on error
            if status != (gl::TRUE as gl::GLint) {
                let mut len: gl::GLint = 0;
                gl::GetProgramiv(program, gl::INFO_LOG_LENGTH, &mut len);
                let mut buf = Vec::with_capacity(len as usize);
                buf.set_len((len as usize) - 1);
                gl::GetProgramInfoLog(
                    program,
                    len,
                    ptr::null_mut(),
                    buf.as_mut_ptr() as *mut gl::GLchar,
                );
                return Err(Error::LinkError(String::from_utf8(buf)?));
            }

            Ok(Program {
                p: program,
                shaders: shaders,
            })
        }
    }

    pub fn use_program(&self) {
        unsafe { gl::UseProgram(self.p); }
    }

    pub fn bind_frag_data_location(&self, idx: u32, s: &str) -> Result<(), Error> {
        unsafe {
            gl::BindFragDataLocation(
                self.p,
                idx,
                ffi::CString::new(s)?.as_ptr(),
            );
        }
        Ok(())
    }

    pub fn get_attrib_location(&self, s: &str) -> Result<i32, Error> {
        Ok(unsafe {
            gl::GetAttribLocation(
                self.p,
                ffi::CString::new(s)?.as_ptr(),
            )
        })
    }
}

//

pub struct VertexArray {
    idx: u32,
}

impl Drop for VertexArray {
    fn drop(&mut self) {
        unsafe { gl::DeleteVertexArrays(1, &self.idx); }
    }
}

pub struct VertexBuffer {
    idx: u32,
    data: VertexArray,
}

impl Drop for VertexBuffer {
    fn drop(&mut self) {
        unsafe { gl::DeleteBuffers(1, &self.idx); }
    }
}

impl VertexArray {
    pub fn new() -> VertexArray {
        let mut idx = 0;
        unsafe {
            gl::GenVertexArrays(1, &mut idx);
            gl::BindVertexArray(idx);
        }

        VertexArray { idx }
    }

    pub fn bind(&self) {
        unsafe {
            gl::BindVertexArray(self.idx);
        }
    }

    pub fn unbind(&self) {
        unsafe {
            gl::BindVertexArray(0);
        }
    }
}

impl VertexBuffer {
    pub fn new_array_buffer(
        data: VertexArray,
        vertex_data: &[gl::GLfloat],
    ) -> VertexBuffer {
        let mut idx = 0;
        unsafe {
            gl::GenBuffers(1, &mut idx);
            gl::BindBuffer(gl::ARRAY_BUFFER, idx);
            gl::BufferData(
                gl::ARRAY_BUFFER,
                (vertex_data.len() * mem::size_of::<gl::GLfloat>()) as gl::GLsizeiptr,
                mem::transmute(&vertex_data[0]),
                gl::STATIC_DRAW,
            );
        }

        VertexBuffer { idx, data }
    }

    pub fn bind(&self) {
        unsafe {
            self.data.bind();
            gl::BindBuffer(gl::ARRAY_BUFFER, self.idx);
        }
    }

    pub fn unbind(&self) {
        unsafe {
            gl::BindBuffer(gl::ARRAY_BUFFER, 0);
            self.data.unbind();
        }
    }
}
