#include "lab/labv2_internal.h"
#include "lab/manager/lab_manager.h"
#include "lab/renderer/lab_renderer.h"
#include "io/key.h"
#include "math/staticrand.h"
#include "missionui/missionscreencommon.h"
#include "model/modelrender.h"
#include "object/object.h"
#include "object/objectdock.h"
#include "debris/debris.h"
#include "ship/ship.h"
#include "ship/shipfx.h"
#include "particle/particle.h"
#include "prop/prop.h"
#include "weapon/muzzleflash.h"
#include "weapon/beam.h"
#include "ai/aigoals.h"

#include "freespace.h"

#include "extensions/ImGuizmo.h"
#include "io/mouse.h"

//Turret firing forward declarations
void ai_turret_execute_behavior(const ship* shipp, ship_subsys* ss);
extern void beam_delete(beam* b);


void lab_exit() {
	getLabManager()->notify_close();
}

namespace ltp = lighting_profiles;
LabManager::LabManager() {
	The_mission.Reset();

	if (The_mission.ai_profile == nullptr)
		The_mission.ai_profile = &Ai_profiles[Default_ai_profile];

	Renderer.reset(new LabRenderer());
	labUi = LabUi();

	obj_init();

	fireball_init();
	debris_init();
	extern void debris_page_in();
	debris_page_in();
	props_level_init();
	asteroid_level_init();
	shockwave_level_init();
	ship_level_init();
	shipfx_flash_init();
	weapon_level_init();
	beam_level_init();
	particle::init();
	init_ai_system();

	ai_paused = 1;

	// No collisions because the Lab is a lie and collisions will be bad
	Saved_cmdline_collisions_value = Cmdline_dis_collisions;
	Cmdline_dis_collisions = 1;

	// do some other setup
	// External weapon displays require a call to weapons_page_in, which in turn requires team data to be set
	Num_teams = 1;

	team_data* teamp = &Team_data[0];

	// In the lab, all ships are valid
	for (size_t i = 0; i < Ship_info.size(); ++i) {
		teamp->ship_list[i] = static_cast<int>(i);
		strcpy_s(teamp->ship_list_variables[i], "");
		teamp->ship_count[i] = 1;
		teamp->loadout_total += 1;
		strcpy_s(teamp->ship_count_variables[i], "");
	}
	teamp->default_ship = 0;
	teamp->num_ship_choices = static_cast<int>(Ship_info.size());

	// you want guns? you get guns.
	for (size_t i = 0; i < Weapon_info.size(); ++i) {
		teamp->weaponry_pool[i] = static_cast<int>(i);
		teamp->weaponry_count[i] = 640; // should be enough for everyone
		strcpy_s(teamp->weaponry_amount_variable[i], "");
		strcpy_s(teamp->weaponry_pool_variable[i], "");
	}
	teamp->num_weapon_choices = static_cast<int>(Weapon_info.size());

	Game_mode |= GM_LAB;

	using namespace ltp;
	graphicsSettings = gfx_options();
	graphicsSettings.ppcv = ltp::lab_get_ppc();
	graphicsSettings.ambient_factor = ltp::lab_get_ambient();
	graphicsSettings.light_factor = ltp::lab_get_light();
	graphicsSettings.emissive_factor = ltp::lab_get_emissive();
	graphicsSettings.exposure_level = ltp::current_exposure();
	graphicsSettings.tonemapper = ltp::current_tonemapper();
	graphicsSettings.bloom_level = gr_bloom_intensity();
	graphicsSettings.aa_mode = Gr_aa_mode;
}

LabManager::~LabManager()
{
	obj_delete_all();
}

void LabManager::resetGraphicsSettings() {
	Renderer->resetGraphicsSettings(graphicsSettings);
}

