#include "lab_ui.h"
#include "globalincs/vmallocator.h"

#include "lab_ui_helpers.h"

#include "asteroid/asteroid.h"
#include "graphics/debug_sphere.h"
#include "graphics/matrix.h"
#include "lab/labv2_internal.h"
#include "lighting/lighting_profiles.h"
#include "ship/shiphit.h"
#include "weapon/weapon.h"
#include "mission/missionload.h"
#include "prop/prop.h"
#include "controlconfig/controlsconfig.h"

using namespace ImGui;

namespace {
SCP_string get_binding_text(int action_id)
{
	const auto& action = Control_config[action_id];

	if (!action.first.empty() && !action.second.empty()) {
		return action.first.textify() + " or " + action.second.textify();
	}

	if (!action.first.empty()) {
		return action.first.textify();
	}

	if (!action.second.empty()) {
		return action.second.textify();
	}

	return "Unbound";
}

void controls_reference_entry(const char* label, const SCP_string& description)
{
	Bullet();
	SameLine();
	TextWrapped("%s: %s", label, description.c_str());
}

void controls_reference_entry(const char* label, const char* description)
{
	Bullet();
	SameLine();
	TextWrapped("%s: %s", label, description);
}
} // namespace

std::map<animation::ModelAnimationTriggerType, std::map<SCP_string, bool>> manual_animation_triggers = {};
std::map<animation::ModelAnimationTriggerType, bool> manual_animations = {};

std::array<bool, MAX_SHIP_PRIMARY_BANKS> triggered_primary_banks = {false, false, false};
std::array<bool, MAX_SHIP_SECONDARY_BANKS> triggered_secondary_banks = {false, false, false, false};

void LabUi::object_changed()
{
	rebuild_after_object_change = true;

	manual_animation_triggers.clear();

	for (auto& trigger : triggered_primary_banks)
		trigger = false;
	for (auto& trigger : triggered_secondary_banks)
		trigger = false;
}

void LabUi::build_species_entry(const species_info &species_def, int species_idx) const
{
	with_TreeNode(species_def.species_name)
	{
		int ship_info_idx = 0;

		for (auto const& class_def : Ship_info) {
			if (class_def.species == species_idx) {
				SCP_string node_label;
				sprintf(node_label, "##ShipClassIndex%i", ship_info_idx);
				TreeNodeEx(node_label.c_str(),
					ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen,
					"%s", class_def.name);
				if (IsItemClicked() && !IsItemToggledOpen()) {
					getLabManager()->changeDisplayedObject(LabMode::Ship, ship_info_idx);
				}
			}
			ship_info_idx++;
		}
	}
}

void LabUi::build_ship_list() const
{
	with_TreeNode("Ship Classes")
	{
		int species_idx = 0;
		for (auto const& species_def : Species_info) {
			build_species_entry(species_def, species_idx);
			species_idx++;
		}
	}
}

void LabUi::build_weapon_subtype_list() const
{
	for (auto weapon_subtype_idx = 0; weapon_subtype_idx < Num_weapon_subtypes; ++weapon_subtype_idx) {
		with_TreeNode(Weapon_subtype_names[weapon_subtype_idx])
		{
			int weapon_idx = 0;

			for (auto const& class_def : Weapon_info) {
				if ((weapon_subtype_idx == 2 && class_def.wi_flags[Weapon::Info_Flags::Beam]) ||
					(class_def.subtype == weapon_subtype_idx && !class_def.wi_flags[Weapon::Info_Flags::Beam])) {
					SCP_string node_label;
					sprintf(node_label, "##WeaponClassIndex%i", weapon_idx);
					TreeNodeEx(node_label.c_str(),
						ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen,
						"%s", class_def.name);

					if (IsItemClicked() && !IsItemToggledOpen()) {
						getLabManager()->changeDisplayedObject(LabMode::Weapon, weapon_idx);
					}
				}
				weapon_idx++;
			}
		}
	}
}

void LabUi::build_prop_subtype_list()
{
	for (auto& propc : Prop_categories) {
		with_TreeNode(propc.name.c_str())
		{
			int prop_idx = 0;

			for (auto const& class_def : Prop_info) {
				if (lcase_equal(prop_get_category(class_def.category_index)->name, propc.name)) {
					SCP_string node_label;
					sprintf(node_label, "##PropClassIndex%i", prop_idx);
					TreeNodeEx(node_label.c_str(),
						ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen,
						"%s",
						class_def.name.c_str());

					if (IsItemClicked() && !IsItemToggledOpen()) {
						getLabManager()->changeDisplayedObject(LabMode::Prop, prop_idx);
					}
				}
				prop_idx++;
			}
		}
	}
}

void LabUi::build_asteroid_list()
{
	with_TreeNode("Asteroids")
	{
		int asteroid_idx = 0;

		for (const auto& info : Asteroid_info) {
			if (info.type == ASTEROID_TYPE_DEBRIS) {
				asteroid_idx++;
				continue;
			}

			int subtype_idx = 0;
			for (const auto& subtype : info.subtypes) {
				SCP_string node_label;
				sprintf(node_label, "##AsteroidClassIndex%i_%i", asteroid_idx, subtype_idx);
				TreeNodeEx(node_label.c_str(),
					ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen,
					"%s (%s)",
					info.name,
					subtype.type_name.c_str());

				if (IsItemClicked() && !IsItemToggledOpen()) {
					getLabManager()->changeDisplayedObject(LabMode::Asteroid, asteroid_idx, subtype_idx);
				}

				subtype_idx++;
			}

			asteroid_idx++;
		}
	}
}

void LabUi::build_debris_list()
{
	with_TreeNode("Debris")
	{
		int debris_idx = 0;

		for (const auto& info : Asteroid_info) {
			if (info.type != ASTEROID_TYPE_DEBRIS) {
				debris_idx++;
				continue;
			}

			SCP_string node_label;
			sprintf(node_label, "##DebrisClassIndex%i", debris_idx);
			TreeNodeEx(node_label.c_str(),
				ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen,
				"%s",
				info.name);

			if (IsItemClicked() && !IsItemToggledOpen()) {
				getLabManager()->changeDisplayedObject(LabMode::Asteroid, debris_idx, 0); // Debris subtype is always 0
			}

			debris_idx++;
		}
	}
}

void LabUi::build_weapon_list() const
{
	with_TreeNode("Weapon Classes")
	{
		build_weapon_subtype_list();
	}
}

void LabUi::build_object_list()
{
	with_TreeNode("Asteroid/Debris Types")
	{
		build_asteroid_list();
		build_debris_list();
	}
}

void LabUi::build_prop_list()
{
	with_TreeNode("Prop Classes")
	{
		build_prop_subtype_list();
	}
}

