use super::errors::*;

use std::path::Path;
use std::fs::File;
use std::io::Read;

use raw::{ProgramId, ShaderType, create_shader, create_program, shader_source, compile_shader,
          attach_shader, link_program, detach_shader, get_shader_info_log, get_program_info_log,
          delete_shader, delete_program, use_program, get_integer_i_v, GetParameterName};

#[derive(Debug)]
pub struct ComputeProgram {
    program_id: ProgramId,
}

impl ComputeProgram {
    #[allow(unused_must_use)]
    pub fn new(sources: &[&str]) -> Result<ComputeProgram> {
        let program_id: ProgramId;
        unsafe {
            let shader_id = create_shader(ShaderType::Compute);

            shader_source(shader_id, sources);
            compile_shader(shader_id);

            let shader_info_log = get_shader_info_log(shader_id);

            match shader_info_log {
                Ok(log) => {
                    if log.len() > 1 {
                        delete_shader(shader_id);
                        bail!(log);
                    }
                }
                Err(_) => {
                    delete_shader(shader_id);
                    shader_info_log.chain_err(
                        || "Could not get shader compilation log",
                    )?;
                }
            }

            program_id = create_program();
            let attach_status = attach_shader(program_id, shader_id);
            if attach_status.is_err() {
                delete_program(program_id);
                delete_shader(shader_id);
                attach_status.chain_err(|| "Could not attach shader")?;
            }

            let link_status = link_program(program_id);
            if link_status.is_err() {
                let program_info_log = get_program_info_log(program_id);
                match program_info_log {
                    Ok(log) => {
                        if log.len() > 1 {
                            delete_program(program_id);
                            delete_shader(shader_id);
                            bail!(log);
                        }
                    }
                    Err(_) => {
                        delete_program(program_id);
                        delete_shader(shader_id);
                        program_info_log.chain_err(
                            || "Could not get program link log",
                        )?;
                    }
                }
            }

            // ignoring error, because according to spec it cannot fail
            // because program_id is program object, shader_id is shader object
            //         shader_id is attached to program_id
            detach_shader(program_id, shader_id);

            delete_shader(shader_id);
        }
        Ok(ComputeProgram { program_id: program_id })
    }

    pub fn from_file(filepath: &Path) -> Result<ComputeProgram> {
        let mut file = match File::open(&filepath) {
            Ok(file) => file,
            Err(e) => return Err(format!("Could not open shader file : {}", e).into()),
        };

        let mut source = String::new();
        match file.read_to_string(&mut source) {
            Ok(_) => {}
            Err(e) => return Err(format!("Could not read shader file! : {}", e).into()),
        };

        ComputeProgram::new(&[source.as_str()])
    }

    pub fn bind(&self) -> Result<()> {
        unsafe { use_program(self.program_id).chain_err(|| "Could not bind compute program") }
    }

    pub fn max_group_count() -> (u32, u32, u32) {
        unsafe {
            let x = get_integer_i_v(GetParameterName::MaxComputeWorkGroupCount, 0).unwrap() as u32;
            let y = get_integer_i_v(GetParameterName::MaxComputeWorkGroupCount, 1).unwrap() as u32;
            let z = get_integer_i_v(GetParameterName::MaxComputeWorkGroupCount, 2).unwrap() as u32;

            (x, y, z)
        }
    }

    pub fn max_group_size() -> (u32, u32, u32) {
        unsafe {
            let x = get_integer_i_v(GetParameterName::MaxComputeWorkGroupSize, 0).unwrap() as u32;
            let y = get_integer_i_v(GetParameterName::MaxComputeWorkGroupSize, 1).unwrap() as u32;
            let z = get_integer_i_v(GetParameterName::MaxComputeWorkGroupSize, 2).unwrap() as u32;

            (x, y, z)
        }
    }

    pub fn max_group_invocations() -> u32 {
        unsafe {
            get_integer_i_v(GetParameterName::MaxComputeWorkGroupInvocations, 0).unwrap() as u32
        }
    }
}

impl Drop for ComputeProgram {
    fn drop(&mut self) {
        unsafe {
            delete_program(self.program_id);
        }
    }
}