void LabManager::onFrame(float frametime) {
	if (gr_screen.mode == GR_OPENGL)
		ImGui_ImplOpenGL3_NewFrame();
	ImGui_ImplSDL2_NewFrame(gr_screen.max_w, gr_screen.max_h);
	ImGui::NewFrame();

	Renderer->onFrame(frametime);

	labUi.create_ui();

	int key = game_check_key();

	int dx, dy, dz;
	mouse_get_delta(&dx, &dy);
	mouse_get_wheel_delta(nullptr, &dz);
	int mouse_x = 0;
	int mouse_y = 0;
	mouse_get_pos(&mouse_x, &mouse_y);

	const bool lmb_down = mouse_down(MOUSE_LEFT_BUTTON) != 0;
	bool lmb_pressed = lmb_down && !LastLmbDown;
	LastLmbDown = lmb_down;

	if (lmb_pressed && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) {
		lmb_pressed = false;
	}

	if (dz != 0 && ImGui::IsWindowHovered(ImGuiHoveredFlags_AnyWindow)) {
		dz = 0;
	}
	auto& current_camera = Renderer->getCurrentCamera();
	current_camera->handleInput(
		dx, dy, dz, lmb_down, lmb_pressed, mouse_down(MOUSE_RIGHT_BUTTON) != 0, key_get_shift_status(), mouse_x, mouse_y);

	if (!current_camera->handlesObjectPlacement()) {
		const bool over_camera_overlay = Renderer->getShowOrientationWidget() && current_camera->isOverlayHit(mouse_x, mouse_y);
		if (lmb_down && !over_camera_overlay) {
			angles rot_angle;
			vm_extract_angles_matrix_alternate(&rot_angle, &CurrentOrientation);

			if (RotationMode == LabRotationMode::Yaw) {
				rot_angle.h += dx / RotationSpeedDivisor;
			}
			if (RotationMode == LabRotationMode::Pitch) {
				rot_angle.p += dy / RotationSpeedDivisor;
			}
			if (RotationMode == LabRotationMode::Both) {
				rot_angle.h += dx / RotationSpeedDivisor;
				rot_angle.p += dy / RotationSpeedDivisor;
			}
			if (RotationMode == LabRotationMode::Roll) {
				rot_angle.b += dx / RotationSpeedDivisor;
			}

			if (rot_angle.h < -PI)
				rot_angle.h = PI - 0.001f;
			if (rot_angle.h > PI)
				rot_angle.h = -PI + 0.001f;

			CLAMP(rot_angle.p, -PI_2, PI_2);

			if (rot_angle.b < -PI)
				rot_angle.b = PI - 0.001f;
			if (rot_angle.b > PI)
				rot_angle.b = -PI + 0.001f;

			vm_angles_2_matrix(&CurrentOrientation, &rot_angle);
		}
	}

	if (CurrentMode == LabMode::Ship) {
		Lab_thrust_afterburn = check_control(AFTERBURNER) != 0;
	} else {
		Lab_thrust_afterburn = false;
	}

	if (key != 0) {
		// handle any key presses
		switch (key) {
			// Adjust AA presets

		case KEY_T:
			Renderer->useNextTeamColorPreset();
			break;

		case KEY_Y:
			Renderer->usePreviousTeamColorPreset();
			break;

		case KEY_V:
			Renderer->resetView();
			break;

		case KEY_UP:
			break;

		case KEY_DOWN:
			break;

		case KEY_R:
			switch (RotationMode) {
			case LabRotationMode::Both:
				RotationMode = LabRotationMode::Yaw;
				break;
			case LabRotationMode::Yaw:
				RotationMode = LabRotationMode::Pitch;
				break;
			case LabRotationMode::Pitch:
				RotationMode = LabRotationMode::Roll;
				break;
			case LabRotationMode::Roll:
				RotationMode = LabRotationMode::Both;
				break;
			}
			break;

		case KEY_S:
			RotationSpeedDivisor *= 10.f;
			if (RotationSpeedDivisor > 10000.f)
				RotationSpeedDivisor = 100.f;
			break;

		case KEY_M:
			gr_dump_envmap(Renderer->currentMissionBackground.c_str());
			break;

		case KEY_ESC:
			notify_close();
			break;

		default:
			// check for game-specific controls
			if (CurrentMode == LabMode::Ship) {
				// These don't work because the lab is a lie and ships don't actually move
				// Also the ships are AI and don't really respond to player input anyway so
				// getting these working will be tricky
				/*if (check_control(PLUS_5_PERCENT_THROTTLE, key))
					Lab_thrust_len += 0.05f;
				else if (check_control(MINUS_5_PERCENT_THROTTLE, key))
					Lab_thrust_len -= 0.05f;

				CLAMP(Lab_thrust_len, 0.0f, 1.0f);*/
			}
			break;
		}
	}

	float rev_rate;
	ship_info* sip = nullptr;

	// First delete any dead objects so we don't end up processing them
	obj_delete_all_that_should_be_dead();

	if (CurrentObject != -1 && (Objects[CurrentObject].type == OBJ_SHIP)) {
		sip = &Ship_info[Ships[Objects[CurrentObject].instance].ship_info_index];

		auto obj = &Objects[CurrentObject];
		bool weapons_firing = false;
		for (auto i = 0; i < Ships[obj->instance].weapons.num_primary_banks; ++i) {
			if (FirePrimaries[i]) {
				weapons_firing = true;
				Ships[obj->instance].weapons.current_primary_bank = i;

				ship_fire_primary(obj);

				Ships[obj->instance].weapon_energy = sip->max_weapon_reserve;
			}
		}

		Ships[obj->instance].flags.set(Ship::Ship_Flags::Trigger_down, weapons_firing);

		for (auto i = 0; i < Ships[obj->instance].weapons.num_secondary_banks; ++i) {
			if (FireSecondaries[i]) {
				Ships[obj->instance].weapons.current_secondary_bank = i;

				ship_fire_secondary(obj);
			}
		}

		ship_process_post(obj, frametime);
		ai_process_subobjects(CurrentObject); // So that animations get reset

		if (!getLabManager()->FireTurrets.empty()) {
			for (auto& [subsys, mode, fire] : getLabManager()->FireTurrets) {
				if (!fire || subsys == nullptr)
					continue;

				vec3d new_pos, new_vec;
				ship_get_global_turret_info(&Objects[subsys->parent_objnum], subsys->system_info, &new_pos, &new_vec);

				bool multipart = false;
				// Turret is multipart
				if (subsys->system_info->turret_gun_sobj >= 0 && subsys->system_info->subobj_num != subsys->system_info->turret_gun_sobj) {
					multipart = true;
				}

				switch (mode) {
					case LabTurretAimType::UVEC: {
						subsys->last_aim_enemy_pos = new_pos + new_vec * 500.0f;
						break;
					}
					case LabTurretAimType::INITIAL: {
						subsys->last_aim_enemy_pos = vmd_zero_vector;
						break;
					}
					case LabTurretAimType::RANDOM: {
						bool gen_new_vec = !multipart || subsys->points_to_target <= 0.010f;
						if (gen_new_vec && timestamp_elapsed(subsys->turret_next_fire_stamp)) {
							vec3d rand_vec;
							const int MAX_ATTEMPTS = 100;
							bool valid_vec_found = false;

							// You get 100 tries to find a set of random coords to fire at
							for (int attempt = 0; attempt < MAX_ATTEMPTS; ++attempt) {
								float full_fov_degrees = 2.0f * acosf(subsys->system_info->turret_fov) * (180.0f / PI);
								vm_vec_random_cone(&rand_vec, &new_vec, full_fov_degrees);

								vec3d target_point;
								vm_vec_scale_add(&target_point, &new_pos, &rand_vec, 1000.0f);

								//  Create a vector from the turret to the random point.
								vec3d turret_to_target;
								vm_vec_sub(&turret_to_target, &target_point, &new_pos);
								vm_vec_normalize(&turret_to_target);

								// Test if the generated vector is within the FOV
								if (turret_fov_test(subsys, &new_vec, &turret_to_target, 0.0f)) {
									valid_vec_found = true;
									rand_vec = target_point;
									break;
								}
							}

							if (valid_vec_found) {
								subsys->last_aim_enemy_pos = rand_vec;
							} else {
								subsys->last_aim_enemy_pos = new_pos + new_vec * 500.0f;
							}
						}
						break;
					}
					default:
						Assertion(false, "Invalid Lab Turret Aim Type!");
						break;
				}

				ai_turret_execute_behavior(&Ships[obj->instance], subsys);
			}
		}

		// Check if we have finished an undock test. If so, delete the docker ship
		if (DockerObject >= 0) {
			object* docker_objp = &Objects[DockerObject];
			ship* shipp = &Ships[docker_objp->instance];
			ai_info* aip = &Ai_info[shipp->ai_index];

			bool hasDockGoal = false;
			for (const auto& goal : aip->goals) {
				if (goal.ai_mode == AI_GOAL_DOCK || goal.ai_mode == AI_GOAL_UNDOCK) {
					hasDockGoal = true;
					break;
				}
			}

			if (!hasDockGoal && !object_is_docked(docker_objp)) {
				deleteDockerObject();
			}
		}

		// Check if we have finished a bay test. If so, delete the bay ship
		if (BayObject >= 0) {
			object* bay_objp = &Objects[BayObject];
			ship* shipp = &Ships[bay_objp->instance];
			ai_info* aip = &Ai_info[shipp->ai_index];
			bool hasBayGoal = aip->mode == AIM_BAY_EMERGE || aip->mode == AIM_BAY_DEPART;
			if (!hasBayGoal) {
				deleteBayObject();
			}
		}

	}

	// get correct revolution rate
	rev_rate = REVOLUTION_RATE;

	if (sip != nullptr) {
		if (sip->is_big_ship()) {
			rev_rate *= 1.7f;
		}
		else if (sip->is_huge_ship()) {
			rev_rate *= 3.0f;
		}
	}

	if (Flags[ManagerFlags::ModelRotationEnabled]) {
		angles rot_angles;

		rot_angles.p = 0.0f;
		rot_angles.b = 0.0f;
		rot_angles.h = PI2 * frametime / rev_rate;
		vm_rotate_matrix_by_angles(&CurrentOrientation, &rot_angles);
	}
	
	if (Cmdline_show_imgui_debug)
		ImGui::ShowDemoWindow();
	ImGui::Render();
	if (gr_screen.mode == GR_OPENGL)
		ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());

	if (CloseThis)
		close();

	gr_flip();
}

