class SimpleImageShaderWidget(GLCanvas):
    """
    An image shader is a GLSL program that does all of its computation in
    the fragment shader.

    This widget displays an image generated by an image
    shader. The draw method simply draws a triangle covering the
    entire screen, which causes the fragment shader to be run on
    every pixel in the window. The fragment shader source and -
    optionally - sizes and offsets for uniform buffer blocks can
    be set with set_fragment_shader_source.

    The uniform vec2 viewportSize contains the view port size.
    """

    profile = '3_2'

    vertex_shader_source = b"""
    #version 150

    // Note that GLSL ES 3.0 is based on GLSL 3.30 and used for WebGL 2.0.
    // GLSL 1.50 came with OpenGL 3.2.

    in vec4 position;

    void main()
    {
        gl_Position = position;  // no-op
    }

    """

    fragment_shader_source = b"""
    #version 150

    out vec4 out_FragColor;

    void main()
    {
        out_FragColor = vec4(1.0, 0.0, 0.0, 1.0);
    }
    """

    report_gl_errors = False

    def __init__(self, parent, **kw):
        super().__init__(parent, **kw)

        self._vertex_buffer = VertexBuffer()
        self._vertex_buffer.load(((3,-1), (-1,3), (-1,-1)))

        self.image_shader = GLSLProgram(
            self.vertex_shader_source,
            self.fragment_shader_source,
            name = "fallback image shader")
        self.textures = []
        self.report_time_callback = None

        self.make_current()

        cdef char * missing_gl_function
        missing_gl_function = checkGlewForModernOpenGL()
        if missing_gl_function:
            raise Exception(
                ("Missing gl function: %s. Your graphics card probably does "
                 "not support the required OpenGL version (3.2 or later). Your "
                 "OpenGL version is %s.") % (missing_gl_function,
                                             get_gl_string('GL_VERSION')))

    def set_textures(self, texture_files):
        self.make_current()

        for texture in self.textures:
            texture.delete_resource()

        self.textures = []
        for texture_file in texture_files:
            texture = None
            try:
                texture = ImageBasedTexture(texture_file)
            except Exception as e:
                print("Warning could not read texture %s" % texture_file)
                print(e)

            self.textures.append(texture)

    def set_fragment_shader_source(self,
                                   source,
                                   uniform_block_names_sizes_and_offsets = []):
        self.image_shader.delete_resource()
        self.image_shader = GLSLProgram(
            self.vertex_shader_source, 
            source,
            uniform_block_names_sizes_and_offsets = uniform_block_names_sizes_and_offsets,
            name = "image shader")
        
    def render_to_array(self, width, height, as_float = False):
        """
        Renders the image into an off-screen framebuffer
        of given width and height and returns the result as an array.

        The array either holds unsigned byte or float RGB.
        """
    
        cdef GLuint fbo
        cdef GLenum color_texture_type
        cdef GLuint color_texture
        cdef GLuint depth_texture
        cdef array_type
        cdef array.array c_array

        self.make_current()

        if as_float:
            array_type = 'f'
            color_texture_type = GL_FLOAT
        else:
            array_type = 'B'
            color_texture_type = GL_UNSIGNED_BYTE

        # Create texture for color attachment
        glGenTextures(1, &color_texture)
        glBindTexture(GL_TEXTURE_2D, color_texture)
        glTexImage2D(GL_TEXTURE_2D, 0,
                     GL_RGB, width, height,
                     0,
                     GL_RGB,
                     color_texture_type,
                     NULL)
        glTexParameteri(GL_TEXTURE_2D,
                        GL_TEXTURE_MIN_FILTER,
                        GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D,
                        GL_TEXTURE_MAG_FILTER,
                        GL_LINEAR)

        # Create texture for depth attachment
        glGenTextures(1, &depth_texture)
        glBindTexture(GL_TEXTURE_2D, depth_texture)
        glTexImage2D(GL_TEXTURE_2D, 0,
                     GL_DEPTH_COMPONENT, width, height,
                     0,
                     GL_DEPTH_COMPONENT,
                     GL_FLOAT,
                     NULL)
        glTexParameteri(GL_TEXTURE_2D,
                        GL_TEXTURE_MIN_FILTER,
                        GL_LINEAR)
        glTexParameteri(GL_TEXTURE_2D,
                        GL_TEXTURE_MAG_FILTER,
                        GL_LINEAR)

        # Create framebuffer
        glGenFramebuffers(1, &fbo)
        glBindFramebuffer(GL_FRAMEBUFFER, fbo)
        glFramebufferTexture2D(GL_FRAMEBUFFER,
                               GL_COLOR_ATTACHMENT0,
                               GL_TEXTURE_2D,
                               color_texture, 0)
        glFramebufferTexture2D(GL_FRAMEBUFFER,
                               GL_DEPTH_ATTACHMENT,
                               GL_TEXTURE_2D,
                               depth_texture, 0)
        if glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE:
            raise Exception("Incomplete framebuffer")

        # Render into the framebuffer
        self.draw_impl(width, height)
        glFinish()

        # Allocate memory and read framebuffer into it
        c_array = array.array(array_type)
        array.resize(c_array, 3 * width * height)
        glPixelStorei(GL_PACK_ALIGNMENT, 1)
        glReadPixels(0, 0, width, height,
                     GL_RGB,
                     color_texture_type,
                     c_array.data.as_voidptr)

        # Unbind framebuffer so that stuff is rendered
        # to screen again.
        glBindFramebuffer(GL_FRAMEBUFFER, 0)
        
        glDeleteFramebuffers(1, &fbo)
        glDeleteTextures(1, &color_texture)
        glDeleteTextures(1, &depth_texture)

        print_gl_errors("Render to off-screen area")

        return c_array

    def save_image(self, width, height, outfile):
        """
        Writes image of given width and height
        as png to given outfile (file object returned by,
        .e.g, open("myFile.png", "wb")).
        """

        data = self.render_to_array(width, height)
        
        # Png writer expects row - we also need to
        # flip the image vertically.
        stride = 3 * width
        rows = [ data[i * stride : (i+1) * stride]
                 for i in range(height - 1, -1, -1) ]

        writer = png.Writer(
            width, height,
            greyscale = False,
            bitdepth = 8,
            alpha = False)
        writer.write(outfile, rows)

    def read_depth_value(self, x, y):

        cdef GLfloat depth

        width = self.winfo_width()
        height = self.winfo_height()

        self.make_current()

        self.draw_impl(width, height,
                       include_depth_value = True)
        glFinish()            

        glReadPixels(x, height - y, 1, 1,
                     GL_DEPTH_COMPONENT,
                     GL_FLOAT, &depth)

        return (depth, width, height)

    def draw_impl(self, width, height,
                  include_depth_value = False):

        if self.report_time_callback:
            start_time = time.time()

        glViewport(0, 0, width, height)
        if include_depth_value:
            # Writes to z-buffer are only done when GL_DEPTH_TEST
            # is enabled
            glClear(GL_DEPTH_BUFFER_BIT)
            glEnable(GL_DEPTH_TEST)
        else:
            glDisable(GL_DEPTH_TEST)
        glDisable(GL_BLEND)
        glDisable(GL_CULL_FACE)

        if self.image_shader.is_valid():
            for i, texture in enumerate(self.textures):
                if texture:
                    glActiveTexture(GL_TEXTURE0 + i)
                    texture.bind()

            self.image_shader.use_program()
            self.image_shader.bind_uniforms(
                self.get_uniform_bindings(width, height))
            self._vertex_buffer.bind()
            
            glDrawArrays(GL_TRIANGLES, 0, 3)

            for i, texture in enumerate(self.textures):
                if texture:
                    glActiveTexture(GL_TEXTURE0 + i)
                    texture.unbind()
            glActiveTexture(GL_TEXTURE0)

        if self.report_time_callback:
            glFinish()
            self.report_time_callback(time.time() - start_time)

    def draw(self):
        width = self.winfo_width()
        height = self.winfo_height()
        if width <= 1 or height <= 1:
            return

        self.make_current()
        self.draw_impl(width, height)
        self.swap_buffers()

        if self.report_gl_errors:
            print_gl_errors("draw")

    def get_uniform_bindings(self, view_width, view_height):
        return {
            'viewportSize' : ('vec2', (view_width, view_height))
            }
