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

#include "taisei.h"

#include "camcontrol.h"

#include "stagetext.h"
#include "coroutine.h"
#include "util/glm.h"
#include "video.h"
#include "entity.h"

#define CAMCTRL_MOVE_SPEED      0.1
// for scroll wheel move speed adjustment
#define CAMCTRL_MOVE_SPEED_STEP (CAMCTRL_MOVE_SPEED * 0.25)

#define CAMCTROL_MOUSE_SENS     0.1

#define CAMCTRL_PITCH_SPEED     -CAMCTROL_MOUSE_SENS
#define CAMCTRL_YAW_SPEED       -CAMCTROL_MOUSE_SENS
#define CAMCTRL_ROLL_SPEED      1.0

#define CAMCTRL_PITCH_AXIS      0
#define CAMCTRL_YAW_AXIS        2
#define CAMCTRL_ROLL_AXIS       1

#define CAMCTRL_KEY_FORWARD     SDL_SCANCODE_W
#define CAMCTRL_KEY_BACK        SDL_SCANCODE_S
#define CAMCTRL_KEY_LEFT        SDL_SCANCODE_A
#define CAMCTRL_KEY_RIGHT       SDL_SCANCODE_D
#define CAMCTRL_KEY_UP          SDL_SCANCODE_R
#define CAMCTRL_KEY_DOWN        SDL_SCANCODE_F

// FIXME doesn't work that well with mouse-look…
#define CAMCTRL_KEY_ROLL_LEFT   SDL_SCANCODE_Q
#define CAMCTRL_KEY_ROLL_RIGHT  SDL_SCANCODE_E

#define CAMCTRL_KEY_TOGGLE_GRAB SDL_SCANCODE_F8

struct cc_state {
	SDL_Window *w;
	bool game_paused;
	bool grab_enabled;
};

INLINE float normangle(float a) {
	return a - 360.0f * floorf((a + 180.0f) / 360.0f);
}

INLINE void addangle(float *a, float d) {
	*a = normangle(*a + d);
}

static void set_mouse_grab(struct cc_state *s, bool enable) {
	enable = enable && s->grab_enabled && !s->game_paused;

	SDL_SetWindowGrab(s->w, enable);
	SDL_CaptureMouse(enable);
	SDL_SetRelativeMouseMode(enable);
	SDL_EventState(SDL_MOUSEMOTION, enable);
	SDL_EventState(SDL_MOUSEWHEEL, enable);
}

static bool keydown_event(SDL_Event *e, void *a) {
	struct cc_state *s = a;

	if(e->key.repeat) {
		return false;
	}

	if(e->key.keysym.scancode == CAMCTRL_KEY_TOGGLE_GRAB) {
		s->grab_enabled = !s->grab_enabled;
		set_mouse_grab(s, s->grab_enabled);
	}

	return false;
}

static bool mouse_event(SDL_Event *e, void *a) {
	Camera3D *cam = a;

	float *pitch = cam->rot.v + CAMCTRL_PITCH_AXIS;
	float *yaw = cam->rot.v + CAMCTRL_YAW_AXIS;

	addangle(pitch, CAMCTRL_PITCH_SPEED * e->motion.yrel);
	addangle(yaw,   CAMCTRL_YAW_SPEED   * e->motion.xrel);

	return false;
}

static bool wheel_event(SDL_Event *e, void *a) {
	float delta = CAMCTRL_MOVE_SPEED_STEP;
	float *move_speed = a;
	*move_speed += e->wheel.y * delta;
	return false;
}

static bool focus_gained_event(SDL_Event *e, void *a) {
	struct cc_state *s = a;
	set_mouse_grab(s, true);
	return false;
}

static bool focus_lost_event(SDL_Event *e, void *a) {
	struct cc_state *s = a;
	set_mouse_grab(s, false);
	return false;
}

static bool pause_event(SDL_Event *e, void *a) {
	struct cc_state *s = a;
	s->game_paused = e->user.code;
	set_mouse_grab(s, !s->game_paused);
	return false;
}

static void nodraw(EntityInterface *ent) { }

static void predraw(EntityInterface *ent, void *a) {
	if(ent) {
		// super lame hack to hide everything
		ent->draw_func = nodraw;
	}
}

TASK(cleanup, { struct cc_state *s; }) {
	ent_unhook_pre_draw(predraw);
	events_unregister_handler(keydown_event);
	events_unregister_handler(mouse_event);
	events_unregister_handler(wheel_event);
	events_unregister_handler(focus_gained_event);
	events_unregister_handler(focus_lost_event);
	events_unregister_handler(pause_event);
	set_mouse_grab(ARGS.s, false);
}