//Cleans the scene and resets object actions. Stops any firing weapons.
void LabManager::cleanup() {
	if (CurrentObject != -1) {

		// Stop any firing weapons
		FireTurrets.clear();
		FirePrimaries.fill(false);
		FireSecondaries.fill(false);

		// Remove all beams
		beam_delete_all();

		// Properly clean up the test objects
		deleteTestObjects();

		// Remove all objects
		obj_delete_all();

		// Reset large-ship split explosion state. In the lab we can delete exploding ships while
		// cycling classes, so clear any lingering Split_ships entries tied to the previous view.
		shipfx_large_blowup_level_init();

		// Clean up the particles
		particle::kill_all();

		// Clean up our path mess
		reset_ai_path_points();

		// Reset lab variables
		CurrentMode = LabMode::None;
		CurrentObject = -1;
		CurrentSubtype = -1;
		CurrentClass = -1;
		DockerDockPoint.clear();
		DockeeDockPoint.clear();
		BayPathMask = 0;
		CurrentPosition = vmd_zero_vector;
		CurrentOrientation = vmd_identity_matrix;
		ModelFilename = "";
		Player_ship = nullptr;

		Lab_object_detail_level = -1;
	}
}

void LabManager::deleteTestObjects() {
	deleteDockerObject();
	deleteBayObject();
}

