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

#include "audio/audio.h"
#include "common_tasks.h"
#include "dialog/marisa.h"
#include "global.h"
#include "plrmodes.h"
#include "renderer/api.h"
#include "stagedraw.h"
#include "util/glm.h"

#define SHOT_FORWARD_DAMAGE 60
#define SHOT_FORWARD_DELAY 6

#define SHOT_SLAVE_DAMAGE 50
#define SHOT_SLAVE_DELAY 5

#define BOMB_NUM_ORBITERS 5

#define HAKKERO_RETRACT_TIME 6

typedef struct MarisaBController {
	struct {
		Sprite *fairy_circle;
		Sprite *lightningball;
		Sprite *maristar_orbit;
		Sprite *stardust;
	} sprites;

	Player *plr;
	cmplx slave_ref_pos;  // follows player

	struct {
		float beams_alpha;
	} bomb;

	COEVENTS_ARRAY(
		slaves_expired
	) events;
} MarisaBController;

DEFINE_ENTITY_TYPE(MarisaBSlave, {
	Sprite *sprite;
	ShaderProgram *shader;
	cmplx pos;
	uint alive;
});

DEFINE_ENTITY_TYPE(MarisaBOrbiter, {
	MarisaBController *ctrl;
	Sprite *sprite;
	cmplx pos;
	cmplx offset;
	Color particle_color;
	Color circle_color;
});

DEFINE_ENTITY_TYPE(MarisaBBeams, {
	BoxedMarisaBOrbiter orbiters[BOMB_NUM_ORBITERS];
	int time;
	float alpha;
});

static void marisa_star_draw_slave(EntityInterface *ent) {
	MarisaBSlave *slave = ENT_CAST(ent, MarisaBSlave);

	ShaderCustomParams shader_params = { 0 };
	// shader_params.color = *RGBA(0.2, 0.4, 0.5, slave->flare_alpha * 0.75);
	float t = global.frames;

	r_draw_sprite(&(SpriteParams) {
		.sprite_ptr = slave->sprite,
		.shader_ptr = slave->shader,
		.pos.as_cmplx = slave->pos,
		.rotation.angle = t * 0.05f,
		// .color = color_lerp(RGB(0.2, 0.4, 0.5), RGB(1.0, 1.0, 1.0), 0.25 * powf(psinf(t / 6.0f), 2.0f) * slave->flare_alpha),
		.color = RGB(0.2, 0.4, 0.5),
		.shader_params = &shader_params,
	});
}

static Color *marisa_star_slave_projectile_color(Color *c, real focus, real brightener) {
	static const Color unfocused = { 0.3, 0.8, 1.0, 0.2 };
	static const Color focused   = { 1.0, 0.8, 0.3, 0.2 };
	*c = unfocused;
	color_lerp(c, &focused, focus);
	return color_add(c, RGBA(brightener, brightener, brightener, brightener));
}

