#include <psemek/gfx/effect/blur.hpp>
#include <psemek/gfx/program.hpp>
#include <psemek/gfx/array.hpp>

#include <psemek/util/to_string.hpp>

#include <iomanip>
#include <sstream>
#include <vector>

namespace psemek::gfx
{

	static char const blur_vs[] =
R"(#version 330

const vec4 vertices[6] = vec4[6](
	vec4(-1.0, -1.0, 0.0, 1.0),
	vec4( 1.0, -1.0, 0.0, 1.0),
	vec4( 1.0,  1.0, 0.0, 1.0),

	vec4(-1.0, -1.0, 0.0, 1.0),
	vec4( 1.0,  1.0, 0.0, 1.0),
	vec4(-1.0,  1.0, 0.0, 1.0)
);

out vec2 texcoord;

void main()
{
	gl_Position = vertices[gl_VertexID];
	texcoord = vertices[gl_VertexID].xy * 0.5 + vec2(0.5);
}
)";

	static char const blur_fs_template[] =
R"(#version 330

#define BLUR_SIZE @BLUR_SIZE@
#define BLUR_COEFFS_COUNT 2 * BLUR_SIZE + 1

const float coeffs[BLUR_COEFFS_COUNT] = float[BLUR_COEFFS_COUNT](@BLUR_COEFFS@);

const vec2 direction = vec2(@BLUR_DIRECTION@);

uniform sampler2D u_texture;
uniform vec2 u_inv_texture_size;
uniform float u_gamma;

in vec2 texcoord;

out vec4 out_color;

void main()
{
	vec4 result = vec4(0.0);
	for (int i = -BLUR_SIZE; i <= BLUR_SIZE; ++i)
	{
		vec4 p = texture(u_texture, texcoord + float(i) * direction * u_inv_texture_size);
		float c = coeffs[i + BLUR_SIZE];
		result.rgb += pow(p.rgb, vec3(1.0 / u_gamma)) * p.a * c;
		result.a += p.a * c;
	}
	if (result.a == 0.0)
		out_color = result;
	else
		out_color = vec4(pow(result.rgb / result.a, vec3(u_gamma)), result.a);
}
)";

	static void replace_all(std::string & str, std::string_view old_str, std::string_view new_str)
	{
		for (size_t i = 0;;)
		{
			i = str.find(old_str, i);
			if (i == std::string::npos) break;
			str.replace(i, old_str.size(), new_str);
			i += new_str.size();
		}
	}

	static std::string generate_blur_fs_source(int size, float sigma, bool horizontal)
	{
		std::string size_str = util::to_string(size);

		std::string direction_str = horizontal ? "1.0, 0.0" : "0.0, 1.0";

		std::vector<float> coeffs(2 * size + 1);
		{
			float sum = 0.f;
			for (int i = -size; i <= size; ++i)
			{
				float x = (i / sigma);
				coeffs[i + size] = std::exp(- x * x);
				sum += coeffs[i + size];
			}
			for (auto & c : coeffs)
				c /= sum;
		}

		std::ostringstream coeffs_ss;
		for (std::size_t i = 0; i < coeffs.size(); ++i)
		{
			if (i > 0) coeffs_ss << ", ";
			coeffs_ss << std::fixed << std::setprecision(10) << coeffs[i];
		}

		std::string result = blur_fs_template;
		replace_all(result, "@BLUR_SIZE@", size_str);
		replace_all(result, "@BLUR_COEFFS@", coeffs_ss.str());
		replace_all(result, "@BLUR_DIRECTION@", direction_str);
		return result;
	}

	struct blur_impl
	{
		gfx::program program;
		gfx::array array;

		blur_impl(int size, float sigma, float gamma, bool horizontal)
			: program(blur_vs, generate_blur_fs_source(size, sigma, horizontal))
		{
			program.bind();
			program["u_texture"] = 0;
			program["u_gamma"] = gamma;
		}

		void invoke(texture_2d const & src, render_target const & dst)
		{
			gl::Disable(gl::DEPTH_TEST);
			gl::Disable(gl::CULL_FACE);
			gl::Disable(gl::BLEND);

			dst.bind();
			gl::ActiveTexture(gl::TEXTURE0);
			src.bind();
			array.bind();
			program.bind();
			program["u_inv_texture_size"] = math::vector{1.f / src.width(), 1.f / src.height()};
			gl::DrawArrays(gl::TRIANGLES, 0, 6);
		}
	};

	struct hblur::impl
		: blur_impl
	{
		using blur_impl::blur_impl;
	};

	hblur::hblur(int size, float sigma, float gamma)
		: pimpl_{make_impl(size, sigma, gamma, true)}
	{}

	hblur::~hblur() = default;

	void hblur::invoke(texture_2d const & src, render_target const & dst)
	{
		impl().invoke(src, dst);
	}

	struct vblur::impl
		: blur_impl
	{
		using blur_impl::blur_impl;
	};

	vblur::vblur(int size, float sigma, float gamma)
		: pimpl_{make_impl(size, sigma, gamma, false)}
	{}

	vblur::~vblur() = default;

	void vblur::invoke(texture_2d const & src, render_target const & dst)
	{
		impl().invoke(src, dst);
	}

}