void LabManager::deleteDockerObject() {
	if (DockerObject >= 0) {
		object* obj = &Objects[CurrentObject];

		while (object_is_docked(obj)) {
			object_jettison_cargo(obj, dock_get_first_docked_object(obj), 50, true);
		}

		obj_delete(DockerObject);
		DockerObject = -1;
		reset_ai_path_points();
	}
}

void LabManager::deleteBayObject()
{
	if (BayObject >= 0) {
		object* bay_objp = &Objects[BayObject];
		ai_info* aip = &Ai_info[Ships[bay_objp->instance].ai_index];

		// This is kind of a hack but it gets the job done
		// Since we're deleting the bay object we can flag bay doors to close
		Ships[Objects[CurrentObject].instance].bay_doors_wanting_open = 0;
		extern void ai_manage_bay_doors(object * pl_objp, ai_info * aip, bool done);
		ai_manage_bay_doors(bay_objp, aip, true);

		obj_delete(BayObject);
		BayObject = -1;
		reset_ai_path_points();
	}
}

void LabManager::spawnDockerObject() {

	deleteTestObjects();

	if (DockerDockPoint.empty() || DockeeDockPoint.empty()) {
		return;
	}

	// Check ship class index
	if (DockerClass < 0 || DockerClass >= static_cast<int>(Ship_info.size())) {
		mprintf(("Invalid ship class index %d\n", DockerClass));
		return;
	}

	object* obj = &Objects[CurrentObject];

	// Spawn near the target
	vec3d spawn_pos = obj->pos;
	vec3d offset = {{{0.0f, -50000.0f, -50000.0f}}}; // Spawn it far away then we can move it based on its radius
	vec3d final_pos;
	vm_vec_add(&final_pos, &spawn_pos, &offset);

	matrix spawn_orient = vmd_identity_matrix;

	DockerObject = ship_create(&spawn_orient, &final_pos, DockerClass, nullptr, true);

	if (DockerObject < 0) {
		mprintf(("Failed to create docker object with ship class index %d!\n", DockerClass));
		return;
	}

	object* new_objp = &Objects[DockerObject];

	// Set a more reasonable starting position
	float offset_radius = obj->radius + new_objp->radius;
	offset = {{{0.0f, obj->pos.xyz.y + offset_radius, obj->pos.xyz.z - offset_radius}}}; // Make this selectable or random?
	vm_vec_add(&final_pos, &spawn_pos, &offset);
	new_objp->pos = final_pos;
}