TASK(marisa_star_slave_projectile, {
	MarisaBController *ctrl;
	BoxedMarisaBSlave slave;
	cmplx pos;
	cmplx vel;
	real damage;
	ShaderProgram *shader;
}) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	MarisaBSlave *slave = NOT_NULL(ENT_UNBOX(ARGS.slave));

	if(!slave->alive || !player_should_shoot(plr)) {
		return;
	}

	Projectile *p = TASK_BIND(PROJECTILE(
		.proto = pp_maristar,
		.pos = ARGS.pos,
		.move = move_linear(ARGS.vel),
		.type = PROJ_PLAYER,
		.damage = ARGS.damage,
		.shader_ptr = ARGS.shader,
		.max_viewport_dist = 128,
	));

	real freq = 0.1;
	cmplx vel = ARGS.vel;
	real focus = !!(plr->inputflags & INFLAG_FOCUS);

	cmplx prev_pos = p->pos;
	cmplx next_pos = p->pos;

	for(int t = 0;; ++t) {
		slave = ENT_UNBOX(ARGS.slave);

		if(slave == NULL || !slave->alive || !player_should_shoot(plr)) {
			break;
		}

		approach_asymptotic_p(&focus, !!(plr->inputflags & INFLAG_FOCUS), 0.2, 1e-3);

		real focusfac = 1;

		if(focus < 1) {
			focusfac = t * 0.015 - 1 / (1 - focus);
			focusfac = tanh(sqrt(fabs(focusfac)));
		}

		cmplx center = clerp(plr->pos, ctrl->slave_ref_pos, tanh(t / 10.0));

		real brightener = -1 / (1 + sqrt(0.03 * fabs(re(p->pos - center))));
		marisa_star_slave_projectile_color(&p->color, focus, brightener);

		real verticalfac = - 5 * t * (1 + 0.01 * t) + 10 * t / (0.01 * t + 1);

		prev_pos = p->pos;
		next_pos = center + focusfac * cbrt(0.1 * t) * re(vel) * 70 * sin(freq * t + im(vel)) + verticalfac*I;
		p->move.velocity = next_pos - prev_pos;

		if(t%(2+(int)round(2*rng_real())) == 0) {  // please never write stuff like this ever again
			PARTICLE(
				.sprite_ptr = ctrl->sprites.stardust,
				.pos = next_pos,
				.color = RGBA(0.5 * focus, 0, 0.5 * (1 - focus), 0),
				.timeout = 5,
				.angle = rng_angle(),
				.angle_delta = 0.1 * rng_sreal(),
				.draw_rule = pdraw_timeout_scalefade(0, 1.4, 1, 0),
				.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
			);
		}

		YIELD;
	}

	p->move.retention = 1.005;
}

TASK(marisa_star_slave_shot, {
	MarisaBController *ctrl;
	BoxedMarisaBSlave slave;
	real phase;
}) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	MarisaBSlave *slave = TASK_BIND(ARGS.slave);

	ShaderProgram *shot_shader = res_shader("sprite_particle");
	real nphase = ARGS.phase / M_TAU;
	real damage = SHOT_SLAVE_DAMAGE;

	int t = 0;
	for(;;) {
		t += WAIT_EVENT_OR_DIE(&plr->events.shoot).frames;

		for(int i = 0; i < 2; ++i) {
			cmplx v = (1 - 2 * i);
			v *= 1 - 0.9 * nphase;
			v -= I * 0.04 * t * (4 - 3 * nphase);

			INVOKE_TASK(marisa_star_slave_projectile,
				.ctrl = ctrl,
				.slave = ENT_BOX(slave),
				.pos = slave->pos,
				.vel = v,
				.damage = damage,
				.shader = shot_shader
			);

			t += WAIT(2);
		}

		static_assert(SHOT_SLAVE_DELAY >= 4, "SHOT_SLAVE_DELAY is too low");
		t += WAIT(SHOT_SLAVE_DELAY - 4);
	}
}

TASK(marisa_star_slave, {
	MarisaBController *ctrl;
	real phase;
}) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	MarisaBSlave *slave = TASK_HOST_ENT(MarisaBSlave);
	slave->alive = 1;
	slave->pos = plr->pos;
	slave->shader = res_shader("sprite_hakkero");
	slave->sprite = res_sprite("hakkero");

	slave->ent.draw_func = marisa_star_draw_slave;
	slave->ent.draw_layer = LAYER_PLAYER_SLAVE;

	INVOKE_SUBTASK_WHEN(&ctrl->events.slaves_expired, common_set_bitflags,
		.pflags = &slave->alive,
		.mask = 0,
		.set = 0
	);

	BoxedTask shot_task = cotask_box(INVOKE_SUBTASK(marisa_star_slave_shot,
		.ctrl = ctrl,
		.slave = ENT_BOX(slave),
		.phase = ARGS.phase
	));

	real angle_step = 0.05;
	real angle = angle_step * global.frames + ARGS.phase;

	for(int t = 0; slave->alive; t += WAIT(1)) {
		cmplx target_pos = ctrl->slave_ref_pos + 80 * sin(angle) + 45*I;
		slave->pos = clerp(plr->pos, target_pos, glm_ease_quad_out(min(1, (real)t/HAKKERO_RETRACT_TIME)));
		slave->ent.draw_layer = cos(angle) < 0 ? LAYER_BACKGROUND : LAYER_PLAYER_SLAVE;
		angle += angle_step;
	}

	CANCEL_TASK(shot_task);
	plrutil_slave_retract(ENT_BOX(plr), &slave->pos, HAKKERO_RETRACT_TIME);
}

