/*******************************************************************************************
*
*   raylib [core] example - 2d camera platformer
*
*   Example complexity rating: [★★★☆] 3/4
*
*   Example originally created with raylib 2.5, last time updated with raylib 3.0
*
*   Example contributed by arvyy (@arvyy) and reviewed by Ramon Santamaria (@raysan5)
*
*   Example licensed under an unmodified zlib/libpng license, which is an OSI-certified,
*   BSD-like license that allows static linking with closed source software
*
*   Copyright (c) 2019-2025 arvyy (@arvyy)
*   converted to C3 by Christoffer Lerno
*
********************************************************************************************/

module raylib_camera_platformer;
import raylib55;

const G = 400;
const float PLAYER_JUMP_SPD = 350;
const float PLAYER_HOR_SPD = 200;

//----------------------------------------------------------------------------------
// Types and Structures Definition
//----------------------------------------------------------------------------------
struct Player
{
	RLVector2 position;
	float speed;
	bool can_jump;
}

struct EnvItem
{
	RLRectangle rect;
	int blocking;
	RLColor color;
}


alias CameraUpdateFn = fn void(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height);

enum CameraUpdateType : (ZString text, CameraUpdateFn func)
{
	CENTER               = { "Follow player center", &update_camera_center },
	CENTER_INSIDE_MAP    = { "Follow player center, but clamp to map edges", &update_camera_center_inside_map },
	CENTER_SMOOTH_FOLLOW = { "Follow player center; smoothed", &update_camera_center_smooth_follow },
	EVEN_OUT_ON_LANDING  = { "Follow player center horizontally; update player center vertically after landing", &update_camera_even_out_on_landing },
	PLAYER_BOUNDS_PUSH   = { "Player push camera on getting too close to screen edge", &update_camera_player_bounds_push }
}

//------------------------------------------------------------------------------------
// Program main entry point
//------------------------------------------------------------------------------------
fn int main()
{
	// Initialization
	//--------------------------------------------------------------------------------------
	const int SCREEN_WIDTH = 800;
	const int SCREEN_HEIGHT = 450;

	rl::init_window(SCREEN_WIDTH, SCREEN_HEIGHT, "raylib [core] example - 2d camera platformer");

	Player player = {
		.position = { 400, 280 },
		.speed = 0,
		.can_jump = false
	};
	EnvItem[*] env_items = {
		{{ 0, 0, 1000, 400 },   0, rl::LIGHTGRAY },
		{{ 0, 400, 1000, 200 }, 1, rl::GRAY },
		{{ 300, 200, 400, 10 }, 1, rl::GRAY },
		{{ 250, 300, 100, 10 }, 1, rl::GRAY },
		{{ 650, 300, 100, 10 }, 1, rl::GRAY }
	};

	RLCamera2D camera = {
		.target = player.position,
		.offset = { SCREEN_WIDTH / 2.0f, SCREEN_HEIGHT / 2.0f },
		.rotation = 0.0f,
		.zoom = 1.0f
	};


	CameraUpdateType camera_option = CENTER;

	rl::set_target_fps(60);
	//--------------------------------------------------------------------------------------

	// Main game loop
	while (!rl::window_should_close())
	{
		// Update
		//----------------------------------------------------------------------------------
		float delta_time = rl::get_frame_time();

		update_player(&player, &env_items, delta_time);
		camera.zoom += ((float)rl::get_mouse_wheel_move() * 0.05f);

		switch
		{
			case camera.zoom > 3.0f: camera.zoom = 3.0f;
			case camera.zoom < 0.25f: camera.zoom = 0.25f;
		}

		if (rl::is_key_pressed(R))
		{
			camera.zoom = 1.0f;
			player.position = { 400, 280 };
		}

		if (rl::is_key_pressed(C)) camera_option = CameraUpdateType.from_ordinal(((int)camera_option + 1) % (int)CameraUpdateType.len);

		// Call update camera function by its pointer
		camera_option.func(&camera, &player, &env_items, delta_time, SCREEN_WIDTH, SCREEN_HEIGHT);
		//----------------------------------------------------------------------------------

		// Draw
		//----------------------------------------------------------------------------------
		rl::@drawing()
		{
			rl::clear_background(rl::LIGHTGRAY);
			rl::@mode2d(camera)
			{
				foreach (item : env_items) rl::draw_rectangle_rec(item.rect, item.color);
				RLRectangle player_rect = { player.position.x - 20, player.position.y - 40, 40.0f, 40.0f };
				rl::draw_rectangle_rec(player_rect, rl::RED);
				rl::draw_circle_v(player.position, 5.0f, rl::GOLD);
			};
			rl::draw_text("Controls:", 20, 20, 10, rl::BLACK);
			rl::draw_text("- Right/Left to move", 40, 40, 10, rl::DARKGRAY);
			rl::draw_text("- Space to jump", 40, 60, 10, rl::DARKGRAY);
			rl::draw_text("- Mouse Wheel to Zoom in-out, R to reset zoom", 40, 80, 10, rl::DARKGRAY);
			rl::draw_text("- C to change camera mode", 40, 100, 10, rl::DARKGRAY);
			rl::draw_text("Current camera mode:", 20, 120, 10, rl::BLACK);
			rl::draw_text(camera_option.text, 40, 140, 10, rl::DARKGRAY);
		};
		//----------------------------------------------------------------------------------
	}

	// De-Initialization
	//--------------------------------------------------------------------------------------
	rl::close_window();        // Close window and OpenGL context
	//--------------------------------------------------------------------------------------

	return 0;
}
fn void update_player(Player* player, EnvItem[] env_items, float delta)
{
	if (rl::is_key_down(LEFT)) player.position.x -= PLAYER_HOR_SPD * delta;
	if (rl::is_key_down(RIGHT)) player.position.x += PLAYER_HOR_SPD * delta;
	if (rl::is_key_down(SPACE) && player.can_jump)
	{
		player.speed = -PLAYER_JUMP_SPD;
		player.can_jump = false;
	}

	bool hit_obstacle = false;
	foreach (item : env_items)
	{
		RLVector2* p = &player.position;
		if (item.blocking
			&& item.rect.x <= p.x
			&& item.rect.x + item.rect.width >= p.x
			&& item.rect.y >= p.y
			&& item.rect.y <= p.y + player.speed * delta)
		{
			hit_obstacle = true;
			player.speed = 0.0f;
			p.y = item.rect.y;
			break;
		}
	}

	if (hit_obstacle)
	{
		player.can_jump = true;
	}
	else
	{
		player.position.y += player.speed * delta;
		player.speed += G * delta;
		player.can_jump = false;
	}
}