void LabManager::spawnBayObject()
{

	deleteTestObjects();

	// Technically this would work but that's not the intention of the test
	// and suggests something went wrong
	if (BayPathMask == 0) {
		return;
	}

	// Check ship class index
	if (!SCP_vector_inbounds(Ship_info, BayClass)) {
		mprintf(("Invalid ship class index %d\n", BayClass));
		return;
	}

	object* obj = &Objects[CurrentObject];

	// Spawn near the target
	vec3d spawn_pos = obj->pos;
	vec3d offset = {{{0.0f, -50000.0f, -50000.0f}}}; // Spawn it far away then we can move it based on its radius
	vec3d final_pos;
	vm_vec_add(&final_pos, &spawn_pos, &offset);

	matrix spawn_orient = vmd_identity_matrix;

	BayObject = ship_create(&spawn_orient, &final_pos, BayClass, nullptr, true);

	if (BayObject < 0) {
		mprintf(("Failed to create bay test object with ship class index %d!\n", BayClass));
		return;
	}

	object* new_objp = &Objects[BayObject];

	// Set a more reasonable starting position
	float offset_radius = obj->radius + new_objp->radius;
	offset = {{{0.0f, obj->pos.xyz.y + offset_radius, obj->pos.xyz.z - offset_radius}}}; // Make this selectable or random?
	vm_vec_add(&final_pos, &spawn_pos, &offset);
	new_objp->pos = final_pos;
}

