/*
 * 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 "common_tasks.h"

#include "audio/audio.h"
#include "random.h"
#include "stage.h"
#include "util/glm.h"

void common_drop_items(cmplx pos, const ItemCounts *items) {
	for(int i = 0; i < ARRAY_SIZE(items->as_array); ++i) {
		for(int j = items->as_array[i]; j; --j) {
			spawn_item(pos, i + ITEM_FIRST);
			WAIT(2);
		}
	}
}

DEFINE_EXTERN_TASK(common_drop_items) {
	common_drop_items(*ARGS.pos, &ARGS.items);
}

void common_move_loop(cmplx *restrict pos, MoveParams *restrict mp) {
	for(;;) {
		move_update(pos, mp);
		YIELD;
	}
}

DEFINE_EXTERN_TASK(common_move) {
	if(ARGS.ent.ent) {
		TASK_BIND(ARGS.ent);
	}

	MoveParams p = ARGS.move_params;
	common_move_loop(ARGS.pos, &p);
}

DEFINE_EXTERN_TASK(common_move_ext) {
	if(ARGS.ent.ent) {
		TASK_BIND(ARGS.ent);
	}

	common_move_loop(ARGS.pos, ARGS.move_params);
}

DEFINE_EXTERN_TASK(common_call_func) {
	ARGS.func();
}

DEFINE_EXTERN_TASK(common_start_bgm) {
	stage_start_bgm(ARGS.bgm);
}

static Projectile *spawn_charge_particle_smoke(cmplx target, real power) {
	real scale = power * rng_range(0.7, 1);
	real opacity = power * rng_range(0.5, 0.8);
	cmplx pos = target + rng_dir() * 16 * power;
	MoveParams move = move_asymptotic_simple(0.5 * rng_dir(), 3);
	real angle = rng_angle();
	int timeout = rng_irange(30, 60);

	return PARTICLE(
		.sprite = "smoke",
		.pos = pos,
		.draw_rule = pdraw_timeout_scalefade_exp(0.01, scale, opacity, 0, 2),
		.move = move,
		.timeout = timeout,
		.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
		.angle = angle,
		.layer = LAYER_PARTICLE_HIGH,
	);
}

static Projectile *spawn_charge_particle(cmplx target, real dist, const Color *clr, real power) {
	cmplx pos = target + rng_dir() * dist;
	MoveParams move = move_towards(0, target, rng_range(0.1, 0.2) + 0.05 * power);
	move.retention = 0.25 * cdir(1.5 * rng_sign());

	spawn_charge_particle_smoke(target, power);

	return PARTICLE(
		.sprite = "graze",
		.color = clr,
		.pos = pos,
		.draw_rule = pdraw_timeout_scalefade(2, 0.05, 0, 1),
		.move = move,
		.timeout = rng_irange(25, 35),
		.flags = PFLAG_NOREFLECT | PFLAG_REQUIREDPARTICLE,
		.layer = LAYER_PARTICLE_HIGH,
	);
}

static void randomize_hue(Color *clr, float r) {
	float h, s, l, a = clr->a;
	float m = max(clr->r, max(clr->g, clr->b));

	if(UNLIKELY(m == 0)) {
		return;
	}

	color_div_scalar(clr, m);
	color_get_hsl(clr, &h, &s, &l);
	h += rng_f32s() * r;
	*clr = *HSLA(h, s, l, a);
	color_mul_scalar(clr, m);
}

TASK(charge_sound_stopper, { SFXPlayID id; }) {
	stop_sfx(ARGS.id);
}

static int common_charge_impl(
	int time,
	const cmplx *anchor,
	cmplx offset,
	const Color *color,
	const char *snd_charge,
	const char *snd_discharge
) {
	real dist = 256;
	int delay = 3;
	real rayfactor = 1.0 / time;
	float hue_rand = 0.02;
	SFXPlayID charge_snd_id = snd_charge ? play_sfx(snd_charge) : 0;
	DECLARE_ENT_ARRAY(Projectile, particles, 256);

	BoxedTask snd_stopper_task = { 0 };

	if(charge_snd_id) {
		snd_stopper_task = cotask_box(INVOKE_TASK_AFTER(&TASK_EVENTS(THIS_TASK)->finished, charge_sound_stopper, charge_snd_id));
	}

	int wait_time = 0;

	for(int i = 0; i < time; ++i, wait_time += WAIT(1)) {
		cmplx pos = *anchor + offset;

		ENT_ARRAY_COMPACT(&particles);
		ENT_ARRAY_FOREACH(&particles, Projectile *p, {
			p->move.attraction_point = pos;
		});

		if(i % delay) {
			continue;
		}

		int stage = (5 * i) / time;
		int nparts = stage + 1;
		real power = (stage + 1) / 6.0;
		real sdist = dist * glm_ease_quad_out(power);

		for(int j = 0; j < nparts; ++j) {
			Color c = *color;
			randomize_hue(&c, hue_rand);
			color_lerp(&c, RGBA(1, 1, 1, 0), rng_real() * 0.2);
			Projectile *p = spawn_charge_particle(pos, sdist * (1 + 0.1 * rng_sreal()), &c, power);
			ENT_ARRAY_ADD(&particles, p);
			color_mul_scalar(&c, 0.2);
		}

		Color c = *color;
		randomize_hue(&c, hue_rand);
		color_lerp(&c, RGBA(1, 1, 1, 0), rng_real() * 0.2);
		color_mul_scalar(&c, 0.5);

		ENT_ARRAY_ADD(&particles, PARTICLE(
			.sprite = "blast_huge_rays",
			.color = &c,
			.pos = pos,
			.draw_rule = pdraw_timeout_scalefade(0, 1, 1, 0),
			.move = move_towards(0, pos, 0.1),
			.timeout = 30,
			.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
			.scale = glm_ease_bounce_out(rayfactor * (i + 1)),
			.angle = rng_angle(),
			.layer = LAYER_PARTICLE_HIGH,
		));
	}

	CANCEL_TASK(snd_stopper_task);

	if(snd_discharge) {
		replace_sfx(charge_snd_id, snd_discharge);
	} else {
		stop_sfx(charge_snd_id);
	}

	Color c = *color;
	randomize_hue(&c, hue_rand);
	color_mul_scalar(&c, 2.0);

	cmplx pos = *anchor + offset;

	PARTICLE(
		.sprite = "blast_huge_halo",
		.color = &c,
		.pos = pos,
		.draw_rule = pdraw_timeout_scalefade(0, 2, 1, 0),
		.timeout = 30,
		.flags = PFLAG_NOREFLECT,
		.angle = rng_angle(),
		.layer = LAYER_PARTICLE_HIGH,
	);

	ENT_ARRAY_FOREACH(&particles, Projectile *p, {
		if(!(p->flags & PFLAG_MANUALANGLE)) {
			p->move.attraction = 0;
			p->move.acceleration += cnormalize(p->pos - pos);
			p->move.retention = 0.8;
		}
	});

	assert(time == wait_time);
	return wait_time;
}

int common_charge(int time, const cmplx *anchor, cmplx offset, const Color *color) {
	return common_charge_impl(time, anchor, offset, color, COMMON_CHARGE_SOUND_CHARGE, COMMON_CHARGE_SOUND_DISCHARGE);
}

int common_charge_static(int time, cmplx pos, const Color *color) {
	cmplx anchor = 0;
	return common_charge_impl(time, &anchor, pos, color, COMMON_CHARGE_SOUND_CHARGE, COMMON_CHARGE_SOUND_DISCHARGE);
}

int common_charge_custom(
	int time,
	const cmplx *anchor,
	cmplx offset,
	const Color *color,
	const char *snd_charge,
	const char *snd_discharge
) {
	cmplx local_anchor = 0;
	anchor = anchor ? anchor : &local_anchor;
	return common_charge_impl(
		time,
		anchor,
		offset,
		color,
		snd_charge,
		snd_discharge
	);
}

DEFINE_EXTERN_TASK(common_charge) {
	Color local_color;
	const Color *p_color = ARGS.color_ref;
	if(!p_color) {
		local_color = *NOT_NULL(ARGS.color);
		p_color = &local_color;
	}

	if(ARGS.bind_to_entity.ent) {
		TASK_BIND(ARGS.bind_to_entity);
	}

	common_charge_custom(
		ARGS.time,
		ARGS.anchor,
		ARGS.pos,
		p_color,
		ARGS.sound.charge,
		ARGS.sound.discharge
	);
}

DEFINE_EXTERN_TASK(common_set_bitflags) {
	assume(ARGS.pflags != NULL);
	*ARGS.pflags = ((*ARGS.pflags & ARGS.mask) | ARGS.set);
}

DEFINE_EXTERN_TASK(common_kill_projectile) {
	kill_projectile(TASK_BIND(ARGS.proj));
}

DEFINE_EXTERN_TASK(common_kill_enemy) {
	enemy_kill(TASK_BIND(ARGS.enemy));
}

cmplx common_wander(cmplx origin, double dist, Rect bounds) {
	int attempts = 32;
	double angle = 0;
	cmplx dest = 0;
	cmplx dir = 0;

	// assert(point_in_rect(origin, bounds));

	while(attempts--) {
		angle = rng_angle();
		dir = cdir(angle);
		dest = origin + dist * dir;

		if(point_in_rect(dest, bounds)) {
			return dest;
		}
	}

	log_warn("Clipping fallback  origin = %f%+fi  dist = %f  bounds.top_left = %f%+fi  bounds.bottom_right = %f%+fi",
		re(origin), im(origin),
		dist,
		re(bounds.top_left), im(bounds.top_left),
		re(bounds.bottom_right), im(bounds.bottom_right)
	);

	// TODO: implement proper line-clipping here?

	double step = cabs(bounds.bottom_right - bounds.top_left) / 16;
	dir *= step;
	dest = origin;

	while(point_in_rect(dest + dir, bounds)) {
		dest += dir;
	}

	return dest;
}

DEFINE_EXTERN_TASK(common_easing_animate) {
	float from = *ARGS.value;
	float scale = ARGS.to - from;
	float ftime = ARGS.duration;

	for(int t = 1; t <= ARGS.duration; t++) {
		YIELD;
		*ARGS.value = from + scale * ARGS.ease(t / ftime);
    }
}

DEFINE_EXTERN_TASK(common_easing_animate_vec3) {
	vec3 from;
	glm_vec3_copy(*ARGS.value, from);

	vec3 scale;
	glm_vec3_sub(ARGS.to, from, scale);

	float ftime = ARGS.duration;

	for(int t = 1; t <= ARGS.duration; t++) {
		YIELD;

		float f = ARGS.ease(t / ftime);
		vec3 d;
		glm_vec3_scale(scale, f, d);
		glm_vec3_add(from, d, *ARGS.value);
	}
}

DEFINE_EXTERN_TASK(common_easing_animate_vec4) {
	vec4 from;
	glm_vec4_copy(*ARGS.value, from);

	vec4 scale;
	glm_vec4_sub((float*)ARGS.to, from, scale);

	float ftime = ARGS.duration;

	for(int t = 1; t <= ARGS.duration; t++) {
		YIELD;

		float f = ARGS.ease(t / ftime);
		vec4 d;
		glm_vec4_scale(scale, f, d);
		glm_vec4_add(from, d, *ARGS.value);
	}
}

void common_rotate_velocity(MoveParams *move, real angle, int duration) {
	cmplx r = cdir(angle / duration);
	move->retention *= r;
	WAIT(duration);
	move->retention /= r;
}

DEFINE_EXTERN_TASK(common_rotate_velocity) {
	common_rotate_velocity(ARGS.move, ARGS.angle, ARGS.duration);
}

DEFINE_EXTERN_TASK(common_easing_animated) {
	double from = *ARGS.value;
	double scale = ARGS.to - from;
	double ftime = ARGS.duration;

	for(int t = 1; t <= ARGS.duration; t++) {
		YIELD;
		*ARGS.value = from + scale * ARGS.ease(t / ftime);
    }
}

DEFINE_EXTERN_TASK(common_play_sfx) {
	play_sfx(ARGS.name);
}
