use std::{ mem, fmt };
use std::error::Error;

use crate::version::Api;
use crate::version::Version;
use crate::context::CommandContext;
use crate::backend::Facade;
use crate::BufferExt;
use crate::GlObject;
use crate::ContextExt;
use crate::CapabilitiesSource;
use crate::TransformFeedbackSessionExt;
use crate::buffer::{Buffer, BufferAnySlice};
use crate::index::PrimitiveType;
use crate::program::OutputPrimitives;
use crate::program::Program;
use crate::vertex::Vertex;

use crate::gl;

/// Transform feedback allows you to obtain in a buffer the list of the vertices generated by
/// the vertex shader, geometry shader, or tessellation evaluation shader of your program. This
/// is usually used to cache the result in order to draw the vertices multiple times with multiple
/// different fragment shaders.
///
/// To use transform feedback, you must create a transform feedback session. A transform feedback
/// session mutably borrows the buffer where the data will be written. Each draw command submitted
/// with a session will continue to append data after the data written by the previous draw command.
/// You can only use the data when the session is destroyed.
///
/// # Notes
///
/// Here are a few things to note if you aren't familiar with transform feedback:
///
/// - The program you use must have transform feedback enabled, either with attributes in the
///   vertex shader's source code (for recent OpenGL versions only) or by indicating a list of
///   vertex attributes when building the program.
///
/// - A transform feedback session is bound to a specific program and buffer. You can't switch
///   them once the session has been created. An error is generated if you draw with a different
///   program than the one you created the session with.
///
/// - The transform feedback process doesn't necessarily fill the whole buffer. To retrieve the
///   number of vertices that are written to the buffer, use a query object (see the
///   `draw_parameters` module). It is however usually easy to determine in advance the number of
///   vertices that will be written based on the input data.
///
/// - The buffer will obtain either a list of points, a list of lines (two vertices), or a list of
///   triangles (three vertices). If you draw a triangle strip or a triangle fan for example,
///   individual triangles will be written to the buffer (meaning that some vertices will be
///   duplicated).
///
/// - You can use the same session multiple times in a row, in which case the data will continue
///   to be pushed in the buffer after the existing data. However you must always use the same type
///   of primitives and the same program.
///
/// # Example
///
/// ```no_run
/// # use glium::{implement_vertex, uniform};
/// # use glium::Surface;
/// # fn example(display: glium::Display, program: glium::Program,
/// #            vb: glium::vertex::VertexBufferAny, ib: glium::index::IndexBuffer<u16>) {
/// #[derive(Copy, Clone, Debug, PartialEq)]
/// struct Vertex {
///     output_val: (f32, f32),
/// }
///
/// implement_vertex!(Vertex, output_val);
///
/// let mut out_buffer: glium::VertexBuffer<Vertex> = glium::VertexBuffer::empty(&display, 6).unwrap();
///
/// {
///     let session = glium::vertex::TransformFeedbackSession::new(&display, &program,
///                                                                &mut out_buffer).unwrap();
///
///     let params = glium::DrawParameters {
///         transform_feedback: Some(&session),
///         .. Default::default()
///     };
///
///     display.draw().draw(&vb, &ib, &program, &uniform!{}, &params).unwrap();
/// }
///
/// let result: Vec<Vertex> = out_buffer.read().unwrap();
/// println!("List of generated vertices: {:?}", result);
/// # }
/// ```
#[derive(Debug)]
pub struct TransformFeedbackSession<'a> {
    buffer: BufferAnySlice<'a>,
    program: &'a Program,
}

/// Error that can happen when creating a `TransformFeedbackSession`.
#[derive(Debug, Clone)]
pub enum TransformFeedbackSessionCreationError {
    /// Transform feedback is not supported by the OpenGL implementation.
    NotSupported,

    /// The format of the output doesn't match what the program is expected to output.
    WrongVertexFormat,
}

impl fmt::Display for TransformFeedbackSessionCreationError {
    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
        use self::TransformFeedbackSessionCreationError::*;
        let desc = match *self {
            NotSupported =>
                "Transform feedback is not supported by the OpenGL implementation",
            WrongVertexFormat =>
                "The format of the output doesn't match what the program is expected to output",
        };
        fmt.write_str(desc)
    }
}

impl Error for TransformFeedbackSessionCreationError {}

/// Returns true if transform feedback is supported by the OpenGL implementation.
#[inline]
pub fn is_transform_feedback_supported<F: ?Sized>(facade: &F) -> bool where F: Facade {
    let context = facade.get_context();

    context.get_version() >= &Version(Api::Gl, 3, 0) ||
    context.get_version() >= &Version(Api::GlEs, 3, 0) ||
    context.get_extensions().gl_ext_transform_feedback
}