void LabUi::build_background_list()
{
	SCP_vector<SCP_string> t_missions;

	cf_get_file_list(t_missions, CF_TYPE_MISSIONS, NOX("*.fs2"));

	// Remove any ignored missions from the list
	SCP_vector<SCP_string> missions;
	for (int i = 0; i < (int)t_missions.size(); i++) {
		if (!mission_is_ignored(t_missions[i].c_str())) {
			missions.push_back(t_missions[i]);
		}
	}

	static SCP_map<SCP_string, SCP_vector<SCP_string>> directories;

	if (directories.empty()) {
		for (const auto& filename : missions) {
			auto res = cf_find_file_location((filename + ".fs2").c_str(), CF_TYPE_MISSIONS);

			if (res.found) {
				auto location = get_directory_or_vp(res.full_name.c_str());

				directories[location].push_back(filename);
			}
		}
	}

	ImGuiTreeNodeFlags node_flags = ImGuiTreeNodeFlags_Leaf | ImGuiTreeNodeFlags_NoTreePushOnOpen;

	TreeNodeEx(LAB_MISSION_NONE_STRING, node_flags, LAB_MISSION_NONE_STRING);
	if (IsItemClicked() && !IsItemToggledOpen()) {
		getLabManager()->Renderer->useBackground(LAB_MISSION_NONE_STRING);
	}

	for (auto const& directory : directories) {
		auto directory_name = directory.first.c_str();
		// if the directory name is empty, this indicates loose files in the root data directory
		// since imgui requires tree nodes to have non-empty names, we substitute a static string here
		if (!strlen(directory_name))
			directory_name = "Root";

		with_TreeNode(directory_name)
		{
			for (const auto& mission_name : directory.second) {
				TreeNodeEx(mission_name.c_str(), node_flags, "%s", mission_name.c_str());

				if (IsItemClicked() && !IsItemToggledOpen()) {
					getLabManager()->Renderer->useBackground(mission_name);
				}
			}
		}
	}
}

void LabUi::build_options_menu()
{
	with_Menu("Options")
	{
		bool show_widget_menu = getLabManager()->Renderer->getShowOrientationWidget();
		MenuItem("Render options", nullptr, &show_render_options_dialog);
		MenuItem("Object selector", nullptr, &show_object_selection_dialog);
		MenuItem("Background selector", nullptr, &show_background_selection_dialog);
		MenuItem("Object options", nullptr, &show_object_options_dialog);
		MenuItem("Controls reference", nullptr, &show_controls_reference_dialog);
		if (MenuItem("Show orientation cube widget", nullptr, show_widget_menu)) {
			getLabManager()->Renderer->setShowOrientationWidget(!show_widget_menu);
		}
		MenuItem("Reset View", nullptr, &reset_view);
		MenuItem("Close lab", "ESC", &close_lab);
	}
}

void LabUi::build_toolbar_entries()
{
	with_MainMenuBar
	{
		build_options_menu();
	}

	if (reset_view) {
		getLabManager()->Renderer->resetView();
		reset_view = false;
	}

	if (close_lab) {
		getLabManager()->notify_close();
		close_lab = false;
	}
}

void LabUi::show_background_selector() const
{
	with_Window("Select background")
	{
		with_CollapsingHeader("Mission Background")
		{
			build_background_list();
		}
	}
}

void LabUi::show_object_selector() const
{
	with_Window("Select object")
	{
		with_CollapsingHeader("Displayed Object")
		{
			build_ship_list();

			build_weapon_list();

			build_prop_list();

			build_object_list();
		}
	}
}

void LabUi::create_ui()
{
	build_toolbar_entries();

	if (show_render_options_dialog)
		show_render_options();

	if (show_object_options_dialog)
		show_object_options();

	if (show_background_selection_dialog)
		show_background_selector();

	if (show_object_selection_dialog)
		show_object_selector();

	if (show_controls_reference_dialog)
		show_controls_reference();

	rebuild_after_object_change = false;
}

void LabUi::show_controls_reference()
{
	with_Window("Lab controls reference")
	{
		TextWrapped("Mouse controls");
		controls_reference_entry("LMB + drag", "Orient the displayed object.");
		controls_reference_entry("RMB + drag", "Rotate the camera.");
		controls_reference_entry("Shift + RMB + drag", "Pan the camera on the X/Y plane.");
		controls_reference_entry("Mouse wheel", "Zoom the camera in or out.");
		TextWrapped("Rotation axis limits and rotation speed apply only to object orientation (LMB), not "
					"camera controls (RMB).");

		Separator();
		TextWrapped("Keyboard shortcuts");
		controls_reference_entry("R", "Cycle object orientation (LMB) axis mode (Yaw, Pitch, Roll, or Both).");
		controls_reference_entry("S", "Cycle object orientation (LMB) speed.");
		controls_reference_entry("V", "Reset camera view.");
		controls_reference_entry("T / Y", "Cycle team color presets.");
		controls_reference_entry("1-9", "Switch anti-aliasing presets.");
		controls_reference_entry("M", "Export an environment map.");
		controls_reference_entry("ESC", "Close the lab.");

		if (getLabManager()->CurrentMode == LabMode::Ship) {
			Separator();
			TextWrapped("Ship-only controls (from current control bindings)");
			// These don't work in the new lab yet
			//controls_reference_entry("Increase throttle by 5%", get_binding_text(PLUS_5_PERCENT_THROTTLE));
			//controls_reference_entry("Decrease throttle by 5%", get_binding_text(MINUS_5_PERCENT_THROTTLE));
			controls_reference_entry("Afterburner", get_binding_text(AFTERBURNER));
		}
	}
}

const char* antialiasing_settings[] = {
	"None",
	"FXAA Low",
	"FXAA Medium",
	"FXAA High",
	"SMAA Low",
	"SMAA Medium",
	"SMAA High",
	"SMAA Ultra",
};

SCP_string tonemappers[] = {
	"Linear",
	"Uncharted",
	"ACES",
	"ACES Approximate",
	"Cineon",
	"Piecewise Power Curve",
	"Piecewise Power Curve (RGB)",
	"Reinhard Extended",
	"Reinhard Jodie",
};

const char* texture_quality_settings[] = {
	"Minimum",
	"Low",
	"Medium",
	"High",
	"Maximum",
};

void LabUi::build_texture_quality_combobox()
{
	with_Combo("Texture quality",
		texture_quality_settings[static_cast<int>(getLabManager()->Renderer->getTextureQuality())])
	{
		for (int n = 0; n < IM_ARRAYSIZE(texture_quality_settings); n++) {
			bool is_selected = n == static_cast<int>(getLabManager()->Renderer->getTextureQuality());
			if (Selectable(texture_quality_settings[n], is_selected))
				getLabManager()->Renderer->setTextureQuality(static_cast<TextureQuality>(n));

			if (is_selected)
				SetItemDefaultFocus();
		}
	}
}

void LabUi::build_team_color_combobox() const
{
	if (!Team_Colors.empty()) {
		with_Combo("Team Color setting", getLabManager()->Renderer->getCurrentTeamColor().c_str())
		{
			for (const auto& team_color_name : Team_Colors) {
				bool is_selected = team_color_name.first == getLabManager()->Renderer->getCurrentTeamColor();

				if (Selectable(team_color_name.first.c_str(), is_selected)) {
					getLabManager()->Renderer->setTeamColor(team_color_name.first);
				}

				if (is_selected)
					SetItemDefaultFocus();
			}
		}
	}
}

void LabUi::build_antialiasing_combobox()
{
	with_Combo("Antialiasing method", antialiasing_settings[static_cast<int>(Gr_aa_mode)])
	{
		for (int n = 0; n < IM_ARRAYSIZE(antialiasing_settings); n++) {
			bool is_selected = static_cast<int>(Gr_aa_mode) == n;

			if (Selectable(antialiasing_settings[n], is_selected))
				getLabManager()->Renderer->setAAMode(static_cast<AntiAliasMode>(n));

			if (is_selected)
				SetItemDefaultFocus();
		}
	}
}
namespace ltp = lighting_profiles;
using namespace ltp;