void LabManager::beginDockingTest() {
	// Spawn a docker object
	spawnDockerObject();

	if (DockerObject >= 0) {
		object* new_objp = &Objects[DockerObject];
		ship* new_shipp = &Ships[new_objp->instance];
		ai_info* aip = &Ai_info[new_shipp->ai_index];

		// TODO: Get the pos of the first dock path point and set the ship's position to that

		// Ensure AI is ready
		ai_clear_ship_goals(aip);

		// Create the dock order
		ai_goal_type type = ai_goal_type::EVENT_SHIP;
		ai_goal* aigp = &aip->goals[0];

		ai_goal_reset(aigp, true);
		aigp->type = type;

		aigp->target_name = ai_get_goal_target_name(Ships[Objects[CurrentObject].instance].ship_name, &aigp->target_name_index);
		aigp->docker.name = ai_add_dock_name(DockerDockPoint.c_str());
		aigp->dockee.name = ai_add_dock_name(DockeeDockPoint.c_str());
		aigp->priority = 200;

		aigp->ai_mode = AI_GOAL_DOCK;
		aigp->ai_submode = AIS_DOCK_0;

		aigp->flags.set(AI::Goal_Flags::Afterburn_hard);
	}
}

void LabManager::beginUndockingTest() {
	// Spawn a docker object if necessary
	if (DockerObject < 0 || !object_is_docked(&Objects[DockerObject])) {
		spawnDockerObject();

		// Once spawned we need to instantly set it to docked
		if (DockerObject >= 0) {
			object* dockee_objp = &Objects[CurrentObject];
			object* docker_objp = &Objects[DockerObject];

			int docker_point_index = model_find_dock_name_index(Ship_info[Ships[docker_objp->instance].ship_info_index].model_num, DockerDockPoint.c_str());
			int dockee_point_index = model_find_dock_name_index(Ship_info[Ships[dockee_objp->instance].ship_info_index].model_num, DockeeDockPoint.c_str());

			// set model animations correctly
			// (fortunately, this function is called AFTER model_anim_set_initial_states in the sea of ship creation
			// functions, which is necessary for model animations to start from t=0 at the correct positions)
			ship *shipp = &Ships[docker_objp->instance];
			ship *goal_shipp = &Ships[dockee_objp->instance];

			ship_info* sip = &Ship_info[shipp->ship_info_index];
			ship_info* goal_sip = &Ship_info[goal_shipp->ship_info_index];

			polymodel_instance* shipp_pmi = model_get_instance(shipp->model_instance_num);
			polymodel_instance* goal_shipp_pmi = model_get_instance(goal_shipp->model_instance_num);

			(sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage1, docker_point_index)
				+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage2, docker_point_index)
				+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage3, docker_point_index)
				+ sip->animations.getAll(shipp_pmi, animation::ModelAnimationTriggerType::Docked, docker_point_index)).start(animation::ModelAnimationDirection::FWD, true, true);
			(goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage1, dockee_point_index)
				+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage2, dockee_point_index)
				+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docking_Stage3, dockee_point_index)
				+ goal_sip->animations.getAll(goal_shipp_pmi, animation::ModelAnimationTriggerType::Docked, dockee_point_index)).start(animation::ModelAnimationDirection::FWD, true, true);

			// Set docker as instantly docked
			dock_orient_and_approach(docker_objp, docker_point_index, dockee_objp, dockee_point_index, DOA_DOCK_STAY);
			ai_do_objects_docked_stuff(docker_objp, docker_point_index, dockee_objp, dockee_point_index, true);
		}
	}

	if (DockerObject >= 0) {
		ai_info* aip = &Ai_info[Ships[Objects[DockerObject].instance].ai_index];

		// Ensure AI is ready
		ai_clear_ship_goals(aip);

		// Create the undock order
		int gindex = 0;
		ai_goal_type type = ai_goal_type::EVENT_SHIP;
		ai_goal* aigp = &aip->goals[gindex];

		ai_goal_reset(aigp, true);
		aigp->type = type;

		aigp->target_name = ai_get_goal_target_name(Ships[Objects[CurrentObject].instance].ship_name, &aigp->target_name_index);
		aigp->priority = 200;

		aigp->ai_mode = AI_GOAL_UNDOCK;
		aigp->ai_submode = AIS_UNDOCK_0;
	}
}

