#pragma once

#include <epoxy/gl.h>
#include <epoxy/glx.h>

#include <string>

namespace endofthejedi {

template <GLenum T> class BufferObject {
  private:
    GLuint m_name;
    GLvoid *m_mappointer;

  protected:
  public:
    BufferObject();
    ~BufferObject();
    void init();
    void bind();
    void bind(GLuint index, GLintptr offset = 0, GLsizeiptr size = 0);
    void fill(GLenum usage, GLsizei size = 0, GLvoid *data = NULL);
    void subfill(GLintptr offset, GLsizei size, const GLvoid *data);
    void map(GLenum access);
    void unmap();
};

typedef BufferObject<GL_ARRAY_BUFFER> VBO;
typedef BufferObject<GL_ELEMENT_ARRAY_BUFFER> IBO;

class VAO {
  private:
    GLuint m_name;

  protected:
  public:
    VAO();
    ~VAO();
    void init();
    void bind();
    void unbind();
    void fill(GLuint index, GLint size, GLenum type, GLboolean normalized,
              GLsizei stride = 0, const GLvoid *pointer = NULL);
};

class Shader {
  private:
    GLuint m_program;

    bool check();
    bool checkShader(GLuint shader);

  protected:
  public:
    Shader();
    ~Shader();
    void init(); // call to init when opengl context exists

    void bind();
    void unbind();
    void load(const std::string &data, GLenum shadertype);
    void loadFile(const std::string &data, GLenum shadertype);
    GLuint location(const std::string &name);
    GLuint program();
};

class Texture {
  private:
    GLuint m_name;

  protected:
  public:
    Texture();
    ~Texture();
    void init();
    void bind();
    void unbind();
    void fill(GLint level, GLint internalFormat, GLsizei width, GLsizei height,
              GLint border, GLenum format, GLenum type, const GLvoid *data);
    GLuint getName();
};

class Framebuffer {
  private:
    GLuint m_name;

  protected:
  public:
    Framebuffer();
    ~Framebuffer();
    void init();
    void bind();
    void unbind();
    void attachRenderbuffer(GLenum attachment, GLuint rbo);
    void attachTexture(GLenum attachment, GLuint tex);
};

class Renderbuffer {
  private:
    GLuint m_name;

  protected:
  public:
    Renderbuffer();
    ~Renderbuffer();
    void init();
    void bind();
    void unbind();
    void create(GLenum internalformat, GLsizei width, GLsizei height);
};
}

#define TBufferObject_(pre, post)                                              \
    template <GLenum T> pre endofthejedi::BufferObject<T>::post
#define TBufferObject(...) TBufferObject_(__VA_ARGS__)

TBufferObject(, BufferObject)() {}

TBufferObject(, ~BufferObject)() { glDeleteBuffers(1, &m_name); }

TBufferObject(void, init)() { glGenBuffers(1, &m_name); }

TBufferObject(void, bind)() { glBindBuffer(T, m_name); }

TBufferObject(void, bind)(GLuint index, GLintptr offset, GLsizeiptr size) {
    // todo
}

TBufferObject(void, fill)(GLenum usage, GLsizei size, GLvoid *data) {
    glBufferData(T, size, data, usage);
}

TBufferObject(void, subfill)(GLintptr offset, GLsizei size,
                             const GLvoid *data) {
    glBufferSubData(T, offset, size, data);
}

TBufferObject(void, map)(GLenum access) {
    // todo
}

TBufferObject(void, unmap)() {
    // todo
}

void discardLastGlError(bool print = false);

// return false if there's an error
bool checkAndPrintGlError();

void printGlError(GLenum err);

const char *stringFromGlError(GLenum err);