void LabUi::build_tone_mapper_combobox()
{
	with_Combo("Tonemapper", ltp::tonemapper_to_name(ltp::current_tonemapper()).c_str())
	{
		for (int n = 0; n < IM_ARRAYSIZE(tonemappers); n++) {
			const bool is_selected =
				ltp::tonemapper_to_name(ltp::current_tonemapper()) == tonemappers[n];
			if (Selectable(tonemappers[n].c_str(), is_selected))
				ltp::lab_set_tonemapper(ltp::name_to_tonemapper(tonemappers[n]));

			if (is_selected)
				SetItemDefaultFocus();
		}
	}
}

void LabUi::show_render_options()
{
	auto profile_list = ltp::list_profiles();
	int bloom_level = gr_bloom_intensity();
	float ambient_factor = ltp::lab_get_ambient();
	float light_factor = ltp::lab_get_light();
	float emissive_factor = ltp::lab_get_emissive();
	float exposure_level = ltp::current_exposure();
	auto ppcv = ltp::lab_get_ppc();
	show_orientation_widget = getLabManager()->Renderer->getShowOrientationWidget();

	bool skip_setting_light_options_this_frame = false;

	with_Window("Render options")
	{
		Checkbox("Enable Model Rotation", &enable_model_rotation);

		with_CollapsingHeader("Model features")
		{
			if (getLabManager()->CurrentMode == LabMode::Ship) {
				Checkbox("Rotate/Translate Subsystems", &animate_subsystems);
			}
			Checkbox("Show full detail", &show_full_detail);
			if (getLabManager()->CurrentMode == LabMode::Ship ||
				getLabManager()->CurrentMode == LabMode::Weapon) {
				Checkbox("Show thrusters", &show_thrusters);
				if (getLabManager()->CurrentMode == LabMode::Ship) {
					Checkbox("Show afterburners", &show_afterburners);
					Checkbox("Show weapons", &show_weapons);
					Checkbox("Show Insignia", &show_insignia);
					Checkbox("Show damage lightning", &show_damage_lightning);
				}
			}
			Checkbox("No glowpoints", &no_glowpoints);
		}

		with_CollapsingHeader("Texture options")
		{
			Checkbox("Diffuse map", &diffuse_map);
			Checkbox("Glow map", &glow_map);
			Checkbox("Specular map", &spec_map);
			Checkbox("Reflection map", &reflect_map);
			Checkbox("Environment map", &env_map);
			Checkbox("Normal map", &normal_map);
			Checkbox("Height map", &height_map);
			Checkbox("Misc map", &misc_map);
			Checkbox("AO map", &ao_map);

			build_texture_quality_combobox();

			build_team_color_combobox();
		}

		with_CollapsingHeader("Scene rendering options")
		{
			Checkbox("Hide Post Processing", &hide_post_processing);
			Checkbox("Hide particles", &no_particles);
			Checkbox("Render as wireframe", &use_wireframe_rendering);
			Checkbox("Orthographic projection", &use_orthographic_projection);
			Checkbox("Show orientation cube widget", &show_orientation_widget);
			Checkbox("Render without light", &no_lighting);
			Checkbox("Render with emissive lighting", &show_emissive_lighting);
			SliderFloat("Light brightness", &light_factor, 0.0f, 10.0f);
			SliderFloat("Ambient factor", &ambient_factor, 0.0f, 10.0f);
			SliderFloat("Emissive amount", &emissive_factor, 0.0f, 10.0f);
			SliderFloat("Exposure", &exposure_level, 0.0f, 8.0f);
			SliderInt("Bloom level", &bloom_level, 0, 200);

			build_antialiasing_combobox();

			build_tone_mapper_combobox();

			if (ltp::current_tonemapper() == TonemapperAlgorithm::PPC ||
				ltp::current_tonemapper() == TonemapperAlgorithm::PPC_RGB) {
				SliderFloat("PPC Toe Strength", &ppcv.toe_strength, 0.0f, 1.0f);
				SliderFloat("PPC Toe Length", &ppcv.toe_length, 0.0f, 1.0f);
				SliderFloat("PPC Shoulder Angle", &ppcv.shoulder_angle, 0.0f, 1.0f);
				SliderFloat("PPC Shoulder Length", &ppcv.shoulder_length, 0.0f, 10.0f);
				SliderFloat("PPC Shoulder Strength", &ppcv.shoulder_strength, 0.0f, 1.0f);
			}

			if (profile_list.size()>1) {
				with_CollapsingHeader("Load lighting profile...") {
					for (const auto &s : profile_list) {
						if (Button(s.c_str(), ImVec2(-FLT_MIN, GetTextLineHeight() * 2))) {
							ltp::switch_to(s);

							// Avoid immediately overwriting the selected profile's values with
							// stale slider state captured before the profile switch.
							skip_setting_light_options_this_frame = true;
						}
					}
				}
			}
		}

		if (The_mission.volumetrics) {
			with_CollapsingHeader("Volumetric nebula options")
			{
				bool set_to_origin = static_cast<bool>(volumetrics_pos_backup);
				Checkbox("Move Volumetrics to Origin", &set_to_origin);
				if (set_to_origin != static_cast<bool>(volumetrics_pos_backup)) {
					if (set_to_origin) {
						volumetrics_pos_backup = The_mission.volumetrics->pos;
						The_mission.volumetrics->pos = vec3d ZERO_VECTOR;
					}
					else {
						The_mission.volumetrics->pos = *volumetrics_pos_backup;
						volumetrics_pos_backup.reset();
					}
				}
				Separator();
				Text("Quality Settings:");
				Checkbox("Enable Edge Smoothing", &The_mission.volumetrics->doEdgeSmoothing);
				SliderInt("Steps Until Opaque", &The_mission.volumetrics->steps, 2, 30);
				SliderInt("Steps Towards Sun", &The_mission.volumetrics->globalLightSteps, 2, 10);
				Separator();
				Text("Visibility Settings:");
				SliderFloat("Maximum Opacity", &The_mission.volumetrics->alphaLim, 0.0001f, 1.0f, "%.4f", ImGuiSliderFlags_Logarithmic | ImGuiSliderFlags_NoRoundToFormat);
				SliderFloat("Opacity Distance", &The_mission.volumetrics->opacityDistance, 0.1f, 100.0f);
				Separator();
				Text("Emissive Settings:");
				SliderFloat("Emissive Light Spreading", &The_mission.volumetrics->emissiveSpread, 0.1f, 2.0f);
				SliderFloat("Emissive Light Intensity", &The_mission.volumetrics->emissiveIntensity, 0.0f, 2.0f);
				SliderFloat("Emissive Light Falloff", &The_mission.volumetrics->emissiveFalloff, 0.1f, 2.0f);
				Separator();
				Text("Global Light Settings:");
				SliderFloat("Henyey-Greenstein", &The_mission.volumetrics->henyeyGreensteinCoeff, -1.0f, 1.0f);
				SliderFloat("Sun Falloff Factor", &The_mission.volumetrics->globalLightDistanceFactor, 0.1f, 3.0f);
				Separator();
				Text("Noise Settings:");
				Checkbox("Noise Active", &The_mission.volumetrics->noiseActive);
				SliderFloat("Noise Intensity", &The_mission.volumetrics->noiseColorIntensity, 0.0f, 3.0f);
				SliderFloat("Noise Scale Base", &std::get<0>(The_mission.volumetrics->noiseScale), 1.0f, 50.0f);
				SliderFloat("Noise Scale Sub", &std::get<1>(The_mission.volumetrics->noiseScale), 1.0f, 50.0f);
			}
		}

		if (getLabManager()->Renderer->currentMissionBackground != LAB_MISSION_NONE_STRING) {
			if (Button("Export environment cubemap", ImVec2(-FLT_MIN, GetTextLineHeight()*2))) {
				gr_dump_envmap(getLabManager()->Renderer->currentMissionBackground.c_str());
			}
		}

		if (graphics_options_changed()) {
			if (Button("Reset graphics settings", ImVec2(-FLT_MIN, GetTextLineHeight() * 2))) {
				getLabManager()->resetGraphicsSettings();

				// In order to make the reset button work, we can't set anything here this frame; we'll wait until
				// the next run through this method to update the state then.
				skip_setting_light_options_this_frame = true;
			}
		}
	}

	if (!skip_setting_light_options_this_frame) {
		getLabManager()->Flags.set(ManagerFlags::ModelRotationEnabled, enable_model_rotation);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowInsignia, show_insignia);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowDamageLightning, show_damage_lightning);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::HidePostProcessing, hide_post_processing);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoDiffuseMap, !diffuse_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoGlowMap, !glow_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoSpecularMap, !spec_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoReflectMap, !reflect_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoEnvMap, !env_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoNormalMap, !normal_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoHeightMap, !height_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoMiscMap, !misc_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoAOMap, !ao_map);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoGlowpoints, no_glowpoints);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowWireframe, use_wireframe_rendering);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoLighting, no_lighting);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowFullDetail, show_full_detail);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowThrusters, show_thrusters);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowAfterburners, show_afterburners || getLabManager()->Lab_thrust_afterburn);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowWeapons, show_weapons);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::ShowEmissiveLighting, show_emissive_lighting);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::MoveSubsystems, animate_subsystems);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::NoParticles, no_particles);
		getLabManager()->Renderer->setRenderFlag(LabRenderFlag::UseOrthographicProjection, use_orthographic_projection);
		getLabManager()->Renderer->setShowOrientationWidget(show_orientation_widget);
		getLabManager()->Renderer->setEmissiveFactor(emissive_factor);
		getLabManager()->Renderer->setAmbientFactor(ambient_factor);
		getLabManager()->Renderer->setLightFactor(light_factor);
		getLabManager()->Renderer->setBloomLevel(bloom_level);
		getLabManager()->Renderer->setExposureLevel(exposure_level);
		getLabManager()->Renderer->setPPCValues(ppcv);
	}
}

