mod gl {
    pub use gl::types::*;
    pub use gl::*;
}
use glutin;

pub mod errors;
pub mod vertices;

pub use self::errors::Error;
use std::{ffi,mem,ptr};
use imagefmt::{Image, ColFmt};
use std::marker::PhantomData;
use std::os::raw::c_void;

/// Clear the screen to a generic color
pub fn clear() {
    unsafe {
        gl::Enable(gl::BLEND);
        gl::BlendFunc(gl::SRC_ALPHA, gl::ONE_MINUS_SRC_ALPHA);
        gl::ClearColor(0.3, 0.3, 0.3, 1.0);
        gl::Clear(gl::COLOR_BUFFER_BIT);
    }
}

/// Abstract over an OpenGL context as well as a basic event loop
pub struct Window {
    events: glutin::EventsLoop,
    gl_window: glutin::GlWindow,
}

impl Window {
    /// Attempt to create a new window, and load all the relevant
    /// OpenGL symbols in the process
    pub fn create() -> Result<Window, Error> {
        use glutin::GlContext;
        let events = glutin::EventsLoop::new();
        let window = glutin::WindowBuilder::new();
        let context = glutin::ContextBuilder::new();
        let gl_window = glutin::GlWindow::new(window, context, &events)?;

        unsafe { gl_window.make_current() }?;

        gl::load_with(|symbol| gl_window.get_proc_address(symbol) as *const _);
        Ok(Window { events, gl_window })
    }

    /// Run the main event loop, calling the supplied callback with
    /// every event
    pub fn run<F>(self, mut cb: F) -> Result<(), Error>
        where F: FnMut(glutin::Event) -> glutin::ControlFlow
    {
        use glutin::GlContext;
        let Window { mut events, gl_window } = self;
        let mut result = Ok(());

        events.run_forever(|ev| {
            let r = cb(ev);

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

        result
    }

}

/// Wrap an OpenGL shader. This will automatically be cleaned up from
/// the OpenGL state machine when it is dropped.
pub struct Shader {
    pub n: gl::GLuint,
}

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

/// Right now, we're only concerned with either Fragment or Vertex
/// shaders. We might eventually want Geometry shaders, as well.
#[derive(PartialEq, Eq, Debug, Copy, Clone)]
pub enum ShaderType {
    Fragment,
    Vertex,
}

impl ShaderType {
    /// Convert a ShaderType value to OpenGL's enum value for the same
    fn to_glenum(&self) -> gl::GLenum {
        match *self {
            ShaderType::Fragment => gl::FRAGMENT_SHADER,
            ShaderType::Vertex => gl::VERTEX_SHADER,
        }
    }
}

impl Shader {
    /// Compile a shade string and provide our Rust-level handle to it
    pub fn compile(ty: ShaderType, src: &str) -> Result<Shader, Error> {
        // this structure is gonna repeat a few times but also can't
        // really get abstracted over in a clean way, so this comment
        // could be consulted for the other functions, too.
        let shader;
        unsafe {
            // we start by getting a new shader from OpenGL
            shader = gl::CreateShader(ty.to_glenum());
            // and also get the C string version of the source code
            let c_str = ffi::CString::new(src.as_bytes())?;

            // and then we compile it!
            gl::ShaderSource(shader, 1, &c_str.as_ptr(), ptr::null());
            gl::CompileShader(shader);

            // this is the common part: we now ask OpenGL for the
            // status of that compile, which OpenGL gives us by
            // updating a pointer
            let mut status = gl::FALSE as gl::GLint;
            gl::GetShaderiv(shader, gl::COMPILE_STATUS, &mut status);

            // If it's not good...
            if status != (gl::TRUE as gl::GLint) {
                // then then need to get the length of the error
                // message, which, again we fetch via pointer
                let mut len = 0;
                gl::GetShaderiv(shader, gl::INFO_LOG_LENGTH, &mut len);

                // create the buffer that's big enough for that
                // message
                let mut buf = Vec::with_capacity(len as usize);
                buf.set_len((len as usize) - 1);
                // and then fill it in via OpenGL's mechanisms
                gl::GetShaderInfoLog(
                    shader,
                    len,
                    ptr::null_mut(),
                    buf.as_mut_ptr() as *mut gl::GLchar
                );
                // and finally return it!
                return Err(Error::CompileError(String::from_utf8(buf)?));
            }
        }

        Ok(Shader { n: shader })
    }
}


//

/// Represents a linked set of OpenGL shaders.
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 {
    /// Link several existing shaders into a single Program
    pub fn link(shaders: Vec<Shader>) -> Result<Program, Error> {
        // the stuff in here looks a lot like Shader::compile, so
        // consult the inline docs for that to figure out what's going on
        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 fn set_texture(
        &self,
        s: &str,
        tex: &Texture,
        n: u32,
    ) -> Result<(), Error> {
        unsafe {
            gl::ActiveTexture(if n < gl::MAX_COMBINED_TEXTURE_IMAGE_UNITS {
                gl::TEXTURE0 + n
            } else {
                panic!("Texture unit {} too high", n)
            });
            gl::BindTexture(gl::TEXTURE_2D, tex.idx);
            let t = gl::GetUniformLocation(
                self.p,
                ffi::CString::new(s)?.as_ptr(),
            );
            gl::Uniform1i(t, n as i32);
        }
        Ok(())
    }
}

//

pub struct VertexArray<'p> {
    idx: u32,
    program: &'p Program,
}

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

pub struct VertexBuffer<'p, Vtx> {
    idx: u32,
    data: VertexArray<'p>,
    phantom: PhantomData<Vtx>,
    len: usize,
}

pub struct VBORef<'a, Vtx : 'a> {
    rf: &'a VertexBuffer<'a, Vtx>,
}

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

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

