#include <obs-module.h>
#include <util/circlebuf.h>
#include <util/util_uint64.h>

#define S_DELAY_MS "delay_ms"
#define T_DELAY_MS obs_module_text("DelayMs")

struct frame {
	gs_texrender_t *render;
	enum gs_color_space space;
	uint64_t ts;
};

struct gpu_delay_filter_data {
	obs_source_t *context;
	struct circlebuf frames;
	uint64_t delay_ns;
	uint64_t interval_ns;
	uint32_t cx;
	uint32_t cy;
	bool target_valid;
	bool processed_frame;
};

static const char *gpu_delay_filter_get_name(void *unused)
{
	UNUSED_PARAMETER(unused);
	return obs_module_text("GPUDelayFilter");
}

static void free_textures(struct gpu_delay_filter_data *f)
{
	obs_enter_graphics();
	while (f->frames.size) {
		struct frame frame;
		circlebuf_pop_front(&f->frames, &frame, sizeof(frame));
		gs_texrender_destroy(frame.render);
	}
	circlebuf_free(&f->frames);
	obs_leave_graphics();
}

static size_t num_frames(struct circlebuf *buf)
{
	return buf->size / sizeof(struct frame);
}

static void update_interval(struct gpu_delay_filter_data *f,
			    uint64_t new_interval_ns)
{
	if (!f->target_valid) {
		free_textures(f);
		return;
	}

	f->interval_ns = new_interval_ns;
	size_t num = (size_t)(f->delay_ns / new_interval_ns);

	if (num > num_frames(&f->frames)) {
		size_t prev_num = num_frames(&f->frames);

		obs_enter_graphics();

		circlebuf_upsize(&f->frames, num * sizeof(struct frame));

		for (size_t i = prev_num; i < num; i++) {
			struct frame *frame =
				circlebuf_data(&f->frames, i * sizeof(*frame));
			frame->render =
				gs_texrender_create(GS_RGBA, GS_ZS_NONE);
		}

		obs_leave_graphics();

	} else if (num < num_frames(&f->frames)) {
		obs_enter_graphics();

		while (num_frames(&f->frames) > num) {
			struct frame frame;
			circlebuf_pop_front(&f->frames, &frame, sizeof(frame));
			gs_texrender_destroy(frame.render);
		}

		obs_leave_graphics();
	}
}

static inline void check_interval(struct gpu_delay_filter_data *f)
{
	struct obs_video_info ovi = {0};
	uint64_t interval_ns;

	obs_get_video_info(&ovi);

	interval_ns = util_mul_div64(ovi.fps_den, 1000000000ULL, ovi.fps_num);

	if (interval_ns != f->interval_ns)
		update_interval(f, interval_ns);
}

static inline void reset_textures(struct gpu_delay_filter_data *f)
{
	f->interval_ns = 0;
	free_textures(f);
	check_interval(f);
}

static inline bool check_size(struct gpu_delay_filter_data *f)
{
	obs_source_t *target = obs_filter_get_target(f->context);
	uint32_t cx;
	uint32_t cy;

	f->target_valid = !!target;
	if (!f->target_valid)
		return true;

	cx = obs_source_get_base_width(target);
	cy = obs_source_get_base_height(target);

	f->target_valid = !!cx && !!cy;
	if (!f->target_valid)
		return true;

	if (cx != f->cx || cy != f->cy) {
		f->cx = cx;
		f->cy = cy;
		reset_textures(f);
		return true;
	}

	return false;
}

static void gpu_delay_filter_update(void *data, obs_data_t *s)
{
	struct gpu_delay_filter_data *f = data;

	f->delay_ns = (uint64_t)obs_data_get_int(s, S_DELAY_MS) * 1000000ULL;

	/* full reset */
	f->cx = 0;
	f->cy = 0;
	f->interval_ns = 0;
	free_textures(f);
}

static obs_properties_t *gpu_delay_filter_properties(void *data)
{
	obs_properties_t *props = obs_properties_create();

	obs_property_t *p = obs_properties_add_int(props, S_DELAY_MS,
						   T_DELAY_MS, 0, 500, 1);
	obs_property_int_set_suffix(p, " ms");

	UNUSED_PARAMETER(data);
	return props;
}

static void *gpu_delay_filter_create(obs_data_t *settings,
				     obs_source_t *context)
{
	struct gpu_delay_filter_data *f = bzalloc(sizeof(*f));
	f->context = context;

	obs_source_update(context, settings);
	return f;
}

static void gpu_delay_filter_destroy(void *data)
{
	struct gpu_delay_filter_data *f = data;

	free_textures(f);
	bfree(f);
}

static void gpu_delay_filter_tick(void *data, float t)
{
	UNUSED_PARAMETER(t);

	struct gpu_delay_filter_data *f = data;

	f->processed_frame = false;

	if (check_size(f))
		return;
	check_interval(f);
}