void LabUi::do_triggered_anim(animation::ModelAnimationTriggerType type,
	const SCP_string& name,
	bool direction,
	int subtype) const
{
	if (getLabManager()->isSafeForShips()) {
		auto shipp = &Ships[Objects[getLabManager()->CurrentObject].instance];

		if (subtype != animation::ModelAnimationSet::SUBTYPE_DEFAULT)
			Ship_info[shipp->ship_info_index]
				.animations.getAll(model_get_instance(shipp->model_instance_num), type, subtype, true)
				.start(direction ? animation::ModelAnimationDirection::RWD : animation::ModelAnimationDirection::FWD);
		else
			Ship_info[shipp->ship_info_index]
				.animations.get(model_get_instance(shipp->model_instance_num), type, name)
				.start(direction ? animation::ModelAnimationDirection::RWD : animation::ModelAnimationDirection::FWD);
	}
}

#define IMGUI_TABLE_ENTRY(colA, colB)     \
	TableNextRow();                \
	TableSetColumnIndex(0);        \
	TextUnformatted(colA);		  \
	TableSetColumnIndex(1);		  \
	TextUnformatted(colB);         \


static void build_ship_table_info_txtbox(ship_info* sip)
{
	with_TreeNode("Table information")
	{
		// since this dialog is closed when the displayed object changes, we need to track whether or not
		// the text needs to be updated separately
		static SCP_string table_text;
		static int old_class = getLabManager()->CurrentClass;

		if (table_text.length() == 0 || old_class != getLabManager()->CurrentClass) {
			table_text = get_ship_table_text(sip);
			old_class = getLabManager()->CurrentClass;
		}

		InputTextMultiline("##table_text",
			table_text.data(),
			table_text.length(),
			ImVec2(-FLT_MIN, GetTextLineHeight() * 16),
			ImGuiInputTextFlags_ReadOnly);
	}
}

static void build_weapon_table_info_txtbox(weapon_info* wip)
{
	with_TreeNode("Table information")
	{
		// Cache result across frames for performance
		static SCP_string table_text;
		static int old_class = getLabManager()->CurrentClass;

		if (table_text.length() == 0 || old_class != getLabManager()->CurrentClass) {
			table_text = get_weapon_table_text(wip);
			old_class = getLabManager()->CurrentClass;
		}

		InputTextMultiline("##weapon_table_text",
			table_text.data(),
			table_text.length(),
			ImVec2(-FLT_MIN, GetTextLineHeight() * 16),
			ImGuiInputTextFlags_ReadOnly);
	}
}

void LabUi::build_model_info_box_actual(ship_info* sip, polymodel* pm) const
{
	ImGuiTableFlags flags = ImGuiTableFlags_Borders | ImGuiTableFlags_RowBg;

	with_Table("model info", 2, flags)
	{
		IMGUI_TABLE_ENTRY("Model File", sip->pof_file)
		IMGUI_TABLE_ENTRY("Target model", sip->pof_file_hud)
		IMGUI_TABLE_ENTRY("Techroom model", sip->pof_file_tech)
		IMGUI_TABLE_ENTRY("Cockpit model", sip->cockpit_pof_file)
		IMGUI_TABLE_ENTRY("POF version", std::to_string(pm->version).c_str());
		IMGUI_TABLE_ENTRY("Materials used", std::to_string(pm->n_textures).c_str());
		IMGUI_TABLE_ENTRY("Radius", std::to_string(pm->core_radius).c_str());
	}
}

void LabUi::build_model_info_box(ship_info* sip, polymodel* pm) const {
	with_TreeNode("Model information")
	{
		build_model_info_box_actual(sip, pm);
	}
}