void LabManager::beginBayTest()
{
	// Spawn a bay object
	spawnBayObject();

	if (BayObject < 0)
		return;

	// Reset the bay status of the current ship
	ship* shipp = &Ships[Objects[CurrentObject].instance];
	shipp->bay_doors_wanting_open = 0;
	shipp->bay_doors_status = MA_POS_NOT_SET;

	if (BayTestMode == BayMode::Arrival) {
		object* new_objp = &Objects[BayObject];
		ship* new_shipp = &Ships[new_objp->instance];
		ai_info* aip = &Ai_info[new_shipp->ai_index];

		// Ensure AI is ready
		ai_clear_ship_goals(aip);

		if (ai_acquire_emerge_path(new_objp, CurrentObject, BayPathMask) == -1) {
			mprintf(("Unable to acquire arrival path on anchor ship %s\n", Ships[Objects[CurrentObject].instance].ship_name)); // Warning instead of print?
			deleteBayObject();
			return;
		}
	}

	if (BayTestMode == BayMode::Departure) {
		object* new_objp = &Objects[BayObject];
		ship* new_shipp = &Ships[new_objp->instance];
		ai_info* aip = &Ai_info[new_shipp->ai_index];
		// Ensure AI is ready
		ai_clear_ship_goals(aip);
		if (ai_acquire_depart_path(new_objp, CurrentObject, BayPathMask) == -1) {
			mprintf(("Unable to acquire departure path on anchor ship %s\n", Ships[Objects[CurrentObject].instance].ship_name)); // Warning instead of print?
			deleteBayObject();
			return;
		}

		// Set the object's position to the start of the path
		new_objp->pos = Path_points[aip->path_start].pos;
	}
}