fn void update_camera_center(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height)
{
	camera.offset = { width / 2.0f, height / 2.0f };
	camera.target = player.position;
}

fn void update_camera_center_inside_map(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height)
{
	camera.target = player.position;
	camera.offset = { width / 2.0f, height / 2.0f };
	float min_x = 1000;
	float min_y = 1000;
	float max_x = -1000;
	float max_y = -1000;

	foreach (item : env_items)
	{
		min_x = min(item.rect.x, min_x);
		max_x = max(item.rect.x + item.rect.width, max_x);
		min_y = min(item.rect.y, min_y);
		max_y = max(item.rect.y + item.rect.height, max_y);
	}

	RLVector2 max = rl::get_world_to_screen2d({ max_x, max_y }, *camera);
	RLVector2 min = rl::get_world_to_screen2d({ min_x, min_y }, *camera);

	if (max.x < width) camera.offset.x = width - (max.x - width / 2.0f);
	if (max.y < height) camera.offset.y = height - (max.y - height / 2.0f);
	if (min.x > 0) camera.offset.x = width / 2.0f - min.x;
	if (min.y > 0) camera.offset.y = height / 2.0f - min.y;
}

fn void update_camera_center_smooth_follow(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height)
{
	const float MIN_SPEED = 30;
	const float MIN_EFFECT_LENGTH = 10;
	const float FRACTION_SPEED = 0.8f;

	camera.offset = { width / 2.0f, height / 2.0f };
	float length = player.position.distance(camera.target);

	if (length > MIN_EFFECT_LENGTH)
	{
		float speed = max(FRACTION_SPEED * length, MIN_SPEED);
		camera.target += (player.position - camera.target) * speed * delta / length;
	}
}

fn void update_camera_even_out_on_landing(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height)
{
	const float EVEN_OUT_SPEED = 700;
	static bool evening_out = false;
	static float even_out_target;

	camera.offset = { width / 2.0f, height / 2.0f };
	camera.target.x = player.position.x;

	if (evening_out)
	{
		if (even_out_target > camera.target.y)
		{
			camera.target.y += EVEN_OUT_SPEED * delta;

			if (camera.target.y > even_out_target)
			{
				camera.target.y = even_out_target;
				evening_out = false;
			}
		}
		else
		{
			camera.target.y -= EVEN_OUT_SPEED * delta;

			if (camera.target.y < even_out_target)
			{
				camera.target.y = even_out_target;
				evening_out = false;
			}
		}
	}
	else
	{
		if (player.can_jump && player.speed == 0 && player.position.y != camera.target.y)
		{
			evening_out = true;
			even_out_target = player.position.y;
		}
	}
}

fn void update_camera_player_bounds_push(RLCamera2D* camera, Player* player, EnvItem[] env_items, float delta, int width, int height)
{
	const RLVector2 BBOX = { 0.2f, 0.2f };

	RLVector2 bboxWorldMin = rl::get_screen_to_world2d({ (1 - BBOX.x) * 0.5f * width, (1 - BBOX.y) * 0.5f*height }, *camera);
	RLVector2 bboxWorldMax = rl::get_screen_to_world2d({ (1 + BBOX.x) * 0.5f * width, (1 + BBOX.y) * 0.5f*height }, *camera);
	camera.offset = { (1 - BBOX.x) * 0.5f * width, (1 - BBOX.y) * 0.5f * height };

	if (player.position.x < bboxWorldMin.x) camera.target.x = player.position.x;
	if (player.position.y < bboxWorldMin.y) camera.target.y = player.position.y;
	if (player.position.x > bboxWorldMax.x) camera.target.x = bboxWorldMin.x + (player.position.x - bboxWorldMax.x);
	if (player.position.y > bboxWorldMax.y) camera.target.y = bboxWorldMin.y + (player.position.y - bboxWorldMax.y);
}