void render_subsystem(ship_subsys* ss, object* objp)
{
	SCP_string buf;

	auto pmi = model_get_instance(Ships[objp->instance].model_instance_num);
	auto pm = model_get(pmi->model_num);
	int subobj_num = ss->system_info->subobj_num;

	g3_start_frame(1);
	gr_set_proj_matrix(Proj_fov, gr_screen.clip_aspect, Min_draw_distance, Max_draw_distance);
	gr_set_view_matrix(&Eye_position, &Eye_matrix);

	if (subobj_num != -1) {
		auto bsp = &pm->submodel[subobj_num];

		vec3d front_top_left = bsp->bounding_box[7];
		vec3d front_top_right = bsp->bounding_box[6];
		vec3d front_bot_left = bsp->bounding_box[4];
		vec3d front_bot_right = bsp->bounding_box[5];
		vec3d back_top_left = bsp->bounding_box[3];
		vec3d back_top_right = bsp->bounding_box[2];
		vec3d back_bot_left = bsp->bounding_box[0];
		vec3d back_bot_right = bsp->bounding_box[1];

		gr_set_color(255, 32, 32);

		// get into the frame of reference of the submodel
		int g3_count = 1;
		g3_start_instance_matrix(&objp->pos, &objp->orient, true);
		int mn = subobj_num;
		while ((mn >= 0) && (pm->submodel[mn].parent >= 0)) {
			g3_start_instance_matrix(&pm->submodel[mn].offset, &pmi->submodel[mn].canonical_orient, true);
			g3_count++;
			mn = pm->submodel[mn].parent;
		}

		// draw a cube around the subsystem
		g3_draw_htl_line(&front_top_left, &front_top_right);
		g3_draw_htl_line(&front_top_right, &front_bot_right);
		g3_draw_htl_line(&front_bot_right, &front_bot_left);
		g3_draw_htl_line(&front_bot_left, &front_top_left);

		g3_draw_htl_line(&back_top_left, &back_top_right);
		g3_draw_htl_line(&back_top_right, &back_bot_right);
		g3_draw_htl_line(&back_bot_right, &back_bot_left);
		g3_draw_htl_line(&back_bot_left, &back_top_left);

		g3_draw_htl_line(&front_top_left, &back_top_left);
		g3_draw_htl_line(&front_top_right, &back_top_right);
		g3_draw_htl_line(&front_bot_left, &back_bot_left);
		g3_draw_htl_line(&front_bot_right, &back_bot_right);

		// draw another cube around a gun for a two-part turret
		if ((ss->system_info->turret_gun_sobj >= 0) &&
			(ss->system_info->turret_gun_sobj != ss->system_info->subobj_num)) {
			bsp_info* bsp_turret = &pm->submodel[ss->system_info->turret_gun_sobj];

			front_top_left = bsp_turret->bounding_box[7];
			front_top_right = bsp_turret->bounding_box[6];
			front_bot_left = bsp_turret->bounding_box[4];
			front_bot_right = bsp_turret->bounding_box[5];
			back_top_left = bsp_turret->bounding_box[3];
			back_top_right = bsp_turret->bounding_box[2];
			back_bot_left = bsp_turret->bounding_box[0];
			back_bot_right = bsp_turret->bounding_box[1];

			g3_start_instance_matrix(&bsp_turret->offset,
				&pmi->submodel[ss->system_info->turret_gun_sobj].canonical_orient,
				true);

			g3_draw_htl_line(&front_top_left, &front_top_right);
			g3_draw_htl_line(&front_top_right, &front_bot_right);
			g3_draw_htl_line(&front_bot_right, &front_bot_left);
			g3_draw_htl_line(&front_bot_left, &front_top_left);

			g3_draw_htl_line(&back_top_left, &back_top_right);
			g3_draw_htl_line(&back_top_right, &back_bot_right);
			g3_draw_htl_line(&back_bot_right, &back_bot_left);
			g3_draw_htl_line(&back_bot_left, &back_top_left);

			g3_draw_htl_line(&front_top_left, &back_top_left);
			g3_draw_htl_line(&front_top_right, &back_top_right);
			g3_draw_htl_line(&front_bot_left, &back_bot_left);
			g3_draw_htl_line(&front_bot_right, &back_bot_right);

			g3_done_instance(true);
		}

		for (int i = 0; i < g3_count; i++)
			g3_done_instance(true);
	} else {
		vec3d subsys_position;
		vm_vec_add(&subsys_position, &objp->pos, &ss->system_info->pnt);
		vec3d rotated_position;
		vm_vec_unrotate(&rotated_position, &subsys_position, &objp->orient);

		color c;
		gr_init_alphacolor(&c, 255, 32, 32, 128);

		g3_draw_htl_sphere(&c,
			&rotated_position,
			ss->system_info->radius,
			ALPHA_BLEND_ALPHA_BLEND_ALPHA,
			ZBUFFER_TYPE_FULL);
	}

	gr_end_proj_matrix();
	gr_end_view_matrix();
	g3_end_frame();
}

void LabUi::build_subsystem_list(object* objp, ship* shipp) const {
	with_TreeNode("Subsystems")
	{
		size_t subsys_index = 0;
		static SCP_vector<bool> show_subsys;
		static int ship_class_idx = shipp->ship_info_index;
		if (ship_class_idx != shipp->ship_info_index) {
			ship_class_idx = shipp->ship_info_index;
			show_subsys.clear();
		}

		for (auto cur_subsys = GET_FIRST(&shipp->subsys_list); cur_subsys != END_OF_LIST(&shipp->subsys_list);
			 cur_subsys = GET_NEXT(cur_subsys)) {
			if (show_subsys.size() <= subsys_index)
				show_subsys.push_back(false);

			auto subsys_name_tmp = cur_subsys->sub_name;
			if (strlen(subsys_name_tmp) == 0)
				subsys_name_tmp = cur_subsys->system_info->name;

			SCP_string subsys_name;
			sprintf(subsys_name, "%s (%i)", subsys_name_tmp, (int)subsys_index);

			build_subsystem_list_entry(subsys_name, show_subsys, subsys_index, cur_subsys, objp, shipp);

			subsys_index++;
		}
	}
}

void LabUi::build_subsystem_list_entry(SCP_string& subsys_name,
	SCP_vector<bool>& show_subsys,
	const size_t& subsys_index,
	ship_subsys* cur_subsys,
	object* objp,
	ship* shipp) const
{
	with_TreeNode(subsys_name.c_str())
	{
		SCP_string node_name;
		sprintf(node_name, "Highlight system##%s", subsys_name.c_str());

		auto display_this = show_subsys[subsys_index] == true;

		Checkbox(node_name.c_str(), &display_this);

		if (display_this) {
			render_subsystem(cur_subsys, objp);
		}

		show_subsys[subsys_index] = display_this;

		sprintf(node_name, "Destroy system##%s", subsys_name.c_str());

		if (Button(node_name.c_str())) {
			cur_subsys->current_hits = 0;
			do_subobj_destroyed_stuff(shipp, cur_subsys, nullptr);
		}
	}
}