impl<'a> TransformFeedbackSession<'a> {
    /// Builds a new transform feedback session.
    ///
    /// TODO: this constructor should ultimately support passing multiple buffers of different
    ///       types
    pub fn new<F: ?Sized, V>(facade: &F, program: &'a Program, buffer: &'a mut Buffer<[V]>)
                     -> Result<TransformFeedbackSession<'a>, TransformFeedbackSessionCreationError>
                     where F: Facade, V: Vertex + Copy + Send + 'static
    {
        if !is_transform_feedback_supported(facade) {
            return Err(TransformFeedbackSessionCreationError::NotSupported);
        }

        if !program.transform_feedback_matches(&<V as Vertex>::build_bindings(),
                                               mem::size_of::<V>())
        {
            return Err(TransformFeedbackSessionCreationError::WrongVertexFormat);
        }

        Ok(TransformFeedbackSession {
            buffer: buffer.as_slice_any(),
            program,
        })
    }
}

impl<'a> TransformFeedbackSessionExt for TransformFeedbackSession<'a> {
    fn bind(&self, ctxt: &mut CommandContext<'_>, draw_primitives: PrimitiveType) {
        // TODO: check that the state matches what is required
        if ctxt.state.transform_feedback_enabled.is_some() {
            unimplemented!();
        }

        // FIXME: use the memory barrier system
        self.buffer.bind_to_transform_feedback(ctxt, 0);

        unsafe {
            let primitives = match (self.program.get_output_primitives(), draw_primitives) {
                (Some(OutputPrimitives::Points), _) => gl::POINTS,
                (Some(OutputPrimitives::Lines), _) => gl::LINES,
                (Some(OutputPrimitives::Triangles), _) => gl::TRIANGLES,
                (Some(OutputPrimitives::Quads), _) => panic!(),         // TODO: return a proper error
                (None, PrimitiveType::Points) => gl::POINTS,
                (None, PrimitiveType::LinesList) => gl::LINES,
                (None, PrimitiveType::LinesListAdjacency) => gl::LINES,
                (None, PrimitiveType::LineStrip) => gl::LINES,
                (None, PrimitiveType::LineStripAdjacency) => gl::LINES,
                (None, PrimitiveType::LineLoop) => gl::LINES,
                (None, PrimitiveType::TrianglesList) => gl::TRIANGLES,
                (None, PrimitiveType::TrianglesListAdjacency) => gl::TRIANGLES,
                (None, PrimitiveType::TriangleStrip) => gl::TRIANGLES,
                (None, PrimitiveType::TriangleStripAdjacency) => gl::TRIANGLES,
                (None, PrimitiveType::TriangleFan) => gl::TRIANGLES,
                (None, PrimitiveType::Patches { .. }) => unreachable!(),
            };

            ctxt.gl.BeginTransformFeedback(primitives);
            ctxt.state.transform_feedback_enabled = Some(primitives);
            ctxt.state.transform_feedback_paused = false;
        }
    }

    #[inline]
    fn unbind(ctxt: &mut CommandContext<'_>) {
        if ctxt.state.transform_feedback_enabled.is_none() {
            return;
        }

        unsafe {
            ctxt.gl.EndTransformFeedback();
            ctxt.state.transform_feedback_enabled = None;
            ctxt.state.transform_feedback_paused = false;
        }
    }

    fn ensure_buffer_out_of_transform_feedback(ctxt: &mut CommandContext<'_>, buffer: gl::types::GLuint) {
        if ctxt.state.transform_feedback_enabled.is_none() {
            return;
        }

        let mut needs_unbind = false;
        for elem in ctxt.state.indexed_transform_feedback_buffer_bindings.iter_mut() {
            if elem.buffer == buffer {
                needs_unbind = true;
                break;
            }
        }

        if needs_unbind {
            TransformFeedbackSession::unbind(ctxt);
        }
    }
}

impl<'a> Drop for TransformFeedbackSession<'a> {
    #[inline]
    fn drop(&mut self) {
        // Since the session can be mem::forget'ed, the code in buffer/alloc.rs ensures that the
        // buffer isn't used by transform feedback.
        // However we end the session now anyway.
        let mut ctxt = self.buffer.get_context().make_current();
        Self::ensure_buffer_out_of_transform_feedback(&mut ctxt, self.buffer.get_id());
    }
}