        VertexArray { idx, program }
    }

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

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

impl<'p, Vtx: vertices::Vertex> VertexBuffer<'p, Vtx> {
    pub fn new_array_buffer(
        data: VertexArray<'p>,
        vertex_data: &[Vtx],
    ) -> VertexBuffer<'p, Vtx> {
        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::<Vtx>()) as gl::GLsizeiptr,
                mem::transmute(&vertex_data[0]),
                gl::STATIC_DRAW,
            );
        }

        let phantom = PhantomData;
        let len = vertex_data.len();
        VertexBuffer { idx, data, phantom, len }
    }

    pub fn with<F, T>(&self, f: F) -> T
    where F: for<'a> Fn(VBORef<'a, Vtx>) -> T
    {
        unsafe {
            self.data.bind();
            gl::BindBuffer(gl::ARRAY_BUFFER, self.idx);
        }
        let res = f(VBORef { rf: &self });
        unsafe {
            gl::BindBuffer(gl::ARRAY_BUFFER, 0);
            self.data.unbind();
        }
        res
    }
}


impl<'p, Vtx: vertices::Vertex> VBORef<'p, Vtx> {
    pub fn draw(&self) {
        unsafe {
            gl::DrawArrays(gl::TRIANGLES, 0, self.rf.len as i32);
        }
    }

    pub fn bind_position(&self, name: &str) -> Result<(), Error> {
        let attr = self.rf.data.program.get_attrib_location(name)?;
        let (offset, num) = Vtx::pos_location();
        unsafe {
            let off = ptr::null::<u8>().offset(
                (mem::size_of::<gl::GLfloat>() * offset as usize) as isize);
            gl::EnableVertexAttribArray(attr as gl::GLuint);
            gl::VertexAttribPointer(
                attr as gl::GLuint,
                num,
                gl::FLOAT,
                gl::FALSE as gl::GLboolean,
                Vtx::size() as i32,
                off as *const c_void,
            )
        }
        Ok(())
    }

    pub fn bind_uv(&self, name: &str) -> Result<(), Error> {
        let attr = self.rf.data.program.get_attrib_location(name)?;

        let (offset, num) = match Vtx::uv_location() {
            Some(p) => p,
            None => return Err(Error::OtherError(
                "No UV data for associated vertex".to_owned())),
        };

        unsafe {
            let off = ptr::null::<u8>().offset(
                (mem::size_of::<gl::GLfloat>() * offset as usize) as isize);
            gl::EnableVertexAttribArray(attr as gl::GLuint);
            gl::VertexAttribPointer(
                attr as gl::GLuint,
                num,
                gl::FLOAT,
                gl::FALSE as gl::GLboolean,
                Vtx::size() as i32,
                off as *const c_void,
            )
        }
        Ok(())
    }

}

#[allow(dead_code)]
pub struct Texture {
    idx: u32,
    image: Image<u8>,
}

impl Texture {

    /// Take a raw Bitmap value and create an OpenGL texture from it
    pub fn new_from_bitmap(image: Image<u8>) -> Texture {
        let mut idx = 0;
        unsafe {
            gl::GenTextures(1, &mut idx);
            gl::BindTexture(gl::TEXTURE_2D, idx);

            let params = [
                (gl::TEXTURE_MIN_FILTER, gl::NEAREST as gl::GLint),
                (gl::TEXTURE_MAG_FILTER, gl::NEAREST as gl::GLint),
                (gl::TEXTURE_WRAP_S, gl::CLAMP_TO_EDGE as gl::GLint),
                (gl::TEXTURE_WRAP_T, gl::CLAMP_TO_EDGE as gl::GLint),
            ];

            for &(param, value) in params.into_iter() {
                gl::TexParameteri(gl::TEXTURE_2D, param, value)
            }

            let fmt = match image.fmt {
                ColFmt::RGB => gl::RGB,
                ColFmt::RGBA => gl::RGBA,
                _ => panic!("Unknown format: {:?}", image.fmt),
            };

            gl::TexImage2D(
                gl::TEXTURE_2D,
                0,
                fmt as gl::GLint,
                image.w as gl::GLint,
                image.h as gl::GLint,
                0,
                fmt,
                gl::UNSIGNED_BYTE,
                mem::transmute(&image.buf[0]),
            );

            gl::BindTexture(gl::TEXTURE_2D, 0);
        }
        Texture { idx, image }
    }

    pub fn new_from_png(p: &str) -> Result<Texture, Error> {
        use imagefmt;
        let mut f = ::std::fs::File::open(p)?;
        let bitmap = imagefmt::png::read(&mut f, imagefmt::ColFmt::RGBA)?;
        Ok(Texture::new_from_bitmap(bitmap))
    }

}