void LabUi::build_weapon_options(ship* shipp) const {
	with_TreeNode("Primaries")
	{
		auto bank = 0;
		for (auto& primary_slot : shipp->weapons.primary_bank_weapons) {
			if (primary_slot != -1) {
				auto wip = &Weapon_info[primary_slot];

				SCP_string text;
				sprintf(text, "##Primary bank %i", bank);

				build_primary_weapon_combobox(text, wip, primary_slot);
				SameLine();
				SCP_string cb_text;
				sprintf(cb_text, "Fire bank %i", bank);
				Checkbox(cb_text.c_str(), &getLabManager()->FirePrimaries[bank]);

				bank++;
			}
		}
	}

	with_TreeNode("Secondaries")
	{
		auto bank = 0;
		for (auto& secondary_slot : shipp->weapons.secondary_bank_weapons) {
			if (secondary_slot != -1) {
				auto wip = &Weapon_info[secondary_slot];

				SCP_string text;
				sprintf(text, "##Secondary bank %i", bank);
				build_secondary_weapon_combobox(text, wip, secondary_slot);
				SameLine();
				SCP_string cb_text;
				sprintf(cb_text, "Fire bank %i##secondary", bank);
				Checkbox(cb_text.c_str(), &getLabManager()->FireSecondaries[bank]);

				bank++;
			}
		}
	}

	with_TreeNode("Turrets")
	{
		auto subsys_idx = 0; // unique IDs

		for (auto* subsys = GET_FIRST(&shipp->subsys_list); subsys != END_OF_LIST(&shipp->subsys_list);
			 subsys = GET_NEXT(subsys)) {
			if (subsys->system_info->type != SUBSYSTEM_TURRET)
				continue;

			SCP_string label;
			sprintf(label, "Turret %i - %s", subsys_idx, subsys->system_info->subobj_name);

			if (TreeNodeEx(label.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) {

				Spacing();

				// Find the turret in FireTurrets
				auto it = std::find_if(getLabManager()->FireTurrets.begin(),
					getLabManager()->FireTurrets.end(),
					[subsys](const auto& tuple) { return std::get<0>(tuple) == subsys; });

				int mode = 0; // Default to UVEC aligned
				bool fire_now = false;

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

				const char* aiming_options[] = {"Random", "UVEC Aligned", "Initial"};

				// In the lab we need to force fire on target because we're faking having an actual target
				subsys->system_info->flags.set(Model::Subsystem_Flags::Fire_on_target);

				// If already added, load current mode + fire state
				if (it != getLabManager()->FireTurrets.end()) {
					mode = static_cast<int>(std::get<1>(*it));
					fire_now = std::get<2>(*it);
				}

				// Fire checkbox
				SCP_string cb_label;
				sprintf(cb_label, "Fire Turret##turret%i", subsys_idx);
				Checkbox(cb_label.c_str(), &fire_now);

				// Mode dropdown
				ImGui::TextUnformatted("Turret Aiming:");
				ImGui::SameLine(150.0f); // Start combo box at 150px
				ImGui::SetNextItemWidth(200.0f);
				ImGui::Combo("##AimingMode", &mode, aiming_options, multipart ? 3 : 2);

				auto modeToAimType = [multipart](int this_mode) -> LabTurretAimType {
					switch (this_mode) {
					case 0:
						return LabTurretAimType::RANDOM;
					case 1:
						return LabTurretAimType::UVEC;
					case 2:
						if (multipart) {
							return LabTurretAimType::INITIAL;
						} else {
							return LabTurretAimType::UVEC;
						}
					default:
						return LabTurretAimType::RANDOM; // fallback
					}
				};

				// Update or add to FireTurrets
				if (it == getLabManager()->FireTurrets.end()) {
					getLabManager()->FireTurrets.emplace_back(subsys, modeToAimType(mode), fire_now);

					// Set timestamps to allow turret to fire immediately
					subsys->turret_next_fire_stamp = timestamp(0);
					auto& weps = subsys->weapons;
					for (auto& stamp : weps.next_primary_fire_stamp) {
						stamp = timestamp(0);
					}

					for (auto& stamp : weps.next_secondary_fire_stamp) {
						stamp = timestamp(0);
					}
				} else {
					std::get<1>(*it) = modeToAimType(mode);
					std::get<2>(*it) = fire_now;
				}

				auto& weps = subsys->weapons;

				// PRIMARY BANKS
				for (int bank = 0; bank < weps.num_primary_banks; ++bank) {
					SCP_string text;
					sprintf(text, "##TurretPrimaryBank%i_%i", subsys_idx, bank);

					auto* wip = &Weapon_info[weps.primary_bank_weapons[bank]];
					build_primary_weapon_combobox(text, wip, weps.primary_bank_weapons[bank]);
				}

				// SECONDARY BANKS
				for (int bank = 0; bank < weps.num_secondary_banks; ++bank) {
					SCP_string text;
					sprintf(text, "##TurretSecondaryBank%i_%i", subsys_idx, bank);

					auto* wip = &Weapon_info[weps.secondary_bank_weapons[bank]];
					build_secondary_weapon_combobox(text, wip, weps.secondary_bank_weapons[bank]);
				}

				TreePop(); // Close this turret node
			}

			subsys_idx++;
		}
	}
}

void LabUi::build_primary_weapon_combobox(SCP_string& text,
	weapon_info* wip,
	int& primary_slot) const
{
	with_Combo(text.c_str(), wip->name)
	{
		for (size_t i = 0; i < Weapon_info.size(); i++) {
			if (Weapon_info[i].subtype == WP_MISSILE)
				continue;
			bool is_selected = i == (size_t)primary_slot;
			if (Selectable(Weapon_info[i].name, is_selected))
				primary_slot = (int)i;
			if (is_selected)
				SetItemDefaultFocus();
		}
	}
}

void LabUi::build_secondary_weapon_combobox(SCP_string& text, weapon_info* wip, int& secondary_slot) const
{
	with_Combo(text.c_str(), wip->name)
	{
		for (size_t i = 0; i < Weapon_info.size(); i++) {
			if (Weapon_info[i].subtype != WP_MISSILE)
				continue;
			bool is_selected = i == (size_t)secondary_slot;
			if (Selectable(Weapon_info[i].name, is_selected))
				secondary_slot = (int)i;
			if (is_selected)
				SetItemDefaultFocus();
		}
	}
}

void LabUi::reset_animations()
{
	// With full animation support for docking stages and fighter bays it's honestly just easier to reload the current object
	getLabManager()->changeDisplayedObject(getLabManager()->CurrentMode, getLabManager()->CurrentClass, getLabManager()->CurrentSubtype);
}

void LabUi::maybe_show_animation_category(const SCP_vector<animation::ModelAnimationSet::RegisteredTrigger>& anim_triggers,
	animation::ModelAnimationTriggerType trigger_type, SCP_string label) const
{
	if (std::any_of(anim_triggers.begin(), anim_triggers.end(), [trigger_type](animation::ModelAnimationSet::RegisteredTrigger t) {
			return t.type == trigger_type;
		})) {
		with_TreeNode(label.c_str())
		{
			int count = 1;
			for (const auto& anim_trigger : anim_triggers) {
				if (anim_trigger.type == trigger_type) {

					SCP_string button_label = anim_trigger.name;
					switch (trigger_type) {
					case animation::ModelAnimationTriggerType::DockBayDoor:
						button_label += "Trigger Bay Door Animation " + std::to_string(count++);
						break;
					case animation::ModelAnimationTriggerType::Docking_Stage1:
						button_label += "Trigger Docking Stage 1 Animation " + std::to_string(count++);
						break;
					case animation::ModelAnimationTriggerType::Docking_Stage2:
						button_label += "Trigger Docking Stage 2 Animation " + std::to_string(count++);
						break;
					case animation::ModelAnimationTriggerType::Docking_Stage3:
						button_label += "Trigger Docking Stage 3 Animation " + std::to_string(count++);
						break;
					case animation::ModelAnimationTriggerType::Docked:
						button_label += "Trigger Docked Animation " + std::to_string(count++);
						break;
					case animation::ModelAnimationTriggerType::Scripted:
						button_label += "Trigger Scripted Animation " + std::to_string(count++);
						break;
					default:
						// We really shouldn't be here, but just in case
						Assertion(false, "Unexpected animation trigger type %d", static_cast<int>(trigger_type));
						button_label += "Trigger Animation " + std::to_string(count++);
						break;
					}

					if (Button(button_label.c_str())) {
						auto& scripted_triggers = manual_animation_triggers[trigger_type];
						auto direction = scripted_triggers[anim_trigger.name];
						do_triggered_anim(trigger_type,
							anim_trigger.name,
							direction,
							anim_trigger.subtype);
						scripted_triggers[anim_trigger.name] = !scripted_triggers[anim_trigger.name];
					}
				}
			}
		}
	}
}

void LabUi::build_dock_test_options(ship* shipp)
{
	with_TreeNode("Docking Tests")
	{
		auto dockee_dock_map = get_docking_point_map(Ship_info[shipp->ship_info_index].model_num);

		if (!dockee_dock_map.empty()) {

			if (ImGui::BeginCombo("Docker Ship Class", Ship_info[getLabManager()->DockerClass].name)) {
				for (size_t i = 0; i < Ship_info.size(); ++i) {
					bool is_selected = (static_cast<int>(i) == getLabManager()->DockerClass);
					if (ImGui::Selectable(Ship_info[i].name, is_selected)) {
						getLabManager()->DockerClass = static_cast<int>(i);
						// Load model if needed
						auto& dsip = Ship_info[getLabManager()->DockerClass];
						if (dsip.model_num < 0) {
							dsip.model_num = model_load(dsip.pof_file, &dsip);
						}
						auto new_dock_map = get_docking_point_map(dsip.model_num);

						// Auto-select first available dockpoint (or clear if none)
						if (!new_dock_map.empty()) {
							getLabManager()->DockerDockPoint = new_dock_map.begin()->second;
						} else {
							getLabManager()->DockerDockPoint.clear();
						}
					}
					if (is_selected)
						ImGui::SetItemDefaultFocus();
				}
				ImGui::EndCombo();
			}

			auto& dsip = Ship_info[getLabManager()->DockerClass];
			if (dsip.model_num < 0) {
				dsip.model_num = model_load(dsip.pof_file, &dsip);
			}
			auto dock_map = get_docking_point_map(dsip.model_num);

			// Ensure DockerDockPoint is initialized once based on the current DockerClass
			if (getLabManager()->DockerDockPoint.empty()) {
				if (!dock_map.empty()) {
					getLabManager()->DockerDockPoint = dock_map.begin()->second;
				}
			}

			const char* docker_label = getLabManager()->DockerDockPoint.c_str();

			if (ImGui::BeginCombo("Docker Dockpoint", docker_label)) {
				if (!dock_map.empty()) {
					for (const auto& [index, name] : dock_map) {
						bool is_selected = (name == getLabManager()->DockerDockPoint);
						if (ImGui::Selectable(name.c_str(), is_selected)) {
							getLabManager()->DockerDockPoint = name;
						}
						if (is_selected) {
							ImGui::SetItemDefaultFocus();
						}
					}
				}
				ImGui::EndCombo();
			}

			// Auto-select first dockpoint if none currently selected
			if (getLabManager()->DockeeDockPoint.empty()) {
				getLabManager()->DockeeDockPoint = dockee_dock_map.begin()->second;
			}

			const char* dockee_label = getLabManager()->DockeeDockPoint.c_str();

			if (ImGui::BeginCombo("Dockee Dockpoint", dockee_label)) {
				for (const auto& [index, name] : dockee_dock_map) {
					bool is_selected = (name == getLabManager()->DockeeDockPoint);
					if (ImGui::Selectable(name.c_str(), is_selected)) {
						getLabManager()->DockeeDockPoint = name;
					}
					if (is_selected) {
						ImGui::SetItemDefaultFocus();
					}
				}
				ImGui::EndCombo();
			}

			if (Button("Begin Docking Test")) {
				getLabManager()->beginDockingTest();
			}

			if (Button("Begin Undocking Test")) {
				getLabManager()->beginUndockingTest();
			}
		}
	}
}

void LabUi::build_bay_test_options(ship_info* sip)
{
	with_TreeNode("Fighterbay Tests")
	{
		auto bay_paths_map = get_bay_paths_map(sip->model_num);

		if (!bay_paths_map.empty()) {

			if (ImGui::BeginCombo("Bay Arrival/Departure Ship Class", Ship_info[getLabManager()->BayClass].name)) {
				for (size_t i = 0; i < Ship_info.size(); ++i) {
					bool is_selected = (static_cast<int>(i) == getLabManager()->BayClass);
					if (ImGui::Selectable(Ship_info[i].name, is_selected)) {
						getLabManager()->BayClass = static_cast<int>(i);
					}
					if (is_selected)
						ImGui::SetItemDefaultFocus();
				}
				ImGui::EndCombo();
			}

			auto& bsip = Ship_info[getLabManager()->BayClass];

			// Load the model
			if (bsip.model_num < 0) {
				bsip.model_num = model_load(bsip.pof_file, &bsip);
			}

			// Pick the first entry in bay_paths_map if it's set to 0
			if (getLabManager()->BayPathMask == 0) {
				int first_idx = bay_paths_map.begin()->first;
				getLabManager()->BayPathMask = (1u << first_idx);
			}

			// Get the preview label by finding the one bit that's set
			const char* path_label = "??";
			for (const auto& [idx, name] : bay_paths_map) {
				if (getLabManager()->BayPathMask & (1u << idx)) {
					path_label = name.c_str();
					break;
				}
			}

			if (ImGui::BeginCombo("Bay Path", path_label)) {
				for (const auto& [idx, name] : bay_paths_map) {
					bool is_selected = (getLabManager()->BayPathMask & (1u << idx)) != 0;
					if (ImGui::Selectable(name.c_str(), is_selected)) {
						getLabManager()->BayPathMask = (1u << idx);
					}
					if (is_selected) {
						ImGui::SetItemDefaultFocus();
					}
				}
				ImGui::EndCombo();
			}

			static const char* mode_names[] = {"Arrival", "Departure"};
			int mode_idx = static_cast<int>(getLabManager()->BayTestMode);

			Assertion(mode_idx == 0 || mode_idx == 1, "Bay test mode is not valid!"); // only two valid modes

			if (ImGui::BeginCombo("Bay Test Mode", mode_names[mode_idx])) {
				// Arrival
				if (ImGui::Selectable("Arrival", getLabManager()->BayTestMode == BayMode::Arrival)) {
					getLabManager()->BayTestMode = BayMode::Arrival;
				}

				// Departure
				if (ImGui::Selectable("Departure", getLabManager()->BayTestMode == BayMode::Departure)) {
					getLabManager()->BayTestMode = BayMode::Departure;
				}

				ImGui::SetItemDefaultFocus();
				ImGui::EndCombo();
			}

			if (Button("Begin Bay Path Test")) {
				getLabManager()->beginBayTest();
			}
		}
	}
}

void LabUi::build_animation_options(ship* shipp, ship_info* sip) const
{
	with_TreeNode("Animations")
	{
		const auto& anim_triggers = sip->animations.getRegisteredTriggers();

		if (Button("Reset animations")) {
			reset_animations();
		}

		if (shipp->weapons.num_primary_banks > 0) {
			create_primary_weapon_anim_node(shipp, sip);
		}

		if (shipp->weapons.num_secondary_banks > 0) {
			create_secondary_weapon_anim_node(shipp, sip);
		}

		if (std::any_of(anim_triggers.begin(),
				anim_triggers.end(),
				[](animation::ModelAnimationSet::RegisteredTrigger t) {
					return t.type == animation::ModelAnimationTriggerType::Afterburner;
				})) {
			create_afterburner_animation_node(anim_triggers);
		}

		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::TurretFiring,
			"Turret firing##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::TurretFired,
			"Turret fired##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::Scripted,
			"Scripted animations##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::DockBayDoor,
			"Dock bay door##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::Docking_Stage1,
			"Docking stage 1##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::Docking_Stage2,
			"Docking stage 2##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::Docking_Stage3,
			"Docking stage 3##anims");
		maybe_show_animation_category(anim_triggers,
			animation::ModelAnimationTriggerType::Docked,
			"Docked animations##anims");
	}
}

