/*
 * Copyright 2016 - 2024 Gary Talent (gary@drinkingtea.net). All rights reserved.
 */

#pragma once

#include <ox/std/defines.hpp>

#include <glad/glad.h>

#include <ox/std/bounds.hpp>
#include <ox/std/cstringview.hpp>
#include <ox/std/error.hpp>
#include <ox/std/size.hpp>
#include <ox/std/string.hpp>
#include <ox/std/vec.hpp>
#include <ox/std/vector.hpp>

namespace glutils {

struct Empty {
	virtual ~Empty() noexcept = default;
};

struct TextureBase {

	GLsizei width = 0;
	GLsizei height = 0;

	constexpr TextureBase() noexcept = default;

	constexpr TextureBase(TextureBase &&tb) noexcept {
		width = tb.width;
		height = tb.height;
		tb.width = 0;
		tb.height = 0;
	}

	constexpr TextureBase &operator=(TextureBase &&tb) noexcept {
		width = tb.width;
		height = tb.height;
		tb.width = 0;
		tb.height = 0;
		return *this;
	}

	virtual ~TextureBase() noexcept = default;

};


template<auto del, typename Base = Empty>
struct GLObject: public Base {

	GLuint id = 0;

	constexpr GLObject() noexcept = default;

	explicit constexpr GLObject(GLuint id) noexcept {
		this->id = id;
	}

	constexpr GLObject(GLObject &&o) noexcept: Base(std::move(o)) {
		id = o.id;
		o.id = 0;
	}

	~GLObject() noexcept override {
		del(id);
	}

	GLObject &operator=(GLObject &&o) noexcept {
		if (this != &o) {
			del(id);
			Base::operator=(std::move(o));
			id = o.id;
			o.id = 0;
		}
		return *this;
	}

	constexpr GLuint release() noexcept {
		auto out = id;
		id = 0;
		return out;
	}

	constexpr operator GLuint&() noexcept {
		return id;
	}

	constexpr operator GLuint const&() const noexcept {
		return id;
	}

};

void deleteBuffer(GLuint b) noexcept;
void deleteFrameBuffer(GLuint b) noexcept;
void deleteRenderBuffer(GLuint b) noexcept;
void deleteTexture(GLuint t) noexcept;
void deleteVertexArray(GLuint v) noexcept;
void deleteProgram(GLuint p) noexcept;
void deleteShader(GLuint s) noexcept;

extern template struct GLObject<deleteBuffer>;
extern template struct GLObject<deleteFrameBuffer>;
extern template struct GLObject<deleteRenderBuffer>;
extern template struct GLObject<deleteTexture, TextureBase>;
extern template struct GLObject<deleteVertexArray>;
extern template struct GLObject<deleteProgram>;
extern template struct GLObject<deleteShader>;

using GLBuffer = GLObject<deleteBuffer>;
using GLFrameBuffer = GLObject<deleteFrameBuffer>;
using GLRenderBuffer = GLObject<deleteRenderBuffer>;
using GLShader = GLObject<deleteShader>;
using GLProgram = GLObject<deleteProgram>;
using GLTexture = GLObject<deleteTexture, TextureBase>;
using GLVertexArray = GLObject<deleteVertexArray>;

/**
 * FrameBuffer holds everything needed for a usable frame buffer to exist.
 * It differs from GLFrameBuffer in that GLFrameBuffer only manages the FBO
 * and not its dependencies.
 */
struct FrameBuffer {
	int width = 0;
	int height = 0;
	GLFrameBuffer fbo;
	GLTexture color;
	GLRenderBuffer depth;

	constexpr operator GLuint&() noexcept {
		return fbo.id;
	}

	constexpr operator GLuint const&() const noexcept {
		return fbo.id;
	}

	[[nodiscard]]
	constexpr ox::Vec2 sizef() const noexcept {
		return {
			static_cast<float>(width),
			static_cast<float>(height),
		};
	}

	[[nodiscard]]
	constexpr ox::Size size() const noexcept {
		return {
			width,
			height,
		};
	}
};

class FrameBufferBind {
	private:
		static FrameBuffer const *s_activeFb;
		FrameBuffer const *m_restoreFb = nullptr;
	public:
		explicit FrameBufferBind(FrameBuffer const &fb) noexcept;
		~FrameBufferBind() noexcept;
};

void bind(FrameBuffer const &fb) noexcept;

struct ShaderVarSet {
	GLsizei len{};
	ox::String name;
};

struct ProgramSource {
	ox::Vector<glutils::ShaderVarSet> const shaderParams;
	GLsizei const rowLen = [this] {
		GLsizei len{};
		for (auto const &v : shaderParams) {
			len += v.len;
		}
		return len;
	}();
	GLsizei const vboLen = rowLen * 4;
	ox::String const vertShader{};
	ox::String const fragShader{};
	ox::String const geomShader{};
};

ox::Result<GLProgram> buildShaderProgram(ProgramSource const &src) noexcept;

ox::Result<GLProgram> buildShaderProgram(
		ox::CStringView const &vert,
		ox::CStringView const &frag,
		ox::CStringView const &geo = "") noexcept;

void setupShaderParams(
		GLProgram const &shader,
		ox::Vector<ShaderVarSet> const &vars,
		GLsizei vertexRowLen) noexcept;

void setupShaderParams(GLProgram const &shader, ox::Vector<ShaderVarSet> const &vars) noexcept;

GLVertexArray generateVertexArrayObject() noexcept;

GLBuffer generateBuffer() noexcept;

[[nodiscard]]
FrameBuffer generateFrameBuffer(int width, int height) noexcept;

void resizeFrameBuffer(FrameBuffer &fb, int width, int height) noexcept;

/**
 * Resizes a FrameBuffer, and creates if it does not already exist.
 */
void resizeInitFrameBuffer(FrameBuffer &fb, int width, int height) noexcept;

void resizeInitFrameBuffer(FrameBuffer &fb, ox::Size const &sz) noexcept;

struct BufferSet {
	GLVertexArray vao;
	GLBuffer vbo;
	GLBuffer ebo;
	GLTexture tex;
	ox::Vector<float> vertices;
	ox::Vector<GLuint> elements;
};

void sendVbo(BufferSet const &bs) noexcept;

void sendEbo(BufferSet const &bs) noexcept;

void clearScreen() noexcept;

}
