/*
 * This software is licensed under the terms of the MIT License.
 * See COPYING for further information.
 * ---
 * Copyright (c) 2011-2024, Lukas Weber <laochailan@web.de>.
 * Copyright (c) 2012-2024, Andrei Alexeyev <akari@taisei-project.org>.
 */

#include "dialog.h"

#include "global.h"
#include "portrait.h"
#include "resource/font.h"

void dialog_init(Dialog *d) {
	memset(d, 0, sizeof(*d));
	d->state = DIALOG_STATE_IDLE;
	d->text.current = &d->text.buffers[0];
	d->text.fading_out = &d->text.buffers[1];
	COEVENT_INIT_ARRAY(d->events);
}

void dialog_deinit(Dialog *d) {
	COEVENT_CANCEL_ARRAY(d->events);

	for(DialogActor *a = d->actors.first; a; a = a->next) {
		if(a->composite.tex) {
			r_texture_destroy(a->composite.tex);
		}
	}
}

void dialog_add_actor(Dialog *d, DialogActor *a, const char *name, DialogSide side) {
	memset(a, 0, sizeof(*a));
	a->name = name;
	a->face = "normal";
	a->side = side;
	a->target_opacity = 1;
	a->composite_dirty = true;

	if(side == DIALOG_SIDE_RIGHT) {
		a->speech_color = *RGB(0.6, 0.6, 1.0);
	} else {
		a->speech_color = *RGB(1.0, 1.0, 1.0);
	}

	alist_append(&d->actors, a);
}

void dialog_actor_set_face(DialogActor *a, const char *face) {
	log_debug("[%s] %s --> %s", a->name, a->face, face);
	if(a->face != face) {
		a->face = face;
		a->composite_dirty = true;
	}
}

void dialog_actor_set_variant(DialogActor *a, const char *variant) {
	log_debug("[%s] %s --> %s", a->name, a->variant, variant);
	if(a->variant != variant) {
		a->variant = variant;
		a->composite_dirty = true;
	}
}

void dialog_update(Dialog *d) {
	if(d->text.current->text) {
		fapproach_p(&d->text.current->opacity, 1, 1/120.0f);
	} else {
		d->text.current->opacity = 0;
	}

	if(d->text.fading_out->text) {
		fapproach_p(&d->text.fading_out->opacity, 0, 1/60.0f);
	} else {
		d->text.fading_out->opacity = 0;
	}

	if(
		d->state == DIALOG_STATE_FADEOUT ||
		(
			d->text.current->opacity == 0 &&
			d->text.fading_out->opacity < 0.25
		)
	) {
		fapproach_asymptotic_p(&d->opacity, 0, 0.1, 1e-3);
	} else {
		fapproach_asymptotic_p(&d->opacity, 1, 0.05, 1e-3);
	}

	const float offset_per_actor = 32;
	float target_offsets[2] = { 0 };

	for(DialogActor *a = d->actors.last; a; a = a->prev) {
		fapproach_asymptotic_p(&a->offset.x, target_offsets[a->side], 0.10, 1e-3);
		target_offsets[a->side] += offset_per_actor;

		if(d->state == DIALOG_STATE_FADEOUT) {
			fapproach_asymptotic_p(&a->opacity, 0, 0.12, 1e-3);
		} else {
			fapproach_asymptotic_p(&a->opacity, a->target_opacity, 0.04, 1e-3);
		}

		fapproach_asymptotic_p(&a->focus, a->target_focus, 0.12, 1e-3);
	}
}

void dialog_skippable_wait(Dialog *d, int timeout) {
	CoEventSnapshot snap = coevent_snapshot(&d->events.skip_requested);

	assert(d->state == DIALOG_STATE_IDLE);
	d->state = DIALOG_STATE_WAITING_FOR_SKIP;

	while(timeout > 0) {
		dialog_update(d);

		--timeout;
		YIELD;

		if(coevent_poll(&d->events.skip_requested, &snap) != CO_EVENT_PENDING) {
			log_debug("Skipped with %i remaining", timeout);
			break;
		}
	}

	assert(d->state == DIALOG_STATE_WAITING_FOR_SKIP);
	d->state = DIALOG_STATE_IDLE;

	if(timeout == 0) {
		log_debug("Timed out");
	}
}