void LabUi::create_afterburner_animation_node(
	const SCP_vector<animation::ModelAnimationSet::RegisteredTrigger>& anim_triggers) const
{
	with_TreeNode("Afterburner")
	{
		if (Button("Trigger afterburner animations")) {
			for (const auto& anim_trigger : anim_triggers) {
				if (anim_trigger.type == animation::ModelAnimationTriggerType::Afterburner) {
					auto& ab_triggers = manual_animation_triggers[animation::ModelAnimationTriggerType::Afterburner];
					do_triggered_anim(animation::ModelAnimationTriggerType::Afterburner,
						anim_trigger.name,
						ab_triggers[anim_trigger.name],
						anim_trigger.subtype);
					ab_triggers[anim_trigger.name] = !ab_triggers[anim_trigger.name];
				}
			}
		}
	}
}

void LabUi::create_secondary_weapon_anim_node(
	ship* shipp,
	ship_info* sip) const
{
	with_TreeNode("Secondary Weapons##Anims")
	{
		for (auto i = 0; i < shipp->weapons.num_secondary_banks; ++i) {
			SCP_string button_label;
			sprintf(button_label, "Trigger animation for secondary bank %i", i);
			if (Button(button_label.c_str())) {
				sip->animations
					.getAll(model_get_instance(shipp->model_instance_num),
						animation::ModelAnimationTriggerType::SecondaryBank,
						i)
					.start(triggered_secondary_banks[i] ? animation::ModelAnimationDirection::RWD
														: animation::ModelAnimationDirection::FWD);
				triggered_secondary_banks[i] = !triggered_secondary_banks[i];
			}
		}
	}
}