void LabManager::changeDisplayedObject(LabMode mode, int info_index, int subtype) {
	// Removing this allows reseting by clicking on the object again,
	// making it easier to respawn destroyed objects
	// If this is re-enabled then it will need to be modified so that
	// ShowingTechModel bool toggles are also accounted for
	//if (mode == CurrentMode && info_index == CurrentClass)
		//return;

	// Toggle the show thrusters default when we change modes
	if (mode != CurrentMode) {
		if (mode == LabMode::Ship) {
			labUi.show_thrusters = false;
		}
		if (mode == LabMode::Weapon) {
			labUi.show_thrusters = true;
		}

		// Set the render flag now
		Renderer->setRenderFlag(LabRenderFlag::ShowThrusters, labUi.show_thrusters);
	}

	cleanup();

	CurrentMode = mode;
	CurrentClass = info_index;

	if (CurrentMode == LabMode::Asteroid)
		CurrentSubtype = subtype;

	ai_paused = 1;
	Player_ship = nullptr;

	DockeeDockPoint.clear();
	DockerDockPoint.clear();

	BayPathMask = 0;

	switch (CurrentMode) {
	case LabMode::Ship:
		CurrentObject = ship_create(&CurrentOrientation, &CurrentPosition, CurrentClass);
		changeShipInternal();
		if (isSafeForShips()) {
			Player_ship = &Ships[Objects[CurrentObject].instance];
			ai_paused = 0;

			// Set the ship to play dead so it doesn't move. For the lab there are two special carveouts:
			// 1: Allow subsystem rotations/translations
			// 2: Allow subystems to be processed
			ai_add_ship_goal_scripting(AI_GOAL_PLAY_DEAD_PERSISTENT, -1, 100, nullptr, &Ai_info[Player_ship->ai_index], 0, 0);
		}
		break;
	case LabMode::Prop:
		CurrentObject = prop_create(&CurrentOrientation, &CurrentPosition, CurrentClass);
		if (isSafeForProps()) {
			ModelFilename = Prop_info[CurrentClass].pof_file;
		}
		break;
	case LabMode::Weapon:
		if (ShowingTechModel && VALID_FNAME(Weapon_info[CurrentClass].tech_model)) {
			ModelFilename = Weapon_info[CurrentClass].tech_model;
			CurrentObject = obj_raw_pof_create(ModelFilename.c_str(), &CurrentOrientation, &CurrentPosition);
		}else if (Weapon_info[CurrentClass].wi_flags[Weapon::Info_Flags::Beam]) {
			beam_fire_info fire_info;
			memset(&fire_info, 0, sizeof(beam_fire_info));
			fire_info.accuracy = 0.000001f; // this will guarantee a hit
			fire_info.bfi_flags |= BFIF_FLOATING_BEAM;
			fire_info.turret = nullptr; // A free-floating beam isn't fired from a subsystem.
			fire_info.burst_index = 0;
			fire_info.beam_info_index = CurrentClass;
			fire_info.shooter = nullptr;
			fire_info.team = 0;
			fire_info.starting_pos = CurrentPosition;
			fire_info.target = nullptr;
			fire_info.target_subsys = nullptr;
			fire_info.bfi_flags |= BFIF_TARGETING_COORDS;
			fire_info.fire_method = BFM_SEXP_FLOATING_FIRED;

			// Fire beam straight ahead from spawn origin
			vec3d origin = CurrentPosition;
			vec3d endpoint;
			vm_vec_scale_add(&endpoint, &origin, &vmd_z_vector, 1500.0f); // Fire forward along +Z

			fire_info.target_pos1 = endpoint;
			fire_info.target_pos2 = endpoint;

			CurrentObject = beam_fire(&fire_info);
		} else {
			CurrentObject = weapon_create(&CurrentPosition, &CurrentOrientation, CurrentClass, -1);
			if (Weapon_info[CurrentClass].model_num != -1) {
				ModelFilename = model_get(Weapon_info[CurrentClass].model_num)->filename;
			}
		}
		break;
	case LabMode::Asteroid: {
		// Ensure model is loaded before creating asteroid
		asteroid_load(CurrentClass, CurrentSubtype);
		object* objp = asteroid_create(&Asteroid_field, CurrentClass, CurrentSubtype, false);
		if (objp != nullptr) {
			CurrentObject = OBJ_INDEX(objp);

			// Zero out asteroid velocity
			vm_vec_zero(&objp->phys_info.rotvel);
			vm_vec_zero(&objp->phys_info.desired_rotvel);
			objp->flags.remove(Object::Object_Flags::Physics);
		} else {
			CurrentObject = -1;
			mprintf(("LabManager: Failed to create asteroid for index %d, subtype %d\n", CurrentClass, CurrentSubtype));
		}

		break;
	}
	default:
		UNREACHABLE("Unhandled lab mode %d", (int)mode);
		ModelFilename = "";
		break;
	}

	Assert(CurrentObject != -1);

	Renderer->getCurrentCamera()->displayedObjectChanged();
}

void LabManager::changeShipInternal() {
	auto ship_objp = &Ships[Objects[CurrentObject].instance];
	auto ship_infop = &Ship_info[ship_objp->ship_info_index];

	// This is normally set during mission parse
	ship_objp->special_exp_damage = -1;

	// If the ship class defines replacement textures, load them and apply them to the ship
	// load the texture
	auto replacements = ship_infop->replacement_textures;
	for (auto& tr : replacements) {
		if (!stricmp(tr.new_texture, "invisible"))
		{
			// invisible is a special case
			tr.new_texture_id = REPLACE_WITH_INVISIBLE;
		}
		else
		{
			// try to load texture or anim as normal
			tr.new_texture_id = bm_load_either(tr.new_texture);
		}
	}

	ship_objp->apply_replacement_textures(replacements);
	ship_page_in_textures(ship_objp->ship_info_index);

	if (!ship_infop->default_team_name.empty())
		Renderer->setTeamColor(ship_infop->default_team_name);

	ModelFilename = model_get(ship_infop->model_num)->filename;
}