TASK(camera_control, { Camera3D *cam; }) {
	Camera3D *cam = ARGS.cam;

	struct cc_state estate = { };
	estate.w = NOT_NULL(video_get_window());
	estate.grab_enabled = true;
	set_mouse_grab(&estate, true);
	INVOKE_TASK_AFTER(&TASK_EVENTS(THIS_TASK)->finished, cleanup, &estate);

	float move_speed = CAMCTRL_MOVE_SPEED;

	events_register_handler(
		&(EventHandler) {
			.proc = mouse_event,
			.arg = cam,
			.event_type = SDL_MOUSEMOTION,
		}
	);

	events_register_handler(
		&(EventHandler) {
			.proc = wheel_event,
			.arg = &move_speed,
			.event_type = SDL_MOUSEWHEEL,
		}
	);

	events_register_handler(
		&(EventHandler) {
			.proc = keydown_event,
			.arg = &estate,
			.event_type = SDL_KEYDOWN,
		}
	);

	events_register_handler(
		&(EventHandler) {
			.proc = focus_gained_event,
			.arg = &estate,
			.event_type = SDL_WINDOWEVENT_FOCUS_GAINED,
		}
	);

	events_register_handler(
		&(EventHandler) {
			.proc = focus_lost_event,
			.arg = &estate,
			.event_type = SDL_WINDOWEVENT_FOCUS_LOST,
		}
	);

	events_register_handler(
		&(EventHandler) {
			.proc = pause_event,
			.arg = &estate,
			.event_type = MAKE_TAISEI_EVENT(TE_GAME_PAUSE_STATE_CHANGED)
		}
	);

	ent_hook_pre_draw(predraw, NULL);

	Font *fnt = res_font("monotiny");
	StageText *txt = stagetext_add(
		NULL, I*font_get_lineskip(fnt)/2, ALIGN_LEFT, fnt, RGB(1,1,1), 0, 9000000, 0, 0
	);

	StageText *grabtxt = stagetext_add(
		"Mouse input disabled ",
		VIEWPORT_W + I*font_get_lineskip(fnt)/2, ALIGN_RIGHT, fnt, RGB(1,0.5,0), 0, 9000000, 0, 0
	);

	for(;;YIELD) {
		int numkeys = 0;
		const uint8_t *keys = SDL_GetKeyboardState(&numkeys);

		grabtxt->text[0] = estate.grab_enabled ? 0 : 'M';

		if(numkeys < 1) {
			continue;
		}

		float m = move_speed;

		vec3 forward = {  0,  0, -m };
		vec3 right   = {  m,  0,  0 };
		vec3 up      = {  0,  m,  0 };

		mat4 camera_trans;
		glm_mat4_identity(camera_trans);
		camera3d_apply_transforms(cam, camera_trans);
		glm_mat4_inv_precise(camera_trans, camera_trans);

		glm_vec3_rotate_m4(camera_trans, forward, forward);
		glm_vec3_rotate_m4(camera_trans, right, right);
		glm_vec3_rotate_m4(camera_trans, up, up);

		if(keys[CAMCTRL_KEY_FORWARD]) glm_vec3_add(cam->pos, forward, cam->pos);
		if(keys[CAMCTRL_KEY_BACK])    glm_vec3_sub(cam->pos, forward, cam->pos);
		if(keys[CAMCTRL_KEY_RIGHT])   glm_vec3_add(cam->pos, right,   cam->pos);
		if(keys[CAMCTRL_KEY_LEFT])    glm_vec3_sub(cam->pos, right,   cam->pos);
		if(keys[CAMCTRL_KEY_UP])      glm_vec3_add(cam->pos, up,      cam->pos);
		if(keys[CAMCTRL_KEY_DOWN])    glm_vec3_sub(cam->pos, up,      cam->pos);

		float *roll = cam->rot.v + CAMCTRL_ROLL_AXIS;
		if(keys[CAMCTRL_KEY_ROLL_LEFT])  addangle(roll, -CAMCTRL_ROLL_SPEED);
		if(keys[CAMCTRL_KEY_ROLL_RIGHT]) addangle(roll,  CAMCTRL_ROLL_SPEED);

		snprintf(txt->text, sizeof(txt->text),
			"% 9.03f % 9.03f % 9.03f\n% 9.03f % 9.03f % 9.03f",
			cam->pos[0],   cam->pos[1],   cam->pos[2],
			cam->rot.v[0], cam->rot.v[1], cam->rot.v[2]
		);
	}
}

void camcontrol_init(Camera3D *cam) {
	INVOKE_TASK(camera_control, cam);
}