void LabUi::create_primary_weapon_anim_node(
	ship* shipp,
	ship_info* sip) const
{
	with_TreeNode("Primary Weapons##Anims")
	{
		for (auto i = 0; i < shipp->weapons.num_primary_banks; ++i) {
			SCP_string button_label;
			sprintf(button_label, "Trigger animation for primary bank %i", i);
			if (Button(button_label.c_str())) {
				sip->animations
					.getAll(model_get_instance(shipp->model_instance_num),
						animation::ModelAnimationTriggerType::PrimaryBank,
						i)
					.start(triggered_primary_banks[i] ? animation::ModelAnimationDirection::RWD
													  : animation::ModelAnimationDirection::FWD);
				triggered_primary_banks[i] = !triggered_primary_banks[i];
			}
		}
	}
}

void LabUi::show_object_options() const
{
	
	with_Window("Object Information")
	{
		if (getLabManager()->CurrentMode == LabMode::Ship && getLabManager()->isSafeForShips()) {
			auto objp = &Objects[getLabManager()->CurrentObject];
			auto shipp = &Ships[objp->instance];
			auto sip = &Ship_info[shipp->ship_info_index];
			auto pm = model_get(sip->model_num);

			with_CollapsingHeader(sip->name)
			{
				build_ship_table_info_txtbox(sip);

				build_model_info_box(sip, pm);

				build_subsystem_list(objp, shipp);
			}

			with_CollapsingHeader("Object actions")
			{
				if (getLabManager()->isSafeForShips()) {
					if (Button("Destroy ship")) {
						if (Objects[getLabManager()->CurrentObject].type == OBJ_SHIP) {
							// If we have testing objects, delete them
							getLabManager()->deleteTestObjects();

							auto obj = &Objects[getLabManager()->CurrentObject];

							obj->flags.remove(Object::Object_Flags::Player_ship);
							ship_self_destruct(obj);
						}
					}

					build_dock_test_options(shipp);

					build_bay_test_options(sip);

					build_animation_options(shipp, sip);
				}
			}

			with_CollapsingHeader("Weapons")
			{
				build_weapon_options(shipp);
			}
		} else if (getLabManager()->CurrentMode == LabMode::Weapon && getLabManager()->CurrentClass >= 0) {
			auto wip = &Weapon_info[getLabManager()->CurrentClass];

			with_CollapsingHeader("Weapon Info")
			{
				build_weapon_table_info_txtbox(wip);
			}
			
			with_CollapsingHeader("Object Actions")
			{
				Checkbox("Progress weapon lifetime", &getLabManager()->AllowWeaponDestruction);

				if (VALID_FNAME(wip->tech_model)) {
					if (Checkbox("Show Tech Model", &getLabManager()->ShowingTechModel)) {
						getLabManager()->changeDisplayedObject(LabMode::Weapon, getLabManager()->CurrentClass);
					}
				}
			}
		} else if (getLabManager()->CurrentMode == LabMode::Asteroid && getLabManager()->CurrentClass >= 0) {
			const auto& info = Asteroid_info[getLabManager()->CurrentClass];

			with_CollapsingHeader("Object Info")
			{
				static SCP_string table_text;
				static int old_class = -1;

				if (table_text.empty() || old_class != getLabManager()->CurrentClass) {
					table_text = get_asteroid_table_text(&info);
					old_class = getLabManager()->CurrentClass;
				}

				InputTextMultiline("##asteroid_table_text",
					table_text.data(),
					table_text.length(),
					ImVec2(-FLT_MIN, GetTextLineHeight() * 16),
					ImGuiInputTextFlags_ReadOnly);
			}

			with_CollapsingHeader("Object actions")
			{
				if (getLabManager()->isSafeForAsteroids()) {
					if (Button("Destroy object")) {
						if (Objects[getLabManager()->CurrentObject].type == OBJ_ASTEROID) {
							auto obj = &Objects[getLabManager()->CurrentObject];

							if (obj->type == OBJ_ASTEROID) {
								vec3d dummy_pos = obj->pos;
								vec3d dummy_force = ZERO_VECTOR;

								// Apply full asteroid health to guarantee destruction
								asteroid_hit(obj, nullptr, &dummy_pos, obj->hull_strength + 1.0f, &dummy_force);
							}
						}
					}
				}
			}
		} else if (getLabManager()->CurrentMode == LabMode::Prop && getLabManager()->CurrentClass >= 0) {
			const auto& info = Prop_info[getLabManager()->CurrentClass];

			with_CollapsingHeader("Object Info")
			{
				static SCP_string table_text;
				static int old_class = -1;

				if (table_text.empty() || old_class != getLabManager()->CurrentClass) {
					table_text = get_prop_table_text(&info);
					old_class = getLabManager()->CurrentClass;
				}

				InputTextMultiline("##prop_table_text",
					table_text.data(),
					table_text.length(),
					ImVec2(-FLT_MIN, GetTextLineHeight() * 16),
					ImGuiInputTextFlags_ReadOnly);
			}

			with_CollapsingHeader("Object actions")
			{
				if (getLabManager()->isSafeForProps()) {
					// No actions yet
				}
			}
		}
	}
}