int dialog_util_estimate_wait_timeout_from_text(const char *text) {
	return 1800;
}

static void dialog_set_text(Dialog *d, const char *text, const Color *clr) {
	DialogTextBuffer *temp = d->text.current;
	d->text.current = d->text.fading_out;
	d->text.fading_out = temp;

	d->text.current->color = *clr;
	d->text.current->text = text;
}

void dialog_focus_actor(Dialog *d, DialogActor *actor) {
	for(DialogActor *a = d->actors.first; a; a = a->next) {
		a->target_focus = 0;
	}

	actor->target_focus = 1;

	// make focused actor drawn on top of everyone else
	alist_unlink(&d->actors, actor);
	alist_append(&d->actors, actor);
}

void dialog_message_ex(Dialog *d, const DialogMessageParams *params) {
	assume(params->actor != NULL);
	assume(params->text != NULL);

	log_debug("%s: %s", params->actor->name, params->text);

	dialog_set_text(d, params->text, &params->actor->speech_color);
	dialog_focus_actor(d, params->actor);

	if(params->implicit_wait) {
		assume(params->wait_timeout > 0);

		if(params->wait_skippable) {
			dialog_skippable_wait(d, params->wait_timeout);
		} else {
			WAIT(params->wait_timeout);
		}
	}
}

static void _dialog_message(Dialog *d, DialogActor *actor, const char *text, bool skippable, int delay) {
	DialogMessageParams p = { 0 };
	p.actor = actor;
	p.text = text;
	p.implicit_wait = true;
	p.wait_skippable = skippable;
	p.wait_timeout = delay;
	dialog_message_ex(d, &p);
}

void dialog_message(Dialog *d, DialogActor *actor, const char *text) {
	_dialog_message(d, actor, text, true, dialog_util_estimate_wait_timeout_from_text(text));
}

void dialog_message_unskippable(Dialog *d, DialogActor *actor, const char *text, int delay) {
	_dialog_message(d, actor, text, false, delay);
}

void dialog_end(Dialog *d) {
	d->state = DIALOG_STATE_FADEOUT;
	coevent_signal(&d->events.fadeout_began);

	for(DialogActor *a = d->actors.first; a; a = a->next) {
		a->target_opacity = 0;
	}

	wait_for_fadeout: {
		if(d->opacity > 0) {
			YIELD;
			goto wait_for_fadeout;
		}

		for(DialogActor *a = d->actors.first; a; a = a->next) {
			if(a->opacity > 0) {
				YIELD;
				goto wait_for_fadeout;
			}
		}
	}

	coevent_signal(&d->events.fadeout_ended);
	dialog_deinit(d);
}

static void dialog_actor_update_composite(DialogActor *a) {
	assume(a->name != NULL);
	assume(a->face != NULL);

	if(!a->composite_dirty) {
		return;
	}

	log_debug("%s (%p) is dirty; face=%s; variant=%s", a->name, (void*)a, a->face, a->variant);

	if(a->composite.tex != NULL) {
		log_debug("destroyed texture at %p", (void*)a->composite.tex);
		r_texture_destroy(a->composite.tex);
	}

	portrait_render_byname(a->name, a->variant, a->face, &a->composite);
	log_debug("created texture at %p", (void*)a->composite.tex);
	a->composite_dirty = false;
}