static const char *
get_tech_name_and_multiplier(enum gs_color_space current_space,
			     enum gs_color_space source_space,
			     float *multiplier)
{
	const char *tech_name = "Draw";
	*multiplier = 1.f;

	switch (source_space) {
	case GS_CS_SRGB:
	case GS_CS_SRGB_16F:
		switch (current_space) {
		case GS_CS_709_SCRGB:
			tech_name = "DrawMultiply";
			*multiplier = obs_get_video_sdr_white_level() / 80.0f;
		}
		break;
	case GS_CS_709_EXTENDED:
		switch (current_space) {
		case GS_CS_SRGB:
		case GS_CS_SRGB_16F:
			tech_name = "DrawTonemap";
			break;
		case GS_CS_709_SCRGB:
			tech_name = "DrawMultiply";
			*multiplier = obs_get_video_sdr_white_level() / 80.0f;
		}
		break;
	case GS_CS_709_SCRGB:
		switch (current_space) {
		case GS_CS_SRGB:
		case GS_CS_SRGB_16F:
			tech_name = "DrawMultiplyTonemap";
			*multiplier = 80.0f / obs_get_video_sdr_white_level();
			break;
		case GS_CS_709_EXTENDED:
			tech_name = "DrawMultiply";
			*multiplier = 80.0f / obs_get_video_sdr_white_level();
		}
	}

	return tech_name;
}

static void draw_frame(struct gpu_delay_filter_data *f)
{
	struct frame frame;
	circlebuf_peek_front(&f->frames, &frame, sizeof(frame));

	const enum gs_color_space current_space = gs_get_color_space();
	float multiplier;
	const char *technique = get_tech_name_and_multiplier(
		current_space, frame.space, &multiplier);

	gs_effect_t *effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
	gs_texture_t *tex = gs_texrender_get_texture(frame.render);
	if (tex) {
		const bool previous = gs_framebuffer_srgb_enabled();
		gs_enable_framebuffer_srgb(true);

		gs_effect_set_texture_srgb(
			gs_effect_get_param_by_name(effect, "image"), tex);
		gs_effect_set_float(gs_effect_get_param_by_name(effect,
								"multiplier"),
				    multiplier);

		while (gs_effect_loop(effect, technique))
			gs_draw_sprite(tex, 0, f->cx, f->cy);

		gs_enable_framebuffer_srgb(previous);
	}
}

static void gpu_delay_filter_render(void *data, gs_effect_t *effect)
{
	struct gpu_delay_filter_data *f = data;
	obs_source_t *target = obs_filter_get_target(f->context);
	obs_source_t *parent = obs_filter_get_parent(f->context);

	if (!f->target_valid || !target || !parent || !f->frames.size) {
		obs_source_skip_video_filter(f->context);
		return;
	}

	if (f->processed_frame) {
		draw_frame(f);
		return;
	}

	struct frame frame;
	circlebuf_pop_front(&f->frames, &frame, sizeof(frame));

	const enum gs_color_space preferred_spaces[] = {
		GS_CS_SRGB,
		GS_CS_SRGB_16F,
		GS_CS_709_EXTENDED,
	};
	const enum gs_color_space space = obs_source_get_color_space(
		target, OBS_COUNTOF(preferred_spaces), preferred_spaces);
	const enum gs_color_format format = gs_get_format_from_space(space);
	if (gs_texrender_get_format(frame.render) != format) {
		gs_texrender_destroy(frame.render);
		frame.render = gs_texrender_create(format, GS_ZS_NONE);
	}

	gs_texrender_reset(frame.render);

	gs_blend_state_push();
	gs_blend_function(GS_BLEND_ONE, GS_BLEND_ZERO);

	if (gs_texrender_begin_with_color_space(frame.render, f->cx, f->cy,
						space)) {
		uint32_t parent_flags = obs_source_get_output_flags(target);
		bool custom_draw = (parent_flags & OBS_SOURCE_CUSTOM_DRAW) != 0;
		bool async = (parent_flags & OBS_SOURCE_ASYNC) != 0;
		struct vec4 clear_color;

		vec4_zero(&clear_color);
		gs_clear(GS_CLEAR_COLOR, &clear_color, 0.0f, 0);
		gs_ortho(0.0f, (float)f->cx, 0.0f, (float)f->cy, -100.0f,
			 100.0f);

		if (target == parent && !custom_draw && !async)
			obs_source_default_render(target);
		else
			obs_source_video_render(target);

		gs_texrender_end(frame.render);

		frame.space = space;
	}

	gs_blend_state_pop();

	circlebuf_push_back(&f->frames, &frame, sizeof(frame));
	draw_frame(f);
	f->processed_frame = true;

	UNUSED_PARAMETER(effect);
}

static enum gs_color_space
gpu_delay_filter_get_color_space(void *data, size_t count,
				 const enum gs_color_space *preferred_spaces)
{
	struct gpu_delay_filter_data *const f = data;
	obs_source_t *target = obs_filter_get_target(f->context);
	obs_source_t *parent = obs_filter_get_parent(f->context);

	if (!f->target_valid || !target || !parent || !f->frames.size) {
		return (count > 0) ? preferred_spaces[0] : GS_CS_SRGB;
	}

	struct frame frame;
	circlebuf_peek_front(&f->frames, &frame, sizeof(frame));
	enum gs_color_space space = frame.space;
	for (size_t i = 0; i < count; ++i) {
		space = preferred_spaces[i];
		if (space == frame.space)
			break;
	}

	return space;
}

struct obs_source_info gpu_delay_filter = {
	.id = "gpu_delay",
	.type = OBS_SOURCE_TYPE_FILTER,
	.output_flags = OBS_SOURCE_VIDEO,
	.get_name = gpu_delay_filter_get_name,
	.create = gpu_delay_filter_create,
	.destroy = gpu_delay_filter_destroy,
	.update = gpu_delay_filter_update,
	.get_properties = gpu_delay_filter_properties,
	.video_tick = gpu_delay_filter_tick,
	.video_render = gpu_delay_filter_render,
	.video_get_color_space = gpu_delay_filter_get_color_space,
};
