/* This file is part of the Pangolin Project.
 * http://github.com/stevenlovegrove/Pangolin
 *
 * Copyright (c) 2011 Steven Lovegrove
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#pragma once

#include <pangolin/gl/gl.h>
#include <pangolin/gl/glpixformat.h>
#include <pangolin/image/image_io.h>
#include <pangolin/utils/type_convert.h>
#include <algorithm>
#include <stdexcept>
#include <assert.h>

namespace pangolin
{

////////////////////////////////////////////////
// Implementation of gl.h
////////////////////////////////////////////////

#ifndef HAVE_GLES
const int MAX_ATTACHMENTS = 8;
const static GLuint attachment_buffers[] = {
    GL_COLOR_ATTACHMENT0_EXT,
    GL_COLOR_ATTACHMENT1_EXT,
    GL_COLOR_ATTACHMENT2_EXT,
    GL_COLOR_ATTACHMENT3_EXT,
    GL_COLOR_ATTACHMENT4_EXT,
    GL_COLOR_ATTACHMENT5_EXT,
    GL_COLOR_ATTACHMENT6_EXT,
    GL_COLOR_ATTACHMENT7_EXT
};
#else // HAVE_GLES
const int MAX_ATTACHMENTS = 1;
const static GLuint attachment_buffers[] = {
    GL_COLOR_ATTACHMENT0_EXT
};
#endif // HAVE_GLES

const static size_t datatype_bytes[] = {
    1, //  #define GL_BYTE 0x1400
    1, //  #define GL_UNSIGNED_BYTE 0x1401
    2, //  #define GL_SHORT 0x1402
    2, //  #define GL_UNSIGNED_SHORT 0x1403
    4, //  #define GL_INT 0x1404
    4, //  #define GL_UNSIGNED_INT 0x1405
    4, //  #define GL_FLOAT 0x1406
    2, //  #define GL_2_BYTES 0x1407
    3, //  #define GL_3_BYTES 0x1408
    4, //  #define GL_4_BYTES 0x1409
    8  //  #define GL_DOUBLE 0x140A
};

const static size_t format_channels[] = {
    1, //  #define GL_RED 0x1903
    1, //  #define GL_GREEN 0x1904
    1, //  #define GL_BLUE 0x1905
    1, //  #define GL_ALPHA 0x1906
    3, //  #define GL_RGB 0x1907
    4, //  #define GL_RGBA 0x1908
    1, //  #define GL_LUMINANCE 0x1909
    2  //  #define GL_LUMINANCE_ALPHA 0x190A
};

inline size_t GlDataTypeBytes(GLenum type)
{
    return datatype_bytes[type - GL_BYTE];
}

inline size_t GlFormatChannels(GLenum data_layout)
{
    if (data_layout == GL_BGR) return 3;
    if (data_layout == GL_BGRA) return 4;
    return format_channels[data_layout - GL_RED];
}

//template<typename T>
//struct GlDataTypeTrait {};
//template<> struct GlDataTypeTrait<float>{ static const GLenum type = GL_FLOAT; };
//template<> struct GlDataTypeTrait<int>{ static const GLenum type = GL_INT; };
//template<> struct GlDataTypeTrait<unsigned char>{ static const GLenum type = GL_UNSIGNED_BYTE; };

inline GlTexture::GlTexture()
    : internal_format(0), tid(0), width(0), height(0)
{
    // Not a texture constructor
}

inline GlTexture::GlTexture(GLint width, GLint height, GLint internal_format, bool sampling_linear, int border, GLenum glformat, GLenum gltype, GLvoid* data )
    : internal_format(0), tid(0)
{
    Reinitialise(width,height,internal_format,sampling_linear,border,glformat,gltype,data);
}

inline GlTexture::GlTexture(const TypedImage& img, bool sampling_linear)
{
    this->Load(img, sampling_linear);
}

inline GlTexture::GlTexture(GlTexture&& tex)
{
    *this = std::move(tex);
}

inline GlTexture& GlTexture::operator=(GlTexture&& tex)
{
    if (&tex != this) {
        internal_format = tex.internal_format;
        tid = tex.tid;
        width = tex.width;
        height = tex.height;
        
        tex.internal_format = 0;
        tex.tid = 0;
    }
    return *this;
}

inline bool GlTexture::IsValid() const
{
    return tid != 0;
}

inline void GlTexture::Delete()
{
    if(internal_format!=0 ) {
        glDeleteTextures(1,&tid);
        internal_format = 0;
        tid = 0;
        width = 0;
        height = 0;
    }
}

inline GlTexture::~GlTexture()
{
    Delete();
}

inline void GlTexture::Bind() const
{
    glBindTexture(GL_TEXTURE_2D, tid);
}

inline void GlTexture::Unbind() const
{
    glBindTexture(GL_TEXTURE_2D, 0);
}

inline void GlTexture::Reinitialise(GLsizei w, GLsizei h, GLint int_format, bool sampling_linear, int border, GLenum glformat, GLenum gltype, GLvoid* data )
{
    if(tid!=0) {
        glDeleteTextures(1,&tid);
    }

    internal_format = int_format;
    width = w;
    height = h;

    glGenTextures(1,&tid);
    Bind();

    // GL_LUMINANCE and GL_FLOAT don't seem to actually affect buffer, but some values are required
    // for call to succeed.
    glTexImage2D(GL_TEXTURE_2D, 0, internal_format, width, height, border, glformat, gltype, data);

    if(sampling_linear) {
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    }else{
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
        glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    }

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);

    CheckGlDieOnError();
}

inline void GlTexture::Upload(
    const void* data,
    GLenum data_format, GLenum data_type
) {
    Bind();
    glTexSubImage2D(GL_TEXTURE_2D,0,0,0,width,height,data_format,data_type,data);
    CheckGlDieOnError();
}

inline void GlTexture::Upload(
    const void* data,
    GLsizei tex_x_offset, GLsizei tex_y_offset,
    GLsizei data_w, GLsizei data_h,
    GLenum data_format, GLenum data_type )
{
    Bind();
    glTexSubImage2D(GL_TEXTURE_2D,0,tex_x_offset,tex_y_offset,data_w,data_h,data_format,data_type,data);
    CheckGlDieOnError();
}

inline void GlTexture::Load(const TypedImage& image, bool sampling_linear)
{
    GlPixFormat fmt(image.fmt);
    Reinitialise((GLint)image.w, (GLint)image.h, fmt.scalable_internal_format, sampling_linear, 0, fmt.glformat, fmt.gltype, image.ptr );
}

template<typename T>
void GlTexture::Load(const Image<T>& image, bool sampling_linear)
{
    using GlFmt = GlFormatTraits<T>;

    Reinitialise(
        (GLint)image.w, (GLint)image.h,
        GlFmt::glinternalformat,
        sampling_linear, 0,
        GlFmt::glformat, GlFmt::gltype,
        image.ptr);
}

inline void GlTexture::LoadFromFile(const std::string& filename, bool sampling_linear)
{
    TypedImage image = LoadImage(filename);
    Load(image, sampling_linear);
}

inline void GlTexture::Download([[maybe_unused]] void* image,
                                [[maybe_unused]] GLenum data_layout,
                                [[maybe_unused]] GLenum data_type) const
{
    Bind();
#ifndef HAVE_GLES
    glGetTexImage(GL_TEXTURE_2D, 0, data_layout, data_type, image);
#else
    throw std::runtime_error("glGetTexImage not implemented on this platform.");
#endif // HAVE_GLES
    Unbind();
}

inline void GlTexture::Download(TypedImage& image) const
{
    switch (internal_format)
    {
    case GL_LUMINANCE8:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY8") );
        Download(image.ptr, GL_LUMINANCE, GL_UNSIGNED_BYTE);
        break;
    case GL_LUMINANCE16:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY16LE") );
        Download(image.ptr, GL_LUMINANCE, GL_UNSIGNED_SHORT);
        break;
    case GL_RGB8:
        image.Reinitialise(width, height, PixelFormatFromString("RGB24"));
        Download(image.ptr, GL_RGB, GL_UNSIGNED_BYTE);
        break;
    case GL_RGBA8:
        image.Reinitialise(width, height, PixelFormatFromString("RGBA32"));
        Download(image.ptr, GL_RGBA, GL_UNSIGNED_BYTE);
        break;
    case GL_RED_INTEGER:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY32") );
        Download(image.ptr, GL_RED, GL_UNSIGNED_INT);
        break;
    case GL_LUMINANCE:
    case GL_LUMINANCE32F_ARB:
    case GL_R32F:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY32F"));
        Download(image.ptr, GL_LUMINANCE, GL_FLOAT);
        break;
    case GL_RGB16:
        image.Reinitialise(width, height, PixelFormatFromString("RGB48"));
        Download(image.ptr, GL_RGB, GL_UNSIGNED_SHORT);
        break;
    case GL_RGBA16:
        image.Reinitialise(width, height, PixelFormatFromString("RGBA64"));
        Download(image.ptr, GL_RGBA, GL_UNSIGNED_SHORT);
        break;
    case GL_RGB16F:
        image.Reinitialise(width, height, PixelFormatFromString("RGB48F"));
        Download(image.ptr, GL_RGB, GL_HALF_FLOAT);
        break;
    case GL_RGBA16F:
        image.Reinitialise(width, height, PixelFormatFromString("RGBA64F"));
        Download(image.ptr, GL_RGBA, GL_HALF_FLOAT);
        break;
    case GL_RGB:
    case GL_RGB32F:
        image.Reinitialise(width, height, PixelFormatFromString("RGB96F"));
        Download(image.ptr, GL_RGB, GL_FLOAT);
        break;
    case GL_RGBA:
    case GL_RGBA32F:
        image.Reinitialise(width, height, PixelFormatFromString("RGBA128F"));
        Download(image.ptr, GL_RGBA, GL_FLOAT);
        break;
    case GL_DEPTH_COMPONENT16:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY16LE"));
        Download(image.ptr, GL_DEPTH_COMPONENT, GL_UNSIGNED_SHORT);
        break;
    case GL_DEPTH_COMPONENT24:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY32"));
        Download(image.ptr, GL_DEPTH_COMPONENT, GL_UNSIGNED_INT);
        break;
    case GL_DEPTH_COMPONENT32F:
        image.Reinitialise(width, height, PixelFormatFromString("GRAY32F"));
        Download(image.ptr, GL_DEPTH_COMPONENT, GL_FLOAT);
        break;
    default:
        throw std::runtime_error(
            "GlTexture::Download - Unknown internal format (" +
            pangolin::Convert<std::string,GLint>::Do(internal_format) +
            ")"
        );
    }

}

inline void GlTexture::CopyFrom([[maybe_unused]] const GlTexture& tex)
{
#ifndef HAVE_GLES
    if(!tid || width != tex.width || height != tex.height ||
       internal_format != tex.internal_format)
    {
        Reinitialise(tex.width, tex.height, tex.internal_format, true);
    }

    glCopyImageSubDataNV(tex.tid, GL_TEXTURE_2D, 0, 0, 0, 0,
                       tid, GL_TEXTURE_2D, 0, 0, 0, 0,
                       width, height, 1);
    CheckGlDieOnError();
#else
    throw std::runtime_error("glCopyImageSubDataNV not implemented on this platform.");
#endif
}

inline void GlTexture::Save(const std::string& filename, bool top_line_first)
{
    TypedImage image;
    Download(image);
    pangolin::SaveImage(image, filename, top_line_first);
}

inline void GlTexture::SetLinear()
{
    Bind();
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    Unbind();
}

inline void GlTexture::SetNearestNeighbour()
{
    Bind();
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
    Unbind();
}

inline void GlTexture::RenderToViewport(const bool flip) const
{
    if(flip) {
        RenderToViewportFlipY();
    }else{
        RenderToViewport();
    }
}

inline void GlTexture::RenderToViewport() const
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    GLfloat sq_vert[] = { -1,-1,  1,-1,  1, 1,  -1, 1 };
    glVertexPointer(2, GL_FLOAT, 0, sq_vert);
    glEnableClientState(GL_VERTEX_ARRAY);

    GLfloat sq_tex[]  = { 0,0,  1,0,  1,1,  0,1  };
    glTexCoordPointer(2, GL_FLOAT, 0, sq_tex);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

#ifndef HAVE_GLES
    glEnable(GL_TEXTURE_2D);
#endif
    Bind();

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

#ifndef HAVE_GLES
    glDisable(GL_TEXTURE_2D);
#endif
}

inline void GlTexture::RenderToViewport(Viewport tex_vp, bool flipx, bool flipy) const
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    GLfloat sq_vert[] = { -1,-1,  1,-1,  1, 1,  -1, 1 };
    glVertexPointer(2, GL_FLOAT, 0, sq_vert);
    glEnableClientState(GL_VERTEX_ARRAY);

    GLfloat l = tex_vp.l / (float)(width);
    GLfloat b = tex_vp.b / (float)(height);
    GLfloat r = (tex_vp.l+tex_vp.w) / (float)(width);
    GLfloat t = (tex_vp.b+tex_vp.h) / (float)(height);

    if(flipx) std::swap(l,r);
    if(flipy) std::swap(b,t);

    GLfloat sq_tex[]  = { l,b,  r,b,  r,t,  l,t };
    glTexCoordPointer(2, GL_FLOAT, 0, sq_tex);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glEnable(GL_TEXTURE_2D);
    Bind();

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_TEXTURE_2D);
}

inline void GlTexture::RenderToViewportFlipY() const
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    GLfloat sq_vert[] = { -1,-1,  1,-1,  1, 1,  -1, 1 };
    glVertexPointer(2, GL_FLOAT, 0, sq_vert);
    glEnableClientState(GL_VERTEX_ARRAY);

    GLfloat sq_tex[]  = { 0,1,  1,1,  1,0,  0,0  };
    glTexCoordPointer(2, GL_FLOAT, 0, sq_tex);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glEnable(GL_TEXTURE_2D);
    Bind();

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_TEXTURE_2D);
}

inline void GlTexture::RenderToViewportFlipXFlipY() const
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glMatrixMode(GL_MODELVIEW);
    glLoadIdentity();

    GLfloat sq_vert[] = { 1,1,  -1,1,  -1,-1,  1,-1 };
    glVertexPointer(2, GL_FLOAT, 0, sq_vert);
    glEnableClientState(GL_VERTEX_ARRAY);

    GLfloat sq_tex[]  = { 0,0,  1,0,  1,1,  0,1  };
    glTexCoordPointer(2, GL_FLOAT, 0, sq_tex);
    glEnableClientState(GL_TEXTURE_COORD_ARRAY);

    glEnable(GL_TEXTURE_2D);
    Bind();

    glDrawArrays(GL_TRIANGLE_FAN, 0, 4);

    glDisableClientState(GL_VERTEX_ARRAY);
    glDisableClientState(GL_TEXTURE_COORD_ARRAY);

    glDisable(GL_TEXTURE_2D);
}

////////////////////////////////////////////////////////////////////////////

inline GlRenderBuffer::GlRenderBuffer()
    : width(0), height(0), rbid(0)
{
}

inline GlRenderBuffer::GlRenderBuffer(GLint width, GLint height, GLint internal_format )
    : width(0), height(0), rbid(0)
{
    Reinitialise(width,height,internal_format);
}

#ifndef HAVE_GLES
inline void GlRenderBuffer::Reinitialise(GLint width, GLint height, GLint internal_format)
{
    if( this->width != 0 ) {
        glDeleteRenderbuffersEXT(1, &rbid);
    }

    this->width = width;
    this->height = height;
    glGenRenderbuffers(1, &rbid);
    glBindRenderbuffer(GL_RENDERBUFFER_EXT, rbid);
    glRenderbufferStorage(GL_RENDERBUFFER_EXT, internal_format, width, height);
    glBindRenderbuffer(GL_RENDERBUFFER_EXT, 0);
}

inline GlRenderBuffer::~GlRenderBuffer()
{
    if( width!=0 ) {
        glDeleteRenderbuffers(1, &rbid);
    }
}
#else
inline void GlRenderBuffer::Reinitialise(GLint width, GLint height, GLint internal_format)
{
    if( width!=0 ) {
        glDeleteTextures(1, &rbid);
    }

    // Use a texture instead...
    glGenTextures(1, &rbid);
    glBindTexture(GL_TEXTURE_2D, rbid);

    glTexImage2D(GL_TEXTURE_2D, 0, internal_format,
            width, height,
            0, internal_format, GL_UNSIGNED_SHORT, NULL);

    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
}

inline GlRenderBuffer::~GlRenderBuffer()
{
    // We have no GL context whilst exiting.
    if( width!=0 ) {
        glDeleteTextures(1, &rbid);
    }
}
#endif // HAVE_GLES

inline GlRenderBuffer::GlRenderBuffer(GlRenderBuffer&& tex)
    : width(tex.width), height(tex.height), rbid(tex.rbid)
{
    tex.rbid = tex.width = tex.height = 0;
}

////////////////////////////////////////////////////////////////////////////

inline GlFramebuffer::GlFramebuffer()
    : fbid(0), attachments(0)
{
}

inline GlFramebuffer::~GlFramebuffer()
{
    if(fbid) {
        glDeleteFramebuffers(1, &fbid);
    }
}

inline GlFramebuffer::GlFramebuffer(GlTexture& colour)
    : attachments(0)
{
    glGenFramebuffers(1, &fbid);
    AttachColour(colour);
    CheckGlDieOnError();
}

inline GlFramebuffer::GlFramebuffer(GlTexture& colour, GlRenderBuffer& depth)
    : attachments(0)
{
    glGenFramebuffers(1, &fbid);
    AttachColour(colour);
    AttachDepth(depth);
    CheckGlDieOnError();
}

inline GlFramebuffer::GlFramebuffer(GlTexture& colour0, GlTexture& colour1, GlRenderBuffer& depth)
    : attachments(0)
{
    glGenFramebuffers(1, &fbid);
    AttachColour(colour0);
    AttachColour(colour1);
    AttachDepth(depth);
    CheckGlDieOnError();
}

inline GlFramebuffer::GlFramebuffer(GlTexture& colour0, GlTexture& colour1, GlTexture& colour2, GlRenderBuffer& depth)
    : attachments(0)
{
    glGenFramebuffers(1, &fbid);
    AttachColour(colour0);
    AttachColour(colour1);
    AttachColour(colour2);
    AttachDepth(depth);
    CheckGlDieOnError();
}

inline GlFramebuffer::GlFramebuffer(GlTexture& colour0, GlTexture& colour1, GlTexture& colour2, GlTexture& colour3, GlRenderBuffer& depth)
    : attachments(0)
{
    glGenFramebuffers(1, &fbid);
    AttachColour(colour0);
    AttachColour(colour1);
    AttachColour(colour2);
    AttachColour(colour3);
    AttachDepth(depth);
    CheckGlDieOnError();
}

inline void GlFramebuffer::Bind() const
{
    glBindFramebuffer(GL_FRAMEBUFFER_EXT, fbid);
    glDrawBuffers( attachments, attachment_buffers );
}

inline void GlFramebuffer::Reinitialise()
{
    if(fbid) {
        glDeleteFramebuffers(1, &fbid);
    }
    glGenFramebuffers(1, &fbid);
}

inline void GlFramebuffer::Unbind() const
{
    glDrawBuffers( 1, attachment_buffers );
    glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
}

inline GLenum GlFramebuffer::AttachColour(GlTexture& tex )
{
    if(!fbid) Reinitialise();

    const GLenum color_attachment = GL_COLOR_ATTACHMENT0_EXT + attachments;
    glBindFramebuffer(GL_FRAMEBUFFER_EXT, fbid);
    glFramebufferTexture2D(GL_FRAMEBUFFER_EXT, color_attachment, GL_TEXTURE_2D, tex.tid, 0);
    glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
    attachments++;
    CheckGlDieOnError();
    return color_attachment;
}

inline void GlFramebuffer::AttachDepth(GlRenderBuffer& rb )
{
    if(!fbid) Reinitialise();

    glBindFramebuffer(GL_FRAMEBUFFER_EXT, fbid);
#if !defined(HAVE_GLES)
    glFramebufferRenderbuffer(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_RENDERBUFFER_EXT, rb.rbid);
#elif defined(HAVE_GLES_2)
    glFramebufferTexture2DEXT(GL_FRAMEBUFFER_EXT, GL_DEPTH_ATTACHMENT_EXT, GL_TEXTURE_2D, rb.rbid, 0);
#else
    throw std::exception();
#endif
    glBindFramebuffer(GL_FRAMEBUFFER_EXT, 0);
    CheckGlDieOnError();
}

////////////////////////////////////////////////////////////////////////////

inline GlBufferData::GlBufferData()
    : bo(0)
{
}

inline GlBufferData::GlBufferData(GlBufferType buffer_type, GLsizeiptr size_bytes, GLenum gluse, const void* data )
    : bo(0)
{
    Reinitialise(buffer_type, size_bytes, gluse, data );
}

template<typename T>
GlBufferData::GlBufferData(GlBufferType buffer_type, const std::vector<T>& data, GLenum gluse)
    : bo(0)
{
    Reinitialise(buffer_type, data.size()*sizeof(T), gluse, &data[0]);
}

//! Move Constructor
inline GlBufferData::GlBufferData(GlBufferData&& tex)
    : bo(0)
{
    *this = std::move(tex);
}

inline GlBufferData& GlBufferData::operator=(GlBufferData&& tex)
{
    Free();
    this->bo = tex.bo;
    this->buffer_type = tex.buffer_type;
    this->gluse = tex.gluse;
    this->size_bytes = tex.size_bytes;
    tex.bo = 0;
    return *this;
}

inline GlBufferData::~GlBufferData()
{
    Free();
}

inline void GlBufferData::Free()
{
    if(bo!=0) {
        glDeleteBuffers(1, &bo);
    }
}

inline bool GlBufferData::IsValid() const
{
    return bo != 0;
}

inline GLsizeiptr GlBufferData::SizeBytes() const
{
    return size_bytes;
}

inline void GlBufferData::Reinitialise(GlBufferType buffer_type, GLsizeiptr size_bytes, GLenum gluse, const void* data )
{
    if(!bo) {
        glGenBuffers(1, &bo);
    }

    this->buffer_type = buffer_type;
    this->gluse = gluse;
    this->size_bytes = size_bytes;

    Bind();
    glBufferData(buffer_type, size_bytes, data, gluse);
    Unbind();
}

inline void GlBufferData::Bind() const
{
    glBindBuffer(buffer_type, bo);
}

inline void GlBufferData::Unbind() const
{
    glBindBuffer(buffer_type, 0);
}

inline void GlBufferData::Upload(const GLvoid* data, GLsizeiptr size_bytes, GLintptr offset)
{
    if(offset + size_bytes > this->size_bytes) {
        throw std::runtime_error("GlBufferData: Trying to upload past capacity.");
    }

    Bind();
    glBufferSubData(buffer_type,offset,size_bytes,data);
    Unbind();
}

inline void GlBufferData::Download(GLvoid* data, GLsizeiptr size_bytes, GLintptr offset) const
{
    Bind();
    glGetBufferSubData(buffer_type, offset, size_bytes, data);
    Unbind();
}

template<typename T>
inline void GlBufferData::Upload(const std::vector<T>& data, GLintptr offset)
{
    const size_t total_bytes = data.size() * sizeof(T);
    assert(offset + total_bytes <= size_bytes);
    Upload(&data[0], total_bytes, offset);
}

#ifdef USE_EIGEN
template<typename Derived>
inline void GlBufferData::Upload(const Eigen::DenseBase<Derived>& data, GLintptr offset)
{
    const size_t num_elements = data.outerSize();
    const size_t matrix_bytes = data.outerStride() * num_elements;
    assert(offset + matrix_bytes <= size_bytes);
    Upload(data.data(), matrix_bytes, offset);
}
#endif

////////////////////////////////////////////////////////////////////////////

inline GlBuffer::GlBuffer()
    : GlBufferData(), datatype(0), num_elements(0), count_per_element(0)
{
}

inline GlBuffer::GlBuffer(GlBufferType buffer_type, GLuint num_elements, GLenum datatype, GLuint count_per_element, GLenum gluse )
    : GlBufferData(buffer_type, GLuint(num_elements * count_per_element * GlDataTypeBytes(datatype)), gluse),
      datatype(datatype), num_elements(num_elements), count_per_element(count_per_element)
{
}


inline GlBuffer::GlBuffer(GlBuffer&& o)
    : GlBufferData()
{
    *this = std::move(o);
}

inline GlBuffer& GlBuffer::operator=(GlBuffer&& o)
{
    datatype = o.datatype;
    num_elements = o.num_elements;
    count_per_element = o.count_per_element;
    GlBufferData::operator =(std::move(o));
    return *this;
}

inline void GlBuffer::Reinitialise(GlBufferType buffer_type, GLuint num_elements, GLenum datatype, GLuint count_per_element, GLenum gluse, const void *data )
{
    this->datatype = datatype;
    this->num_elements = num_elements;
    this->count_per_element = count_per_element;
    const GLuint size_bytes = GLuint(num_elements * count_per_element * GlDataTypeBytes(datatype));
    GlBufferData::Reinitialise(buffer_type, size_bytes, gluse, data);
}

inline void GlBuffer::Reinitialise(GlBuffer const& other )
{
    Reinitialise(other.buffer_type, other.num_elements, other.datatype, other.count_per_element, other.gluse);
}

inline void GlBuffer::Resize(GLuint new_num_elements)
{
    if(bo!=0 && num_elements > 0) {
#ifndef HAVE_GLES
        // Backup current data, reinit memory, restore old data
        const size_t backup_elements = std::min(new_num_elements,num_elements);
        const size_t backup_size_bytes = backup_elements*GlDataTypeBytes(datatype)*count_per_element;
        unsigned char* backup = new unsigned char[backup_size_bytes];
        Bind();
        glGetBufferSubData(buffer_type, 0, backup_size_bytes, backup);
        glBufferData(buffer_type, new_num_elements*GlDataTypeBytes(datatype)*count_per_element, 0, gluse);
        glBufferSubData(buffer_type, 0, backup_size_bytes, backup);
        Unbind();
        delete[] backup;
#else
        throw std::exception();
#endif
    }else{
        Reinitialise(buffer_type, new_num_elements, datatype, count_per_element, gluse);
    }
    num_elements = new_num_elements;
}

template<typename Scalar>
GlBuffer::GlBuffer(GlBufferType buffer_type, const std::vector<Scalar>& data, GLenum gluse)
    : GlBufferData(buffer_type, data, gluse)
{
    datatype = pangolin::GlFormatTraits<Scalar>::gltype;
    num_elements = data.size();
    count_per_element = 1;
}


#ifdef USE_EIGEN
    template<typename Scalar, int R, int C>
    GlBuffer::GlBuffer(GlBufferType buffer_type, const std::vector<Eigen::Matrix<Scalar, R,C>>& data, GLenum gluse)
        : GlBufferData(buffer_type, data, gluse)
    {
        typedef typename Eigen::Matrix<Scalar, R,C> Element;
        static_assert( Element::SizeAtCompileTime != Eigen::Dynamic, "No Dynamically sized elements.");
        static_assert( Element::SizeAtCompileTime * sizeof(Scalar) == sizeof(Element), "No padded elements.");

        datatype = pangolin::GlFormatTraits<Scalar>::gltype;
        num_elements = data.size();
        count_per_element = Element::SizeAtCompileTime;
    }
#endif

////////////////////////////////////////////////////////////////////////////

inline GlSizeableBuffer::GlSizeableBuffer(GlBufferType buffer_type, GLuint initial_num_elements, GLenum datatype, GLuint count_per_element, GLenum gluse )
    : GlBuffer(buffer_type, initial_num_elements, datatype, count_per_element, gluse), m_num_verts(0)
{

}

inline void GlSizeableBuffer::Clear()
{
    m_num_verts = 0;
}

#ifdef USE_EIGEN
template<typename Derived> inline
void GlSizeableBuffer::Add(const Eigen::DenseBase<Derived>& vec)
{
    typedef typename Eigen::DenseBase<Derived>::Scalar Scalar;
    assert(vec.rows()==GlBuffer::count_per_element);
    CheckResize(m_num_verts + 1);
    // TODO: taking address of first element is really dodgey. Need to work out
    // when this is okay!
    Upload(&vec(0,0), sizeof(Scalar)*vec.rows()*vec.cols(), sizeof(Scalar)*vec.rows()*m_num_verts);
    m_num_verts += vec.cols();
}

template<typename Derived> inline
void GlSizeableBuffer::Update(const Eigen::DenseBase<Derived>& vec, size_t position )
{
    typedef typename Eigen::DenseBase<Derived>::Scalar Scalar;
    assert(vec.rows()==GlBuffer::count_per_element);
    CheckResize(position + vec.cols() );
    // TODO: taking address of first element is really dodgey. Need to work out
    // when this is okay!
    Upload(&vec(0,0), sizeof(Scalar)*vec.rows()*vec.cols(), sizeof(Scalar)*vec.rows()*position );
    m_num_verts = std::max(position+vec.cols(), m_num_verts);
}
#endif

inline size_t GlSizeableBuffer::start() const {
    return 0;
}

inline size_t GlSizeableBuffer::size() const {
    return m_num_verts;
}

inline void GlSizeableBuffer::CheckResize(size_t num_verts)
{
    if( num_verts > GlBuffer::num_elements) {
        const size_t new_size = NextSize(num_verts);
        GlBuffer::Resize((GLuint)new_size);
    }
}

inline size_t GlSizeableBuffer::NextSize(size_t min_size) const
{
    size_t new_size = std::max(GlBuffer::num_elements, 1u);
    while(new_size < min_size) {
        new_size *= 2;
    }
    return new_size;
}

////////////////////////////////////////////////////////////////////////////

inline GlVertexArrayObject::GlVertexArrayObject()
{
    glGenVertexArrays(1, &vao);
}

inline GlVertexArrayObject::~GlVertexArrayObject()
{
    glDeleteVertexArrays(1, &vao);
}

inline void GlVertexArrayObject::Bind() const
{
    glBindVertexArray(vao);

}

inline void GlVertexArrayObject::Unbind() const
{
    glBindVertexArray(0);
}

inline void GlVertexArrayObject::AddVertexAttrib(GLuint attrib_location, const GlBuffer& bo, size_t offset_bytes, size_t stride_bytes, GLboolean normalized)
{
    Bind();
    bo.Bind();
    CheckGlDieOnError();
    glVertexAttribPointer(attrib_location, bo.count_per_element, bo.datatype, normalized, stride_bytes, (void*)offset_bytes);
    CheckGlDieOnError();
    glEnableVertexAttribArray(attrib_location);

}

////////////////////////////////////////////////////////////////////////////

inline TypedImage ReadFramebuffer(const Viewport& v, const std::string& pixel_format)
{
    const PixelFormat fmt = PixelFormatFromString(pixel_format);
    const GlPixFormat glfmt(fmt);

    TypedImage buffer(v.w, v.h, fmt );
    glReadBuffer(GL_BACK);
    glPixelStorei(GL_PACK_ALIGNMENT, 1);
    glReadPixels(v.l, v.b, v.w, v.h, glfmt.glformat, glfmt.gltype, buffer.ptr );
    return buffer;
}

}
