use super::framebuffer::*;
use super::texture::*;
use super::pipeline::*;
use super::buffer::*;
use super::raw::*;
use vertex_array_object::*;
use compute_program::ComputeProgram;

use std;
use glutin;

mod errors {
    error_chain! {
        errors {
        }
    }
}

use self::errors::*;

#[derive(Copy, Clone)]
pub struct BufferObjectTargetLocation<'a> {
    pub target: BufferBaseTarget,
    pub index: BlockIndex,
    pub buffer: &'a Buffer,
}

impl<'a> std::fmt::Debug for BufferObjectTargetLocation<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        write!(
            f,
            "BufferObjectTargetLocation {{ target: {:?}, index: {:?}, buffer id: {:?} }}",
            self.target,
            self.index,
            self.buffer.get_id()
        )
    }
}

pub struct Renderer<'a, C: glutin::GlContext + 'a> {
    context: &'a C,
    state: FramebufferState,
    vertex_array_object: VertexArrayObjectId,
    default_framebuffer: DefaultFramebuffer,
}

impl<'a, C: glutin::GlContext> Renderer<'a, C> {
    pub fn new(context: &'a C) -> Result<Renderer<'a, C>> {
        unsafe {
            context.make_current().chain_err(
                || "Unable to make Renderer context current",
            )?;
        }

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

        // Create empty VAO to satisfy OpenGL Core requirement
        let vao;
        unsafe {
            vao = create_vertex_array();
            bind_vertex_array(vao);
        }

        Ok(Renderer {
            context: context,
            state: FramebufferState::default(),
            vertex_array_object: vao,
            default_framebuffer: DefaultFramebuffer::new(),
        })
    }

    pub fn bind_default_framebuffer(&mut self) {
        self.default_framebuffer.bind();
        self.state.sync(&self.default_framebuffer.state());
    }

    pub fn bind_framebuffer(&mut self, framebuffer: &GeneralFramebuffer) {
        framebuffer.bind();
        self.state.sync(&framebuffer.state());
    }

    pub fn clear_default_framebuffer(&mut self, buffers: ClearBuffers) -> () {
        self.bind_default_framebuffer();

        unsafe {
            clear(buffers);
        }
    }

    pub fn clear_framebuffer(
        &mut self,
        framebuffer: &GeneralFramebuffer,
        buffers: ClearBuffers,
    ) -> () {
        self.bind_framebuffer(framebuffer);

        unsafe {
            clear(buffers);
        }
    }

    pub fn multi_draw_arrays_indirect(
        &mut self,
        framebuffer: &FramebufferRenderTarget,
        buffers: &[BufferObjectTargetLocation],
        textures: &[&Texture],
        pipeline: &Pipeline,
        draw_commands_buffer: (&Buffer, usize),
        clear_buffers: ClearBuffers,
    ) -> Result<()> {
        match *framebuffer {
            FramebufferRenderTarget::Default => self.bind_default_framebuffer(),
            FramebufferRenderTarget::General(framebuffer, attachments) => {
                self.bind_framebuffer(framebuffer);
                framebuffer.attach_textures(attachments).chain_err(
                    || "Could not attach textures",
                )?;
            }
        }

        if let &FramebufferRenderTarget::General(fbo, attachments) = framebuffer {
            fbo.attach_textures(attachments).chain_err(
                || "Could not attach textures",
            )?;
        };


        let res_textures: Vec<ResidentTexture> = textures
            .iter()
            .map(|t| t.make_resident_texture())
            .collect::<_>();

        unsafe {
            if clear_buffers != ClearBuffers::None {
                clear(clear_buffers);
            }
        }

        pipeline.bind();

        // TODO refactor into 4 bind buffers
        for buffer_target in buffers {
            unsafe {
                bind_buffer_base(
                    buffer_target.target,
                    buffer_target.index,
                    buffer_target.buffer.get_id(),
                ).chain_err(|| format!("Unable to bind buffer {:?}", buffer_target))?;
            }
        }

        unsafe {
            bind_buffer(
                BindBufferTarget::DrawIndirectBuffer,
                draw_commands_buffer.0.get_id(),
            ).chain_err(|| "Could not bind draw indirect buffer")?;
            multi_draw_arrays_indirect(draw_commands_buffer.1)
                .chain_err(|| "Could not draw")?;
        }

        Ok(())
    }

    ///
    /// # Arguments
    ///
    /// * `framebuffer`
    /// * `buffers`
    /// * `textures`
    /// * `pipeline`
    /// * `draw commands buffer`
    /// * `clear buffers`
    ///
    /// # Example
    ///
    pub fn multi_draw_elements_indirect(
        &mut self,
        framebuffer: &FramebufferRenderTarget,
        buffers: &[BufferObjectTargetLocation],
        textures: &[&Texture],
        pipeline: &Pipeline,
        draw_commands_buffer: (&Buffer, usize),
        clear_buffers: ClearBuffers,
    ) -> Result<()> {
        // Bind framebuffer (which also syncs its state) and attach textures
        // in case of GeneralFramebuffer
        match *framebuffer {
            FramebufferRenderTarget::Default => self.bind_default_framebuffer(),
            FramebufferRenderTarget::General(framebuffer, attachments) => {
                self.bind_framebuffer(framebuffer);
                framebuffer.attach_textures(attachments).chain_err(
                    || "Could not attach textures",
                )?;
            }
        }

        // Make needed textures resident
        let res_textures: Vec<ResidentTexture> = textures
            .iter()
            .map(|t| t.make_resident_texture())
            .collect::<_>();

        // Clear the requested buffers
        unsafe {
            if clear_buffers != ClearBuffers::None {
                clear(clear_buffers);
            }
        }

        // Bind the pipeline
        pipeline.bind();

        // Bind buffers
        // TODO refactor into 4 bind buffers
        for buffer_target in buffers {
            unsafe {
                bind_buffer_base(
                    buffer_target.target,
                    buffer_target.index,
                    buffer_target.buffer.get_id(),
                ).chain_err(|| format!("Unable to bind buffer {:?}", buffer_target))?;
            }
        }

        // Bind draw command buffer
        // Draw
        unsafe {
            bind_buffer(
                BindBufferTarget::DrawIndirectBuffer,
                draw_commands_buffer.0.get_id(),
            ).chain_err(|| "Could not bind draw indirect buffer")?;

            multi_draw_elements_indirect(draw_commands_buffer.1)
                .chain_err(|| "Could not draw")?;
        }

        Ok(())
    }

    pub fn draw_arrays(
        &mut self,
        framebuffer: FramebufferRenderTarget,
        buffers: &[BufferObjectTargetLocation],
        textures: &[&Texture],
        pipeline: &Pipeline,
        count: usize,
        clear_buffers: ClearBuffers,
    ) -> Result<()> {
        // Bind framebuffer (which also syncs its state) and attach textures
        // in case of GeneralFramebuffer
        match framebuffer {
            FramebufferRenderTarget::Default => self.bind_default_framebuffer(),
            FramebufferRenderTarget::General(framebuffer, attachments) => {
                self.bind_framebuffer(framebuffer);
                framebuffer.attach_textures(attachments).chain_err(
                    || "Could not attach textures",
                )?;
            }
        }

        // Make needed textures resident
        let res_textures: Vec<ResidentTexture> = textures
            .iter()
            .map(|t| t.make_resident_texture())
            .collect::<_>();

        // Clear the requested buffers
        unsafe {
            if clear_buffers != ClearBuffers::None {
                clear(clear_buffers);
            }
        }

        // Bind the pipeline
        pipeline.bind();

        // Bind buffers
        // TODO refactor into 4 bind buffers
        for buffer_target in buffers {
            unsafe {
                bind_buffer_base(
                    buffer_target.target,
                    buffer_target.index,
                    buffer_target.buffer.get_id(),
                ).chain_err(|| format!("Unable to bind buffer {:?}", buffer_target))?;
            }
        }

        // Bind draw command buffer
        // Draw
        unsafe {
            draw_arrays(count).chain_err(|| "Could not draw")?;
        }

        Ok(())
    }

    pub fn dispatch_compute(
        &mut self,
        buffers: &[BufferObjectTargetLocation],
        textures: &[&Texture],
        program: &ComputeProgram,
        command: DispatchCommand,
    ) -> Result<()> {
        let res_textures: Vec<ResidentTexture> = textures
            .iter()
            .map(|t| t.make_resident_texture())
            .collect::<_>();

        program.bind().chain_err(|| "Unable to bind program")?;

        for buffer_target in buffers {
            unsafe {
                bind_buffer_base(
                    buffer_target.target,
                    buffer_target.index,
                    buffer_target.buffer.get_id(),
                ).chain_err(|| "Unable to bind buffer")?;
            }
        }

        unsafe {
            dispatch_compute(
                command.num_groups_x,
                command.num_groups_y,
                command.num_groups_z,
            ).chain_err(|| "Error during command dispatch")?;
        }

        Ok(())
    }

    pub fn dispatch_compute_indirect<T: Buffer>(
        &mut self,
        buffers: &[BufferObjectTargetLocation],
        textures: &[&Texture],
        program: &ComputeProgram,
        command_buffer: &T,
        command_offset: usize,
    ) -> Result<()> {
        let res_textures: Vec<ResidentTexture> = textures
            .iter()
            .map(|t| t.make_resident_texture())
            .collect::<_>();

        program.bind().chain_err(|| "Unable to bind program")?;

        for buffer_target in buffers {
            unsafe {
                bind_buffer_base(
                    buffer_target.target,
                    buffer_target.index,
                    buffer_target.buffer.get_id(),
                ).chain_err(|| "Unable to bind buffer")?;
            }
        }

        unsafe {
            bind_buffer(
                BindBufferTarget::DispatchIndirectBuffer,
                command_buffer.get_id(),
            ).chain_err(|| "Could not bind dispatch indirect buffer")?;
            dispatch_compute_indirect(command_offset).chain_err(
                || "Error during command dispatch",
            )?;
        }

        Ok(())
    }

    pub fn default_framebuffer(&self) -> &DefaultFramebuffer {
        &self.default_framebuffer
    }

    pub fn mut_default_framebuffer(&mut self) -> &mut DefaultFramebuffer {
        &mut self.default_framebuffer
    }

    pub fn make_current(&mut self) -> Result<()> {
        unsafe {
            self.context.make_current().chain_err(
                || "Unable to make Renderer context current",
            )?;
        }

        Ok(())
    }

    pub fn swap_buffers(&mut self) -> Result<()> {
        self.context.swap_buffers().chain_err(
            || "Unable to swap buffers",
        )?;

        Ok(())
    }
}


impl<'a, C: glutin::GlContext> Drop for Renderer<'a, C> {
    fn drop(&mut self) {
        unsafe {
            delete_vertex_array(self.vertex_array_object);
        }
    }
}