static void marisa_star_respawn_slaves(MarisaBController *ctrl, int numslaves) {
	Player *plr = ctrl->plr;
	coevent_signal(&ctrl->events.slaves_expired);
	ctrl->slave_ref_pos = plr->pos;

	for(int i = 0; i < numslaves; i++) {
		INVOKE_TASK(marisa_star_slave,
			.ctrl = ctrl,
			.phase = M_TAU * i / numslaves
		);
	}
}

TASK(marisa_star_power_handler, { MarisaBController *ctrl; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	int old_power = player_get_effective_power(plr) / 100;

	marisa_star_respawn_slaves(ctrl, old_power);

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.effective_power_changed);
		int new_power = player_get_effective_power(plr) / 100;
		if(old_power != new_power) {
			marisa_star_respawn_slaves(ctrl, new_power);
			old_power = new_power;
		}
	}
}

static void marisa_star_draw_orbiter(EntityInterface *ent) {
	MarisaBOrbiter *orbiter = ENT_CAST(ent, MarisaBOrbiter);
	MarisaBController *ctrl = orbiter->ctrl;

	SpriteParams sp = { 0 };
	sp.pos.as_cmplx = orbiter->pos;
	sp.color = color_mul_scalar(COLOR_COPY(&orbiter->circle_color), ctrl->bomb.beams_alpha);
	sp.rotation.angle = global.frames * 10 * DEG2RAD;
	sp.sprite_ptr = ctrl->sprites.fairy_circle;
	r_draw_sprite(&sp);
	sp.sprite_ptr = ctrl->sprites.lightningball;
	sp.scale.both = 0.6;
	r_draw_sprite(&sp);
}

static void marisa_star_draw_beams(EntityInterface *ent) {
	MarisaBBeams *beams_ent = ENT_CAST(ent, MarisaBBeams);
	MarisaBeamInfo beams[BOMB_NUM_ORBITERS], *pbeam = beams;

	float a = beams_ent->alpha;

	for(int i = 0; i < ARRAY_SIZE(beams_ent->orbiters); i++) {
		MarisaBOrbiter *orbiter = ENT_UNBOX(beams_ent->orbiters[i]);

		if(orbiter == NULL) {
			continue;
		}

		pbeam->origin = orbiter->pos;
		pbeam->size = 250 * a + VIEWPORT_H * 1.5 * I;
		pbeam->angle = carg(orbiter->offset) + M_PI/2;
		pbeam->t = beams_ent->time;
		++pbeam;
	}

	marisa_common_masterspark_draw(pbeam - beams, beams, a);
}