void dialog_draw(Dialog *dialog) {
	if(dialog == NULL) {
		return;
	}

	float o = dialog->opacity;

	for(DialogActor *a = dialog->actors.first; a; a = a->next) {
		dialog_actor_update_composite(a);
	}

	r_state_push();
	r_state_push();
	r_shader("sprite_default");

	r_mat_mv_push();
	r_mat_mv_translate(VIEWPORT_X, 0, 0);

	const double dialog_width = VIEWPORT_W * 1.2;

	r_mat_mv_push();
	r_mat_mv_translate(dialog_width/2.0, 64, 0);

	Color clr = { 0 };

	for(DialogActor *a = dialog->actors.first; a; a = a->next) {
		if(a->opacity <= 0) {
			continue;
		}

		dialog_actor_update_composite(a);
		Sprite *portrait = &a->composite;
		assume(portrait->tex != NULL);

		r_mat_mv_push();

		if(a->side == DIALOG_SIDE_LEFT) {
			r_cull(CULL_FRONT);
			r_mat_mv_scale(-1, 1, 1);
		} else {
			r_cull(CULL_BACK);
		}

		if(a->opacity < 1) {
			r_mat_mv_translate(120 * (1 - a->opacity), 0, 0);
		}

		float ofs = 10 * (1 - a->focus);
		r_mat_mv_translate(ofs, ofs, 0);
		float brightness = 0.5 + 0.5 * a->focus;
		clr.r = clr.g = clr.b = brightness;
		clr.a = 1;

		color_mul_scalar(&clr, a->opacity);

		r_flush_sprites();
		r_draw_sprite(&(SpriteParams) {
			.blend = BLEND_PREMUL_ALPHA,
			.color = &clr,
			.pos.x = (dialog_width - portrait->w) / 2 + 32 + a->offset.x,
			.pos.y = VIEWPORT_H - portrait->h / 2 + a->offset.y,
			.sprite_ptr = portrait,
		});

		r_mat_mv_pop();
	}

	r_mat_mv_pop();
	r_state_pop();

	FloatRect dialog_bg_rect = {
		.extent = { VIEWPORT_W-40, 110 },
		.offset = { VIEWPORT_W/2, VIEWPORT_H-55 },
	};

	r_mat_mv_push();
	if(o < 1) {
		r_mat_mv_translate(0, 100 * (1 - o), 0);
	}
	r_color4(0, 0, 0, 0.8 * o);
	r_mat_mv_push();
	r_mat_mv_translate(dialog_bg_rect.x, dialog_bg_rect.y, 0);
	r_mat_mv_scale(dialog_bg_rect.w, dialog_bg_rect.h, 1);
	r_shader_standard_notex();
	r_draw_quad();
	r_mat_mv_pop();

	Font *font = res_font("standard");

	r_mat_tex_push();

	dialog_bg_rect.w = VIEWPORT_W * 0.86;
	dialog_bg_rect.x -= dialog_bg_rect.w * 0.5;
	dialog_bg_rect.y -= dialog_bg_rect.h * 0.5;

	if(dialog->text.fading_out->opacity > 0) {
		clr = dialog->text.fading_out->color;
		color_mul_scalar(&clr, o);

		text_draw_wrapped(dialog->text.fading_out->text, dialog_bg_rect.w, &(TextParams) {
			.shader = "text_dialog",
			.aux_textures = { res_texture("cell_noise") },
			.shader_params = &(ShaderCustomParams) {{ o * (1.0 - (0.2 + 0.8 * (1 - dialog->text.fading_out->opacity))), 1 }},
			.color = &clr,
			.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
			.align = ALIGN_CENTER,
			.font_ptr = font,
			.overlay_projection = &dialog_bg_rect,
		});
	}

	if(dialog->text.current->opacity > 0) {
		clr = dialog->text.current->color;
		color_mul_scalar(&clr, o);

		text_draw_wrapped(dialog->text.current->text, dialog_bg_rect.w, &(TextParams) {
			.shader = "text_dialog",
			.aux_textures = { res_texture("cell_noise") },
			.shader_params = &(ShaderCustomParams) {{ o * dialog->text.current->opacity, 0 }},
			.color = &clr,
			.pos = { VIEWPORT_W/2, VIEWPORT_H-110 + font_get_lineskip(font) },
			.align = ALIGN_CENTER,
			.font_ptr = font,
			.overlay_projection = &dialog_bg_rect,
		});
	}

	r_mat_tex_pop();
	r_mat_mv_pop();
	r_mat_mv_pop();
	r_state_pop();
}

bool dialog_page(Dialog *d) {
	if(d->state == DIALOG_STATE_WAITING_FOR_SKIP) {
		coevent_signal(&d->events.skip_requested);
		return true;
	}

	return false;
}

bool dialog_is_active(Dialog *d) {
	return d && d->state != DIALOG_STATE_FADEOUT;
}

void dialog_preload(ResourceGroup *rg) {
	res_group_preload(rg, RES_SHADER_PROGRAM, RESF_DEFAULT, "text_dialog", NULL);
}