TASK(marisa_star_orbiter_stars, { MarisaBController *ctrl; BoxedMarisaBOrbiter orbiter; Color *color; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;
	MarisaBOrbiter *orbiter = TASK_BIND(ARGS.orbiter);

	for(int t = 0; t < 300; t += WAIT(10 - t / 30)) {
		cmplx vel = -5 * cnormalize(orbiter->pos - plr->pos);
		PARTICLE(
			.sprite_ptr = ctrl->sprites.maristar_orbit,
			.pos = orbiter->pos,
			.color = ARGS.color,
			.draw_rule = pdraw_timeout_scalefade(0, 6, 1, 0),
			.timeout = 150,
			.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
			.angle = t,
			.angle_delta = 0.1,
			.move = move_accelerated(vel, cnormalize(vel) * 0.15),
		);
	}
}

TASK(marisa_star_orbiter, { MarisaBController *ctrl; cmplx dir; real hue; BoxedMarisaBOrbiter *out_ref; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	MarisaBOrbiter *orbiter = TASK_HOST_ENT(MarisaBOrbiter);
	orbiter->ctrl = ctrl;
	orbiter->particle_color = *HSLA(ARGS.hue, 1, 0.6, 1);
	orbiter->circle_color = *HSLA(ARGS.hue, 0.9, 0.5, 0);
	orbiter->ent.draw_layer = LAYER_PLAYER_FOCUS - 1;
	orbiter->ent.draw_func = marisa_star_draw_orbiter;

	*ARGS.out_ref = ENT_BOX(orbiter);

	Color pcolor;

	BoxedTask stars_task = cotask_box(INVOKE_SUBTASK_DELAYED(1, marisa_star_orbiter_stars,
		.ctrl = ctrl,
		.orbiter = ENT_BOX(orbiter),
		.color = &pcolor
	));

	for(int t = 0;; t += WAIT(1)) {
		pcolor = orbiter->particle_color;

		real r = 100 * pow(tanh(t / 20.0), 2);
		orbiter->offset = ARGS.dir * r * cdir(sqrt(1000 + (t * t) * (1 + 0.03 * t)) * 0.04);
		orbiter->pos = plr->pos + orbiter->offset;

		real tb = player_get_bomb_progress(plr);
		real fadetime = 3.0 / 4.0;

		if(tb >= fadetime) {
			pcolor.a = 1 - (tb - fadetime) / (1 - fadetime);
			CANCEL_TASK(stars_task);
			stars_task = (BoxedTask) { 0 };
		}

		color_mul_alpha(&pcolor);
		pcolor.a = 0;

		PARTICLE(
			.sprite_ptr = ctrl->sprites.maristar_orbit,
			.pos = orbiter->pos,
			.color = color_mul_scalar(COLOR_COPY(&pcolor), 0.5),
			.timeout = 10,
			.angle = t * 0.1,
			.draw_rule = pdraw_timeout_scalefade(0, 1 + 4 * tb, 1, 0),
			.flags = PFLAG_NOREFLECT | PFLAG_MANUALANGLE,
		);
	}
}

TASK(marisa_star_bomb_background, { MarisaBController *ctrl; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	ShaderProgram *bg_shader = res_shader("maristar_bombbg");
	Uniform *u_t = r_shader_uniform(bg_shader, "t");
	Uniform *u_decay = r_shader_uniform(bg_shader, "decay");
	Uniform *u_plrpos = r_shader_uniform(bg_shader, "plrpos");
	Texture *bg_tex = res_texture("marisa_bombbg");

	for(;;) {
		WAIT_EVENT_OR_DIE(&stage_get_draw_events()->background_drawn);

		r_state_push();
		r_shader_ptr(bg_shader);
		r_uniform_float(u_t, player_get_bomb_progress(plr));
		r_uniform_float(u_decay, 1);
		r_uniform_vec2_complex(u_plrpos, cwmul(plr->pos, CMPLX(1.0/VIEWPORT_W, 1.0/VIEWPORT_H)));
		fill_viewport_p(0, 0, 1, 1, 0, bg_tex);
		r_state_pop();
	}
}

TASK(marisa_star_bomb, { MarisaBController *ctrl; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	INVOKE_SUBTASK(marisa_star_bomb_background, ctrl);

	YIELD;

	MarisaBBeams *beams = TASK_HOST_ENT(MarisaBBeams);
	beams->ent.draw_layer = LAYER_PLAYER_FOCUS - 1;
	beams->ent.draw_func = marisa_star_draw_beams;

	for(int i = 0; i < BOMB_NUM_ORBITERS; ++i) {
		INVOKE_SUBTASK(marisa_star_orbiter,
			.ctrl = ctrl,
			.dir = cdir(i * M_TAU / BOMB_NUM_ORBITERS),
			.hue = i / (real)BOMB_NUM_ORBITERS,
			.out_ref = beams->orbiters + i
		);
	}

	do {
		stage_shake_view(8);
		player_placeholder_bomb_logic(plr);

		float tb = player_get_bomb_progress(plr);
		float a = 1;

		if(tb < 1.0f / 6.0f) {
			a = tb * 6.0f;
			a = sqrtf(a);
		}

		if(tb > 3.0f / 4.0f) {
			a = 1.0f - tb * 4.0f + 3.0f;
			a *= a;
		}

		ctrl->bomb.beams_alpha = a;
		beams->alpha = a;
		++beams->time;
		YIELD;
	} while(player_is_bomb_active(plr));
}

TASK(marisa_star_bomb_handler, { MarisaBController *ctrl; }) {
	MarisaBController *ctrl = ARGS.ctrl;
	Player *plr = ctrl->plr;

	BoxedTask bomb_task = { 0 };

	for(;;) {
		WAIT_EVENT_OR_DIE(&plr->events.bomb_used);
		CANCEL_TASK(bomb_task);
		play_sfx("bomb_marisa_b");
		bomb_task = cotask_box(INVOKE_SUBTASK(marisa_star_bomb, ctrl));
	}
}

TASK(marisa_star_controller, { BoxedPlayer plr; }) {
	MarisaBController *ctrl = TASK_MALLOC(sizeof(*ctrl));
	ctrl->plr = TASK_BIND(ARGS.plr);
	TASK_HOST_EVENTS(ctrl->events);

	ctrl->sprites.fairy_circle = res_sprite("fairy_circle");
	ctrl->sprites.lightningball = res_sprite("part/lightningball");
	ctrl->sprites.maristar_orbit = res_sprite("part/maristar_orbit");
	ctrl->sprites.stardust = res_sprite("part/stardust");

	INVOKE_SUBTASK(marisa_star_power_handler, ctrl);
	INVOKE_SUBTASK(marisa_star_bomb_handler, ctrl);
	INVOKE_SUBTASK(marisa_common_shot_forward, ARGS.plr, SHOT_FORWARD_DAMAGE, SHOT_FORWARD_DELAY);

	ctrl->slave_ref_pos = ctrl->plr->pos;

	for(;;) {
		cmplx pdelta = ctrl->plr->pos - ctrl->slave_ref_pos;
		ctrl->slave_ref_pos += cclampabs(pdelta, 2 + 2 * !(ctrl->plr->inputflags & INFLAG_FOCUS));
		YIELD;
	}
}

static void marisa_star_init(Player *plr) {
	INVOKE_TASK(marisa_star_controller, { ENT_BOX(plr) });
}

static double marisa_star_property(Player *plr, PlrProperty prop) {
	switch(prop) {
		case PLR_PROP_SPEED: {
			double s = marisa_common_property(plr, prop);

			if(player_is_bomb_active(plr)) {
				s /= 4.0;
			}

			return s;
		}

		default:
			return marisa_common_property(plr, prop);
	}
}

static void marisa_star_preload(ResourceGroup *rg) {
	const int flags = RESF_DEFAULT;

	res_group_preload(rg, RES_SPRITE, flags,
		"hakkero",
		"masterspark_ring",
		"part/maristar_orbit",
		"part/stardust",
		"proj/marisa",
		"proj/maristar",
	NULL);

	res_group_preload(rg, RES_TEXTURE, flags,
		"marisa_bombbg",
	NULL);

	res_group_preload(rg, RES_SHADER_PROGRAM, flags,
		"masterspark",
		"maristar_bombbg",
		"sprite_hakkero",
	NULL);

	res_group_preload(rg, RES_SFX, flags | RESF_OPTIONAL,
		"bomb_marisa_b",
	NULL);
}

PlayerMode plrmode_marisa_b = {
	.name = "Stellar Vortex",
	.description = "As many bullets as there are stars in the sky. Some of them are bound to hit. That's called “homing”, right?",
	.spellcard_name = "Magic Sign “Stellar Vortex”",
	.character = &character_marisa,
	.dialog = &dialog_tasks_marisa,
	.shot_mode = PLR_SHOT_MARISA_STAR,
	.procs = {
		.init = marisa_star_init,
		.preload = marisa_star_preload,
		.property = marisa_star_property,
	},
};
