package com.github.exopandora.shouldersurfing.config;

import com.github.exopandora.shouldersurfing.api.client.IClientConfig;
import com.github.exopandora.shouldersurfing.api.model.CrosshairType;
import com.github.exopandora.shouldersurfing.api.model.CrosshairVisibility;
import com.github.exopandora.shouldersurfing.api.model.Perspective;
import com.github.exopandora.shouldersurfing.api.model.PickOrigin;
import com.github.exopandora.shouldersurfing.api.model.PickVector;
import com.github.exopandora.shouldersurfing.api.model.TurningMode;
import com.github.exopandora.shouldersurfing.api.model.ViewBobbingMode;
import com.github.exopandora.shouldersurfing.client.ShoulderSurfingImpl;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.Identifier;
import net.minecraft.world.item.ItemUseAnimation;
import net.minecraft.world.item.Items;
import net.neoforged.neoforge.common.ModConfigSpec;
import net.neoforged.neoforge.common.ModConfigSpec.BooleanValue;
import net.neoforged.neoforge.common.ModConfigSpec.ConfigValue;
import net.neoforged.neoforge.common.ModConfigSpec.DoubleValue;
import net.neoforged.neoforge.common.ModConfigSpec.IntValue;
import org.apache.commons.lang3.tuple.Pair;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import static com.github.exopandora.shouldersurfing.ShoulderSurfingCommon.MOD_ID;

public class Config
{
	public static final ModConfigSpec CLIENT_SPEC;
	public static final ClientConfig CLIENT;
	
	static
	{
		Pair<ClientConfig, ModConfigSpec> pair = new ModConfigSpec.Builder().configure(ClientConfig::new);
		CLIENT_SPEC = pair.getRight();
		CLIENT = pair.getLeft();
	}
	
	public static class ClientConfig implements IClientConfig
	{
		private final DoubleValue offsetX;
		private final DoubleValue offsetY;
		private final DoubleValue offsetZ;
		
		private final ConfigValue<List<? extends String>> offsetXPresets;
		private final ConfigValue<List<? extends String>> offsetYPresets;
		private final ConfigValue<List<? extends String>> offsetZPresets;
		
		private final DoubleValue minOffsetX;
		private final DoubleValue minOffsetY;
		private final DoubleValue minOffsetZ;
		
		private final DoubleValue maxOffsetX;
		private final DoubleValue maxOffsetY;
		private final DoubleValue maxOffsetZ;
		
		private final BooleanValue unlimitedOffsetX;
		private final BooleanValue unlimitedOffsetY;
		private final BooleanValue unlimitedOffsetZ;
		
		private final DoubleValue passengerOffsetXMultiplier;
		private final DoubleValue passengerOffsetYMultiplier;
		private final DoubleValue passengerOffsetZMultiplier;
		
		private final DoubleValue sprintOffsetXMultiplier;
		private final DoubleValue sprintOffsetYMultiplier;
		private final DoubleValue sprintOffsetZMultiplier;
		
		private final DoubleValue aimingOffsetXMultiplier;
		private final DoubleValue aimingOffsetYMultiplier;
		private final DoubleValue aimingOffsetZMultiplier;
		
		private final DoubleValue fallFlyingOffsetXMultiplier;
		private final DoubleValue fallFlyingOffsetYMultiplier;
		private final DoubleValue fallFlyingOffsetZMultiplier;
		
		private final DoubleValue climbingOffsetXMultiplier;
		private final DoubleValue climbingOffsetYMultiplier;
		private final DoubleValue climbingOffsetZMultiplier;
		
		private final DoubleValue passengerOffsetXModifier;
		private final DoubleValue passengerOffsetYModifier;
		private final DoubleValue passengerOffsetZModifier;
		
		private final DoubleValue sprintOffsetXModifier;
		private final DoubleValue sprintOffsetYModifier;
		private final DoubleValue sprintOffsetZModifier;
		
		private final DoubleValue aimingOffsetXModifier;
		private final DoubleValue aimingOffsetYModifier;
		private final DoubleValue aimingOffsetZModifier;
		
		private final DoubleValue fallFlyingOffsetXModifier;
		private final DoubleValue fallFlyingOffsetYModifier;
		private final DoubleValue fallFlyingOffsetZModifier;
		
		private final DoubleValue climbingOffsetXModifier;
		private final DoubleValue climbingOffsetYModifier;
		private final DoubleValue climbingOffsetZModifier;
		
		private final DoubleValue keepCameraOutOfHeadMultiplier;
		private final DoubleValue cameraStepSize;
		private final DoubleValue cameraTransitionSpeedMultiplier;
		private final DoubleValue centerCameraWhenLookingDownAngle;
		private final BooleanValue dynamicallyAdjustOffsets;
		private final BooleanValue isCameraDecoupled;
		private final BooleanValue orientCameraOnTeleport;
		private final BooleanValue isFovOverrideEnabled;
		private final DoubleValue fovOverride;
		private final ConfigValue<ViewBobbingMode> viewBobbingMode;
		
		private final DoubleValue cameraDragXMultiplier;
		private final DoubleValue cameraDragYMultiplier;
		private final DoubleValue cameraDragZMultiplier;
		
		private final DoubleValue cameraSwayXMaxAngle;
		private final DoubleValue cameraSwayZMaxAngle;
		private final DoubleValue cameraSwayXMaxVelocity;
		private final DoubleValue cameraSwayZMaxVelocity;
		
		private final BooleanValue replaceDefaultPerspective;
		private final BooleanValue isFirstPersonEnabled;
		private final BooleanValue isThirdPersonFrontEnabled;
		private final BooleanValue isThirdPersonBackEnabled;
		private final ConfigValue<Perspective> defaultPerspective;
		private final BooleanValue rememberLastPerspective;
		
		private final BooleanValue playerTransparency;
		private final BooleanValue turnPlayerTransparentWhenAiming;
		private final DoubleValue hidePlayerWhenLookingUpAngle;
		private final ConfigValue<TurningMode> turningModeWhenUsingItem;
		private final ConfigValue<TurningMode> turningModeWhenAttacking;
		private final ConfigValue<TurningMode> turningModeWhenInteraction;
		private final ConfigValue<TurningMode> turningModeWhenPicking;
		private final IntValue turningLockTime;
		private final BooleanValue playerXRotFollowsCamera;
		private final BooleanValue playerYRotFollowsCamera;
		private final DoubleValue playerYRotFollowAngleLimit;
		private final ConfigValue<PickOrigin> entityPickOrigin;
		private final ConfigValue<PickOrigin> blockPickOrigin;
		private final ConfigValue<PickVector> pickVector;
		
		private final ConfigValue<CrosshairType> crosshairType;
		private final DoubleValue customRaytraceDistance;
		private final BooleanValue useCustomRaytraceDistance;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairHoldItems;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairUseItems;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairHoldItemAnimations;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairUseItemAnimations;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairHoldItemDefaultComponents;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairUseItemDefaultComponents;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairHoldItemComponents;
		private final ConfigValue<List<? extends String>> adaptiveCrosshairUseItemComponents;
		private final Map<Perspective, ConfigValue<CrosshairVisibility>> crosshairVisibility = new HashMap<Perspective, ConfigValue<CrosshairVisibility>>();

		private final BooleanValue showObstructionIndicator;
		private final BooleanValue showObstructionIndicatorWhenAiming;
		private final IntValue obstructionIndicatorMinDistanceToCrosshair;
		private final DoubleValue obstructionIndicatorMaxDistanceToObstruction;
		
		private final BooleanValue centerPlayerSounds;
		
		private final ConfigValue<List<? extends String>> curiosAdaptiveCrosshairItems;
		private final ConfigValue<List<? extends String>> curiosAdaptiveCrosshairDefaultItemComponents;
		private final ConfigValue<List<? extends String>> curiosAdaptiveCrosshairItemComponents;
		
		private boolean requiresSaving = false;
		
		public ClientConfig(ModConfigSpec.Builder builder)
		{
			builder.push("camera");
			builder.push("offset");
			
			this.offsetX = builder
				.comment("Third person camera x-offset.")
				.translation(MOD_ID + ".configuration.camera.offset.offset_x")
				.defineInRange("offset_x", -0.75D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.offsetY = builder
				.comment("Third person camera y-offset.")
				.translation(MOD_ID + ".configuration.camera.offset.offset_y")
				.defineInRange("offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.offsetZ = builder
				.comment("Third person camera z-offset.")
				.translation(MOD_ID + ".configuration.camera.offset.offset_z")
				.defineInRange("offset_z", 4.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.push("presets");
			
			this.offsetXPresets = builder
				.comment("A list of x-offset presets that can be toggled via the 'Toggle X-Offset Presets' keybind. WARNING: Duplicate entries can result in undefined behavior!")
				.translation(MOD_ID + ".configuration.offset.presets.offset_x_presets")
				.defineList("presets_offset_x", ArrayList::new, String::new, ClientConfig::isValidDouble);
			
			this.offsetYPresets = builder
				.comment("A list of y-offset presets that can be toggled via the 'Toggle Y-Offset Presets' keybind. WARNING: Duplicate entries can result in undefined behavior!")
				.translation(MOD_ID + ".configuration.offset.presets.offset_y_presets")
				.defineList("presets_offset_y", ArrayList::new, String::new, ClientConfig::isValidDouble);
			
			this.offsetZPresets = builder
				.comment("A list of z-offset presets that can be toggled via the 'Toggle Z-Offset Presets' keybind. WARNING: Duplicate entries can result in undefined behavior!")
				.translation(MOD_ID + ".configuration.offset.presets.offset_z_presets")
				.defineList("presets_offset_z", ArrayList::new, String::new, ClientConfig::isValidDouble);
			
			builder.pop();
			builder.push("min");
			
			this.minOffsetX = builder
				.comment("When x-offset is limited this is the minimum amount.")
				.translation(MOD_ID + ".configuration.offset.min.min_offset_x")
				.defineInRange("min_offset_x", -3.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.minOffsetY = builder
				.comment("When y-offset is limited this is the minimum amount.")
				.translation(MOD_ID + ".configuration.offset.min.min_offset_y")
				.defineInRange("min_offset_y", -1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.minOffsetZ = builder
				.comment("When z-offset is limited this is the minimum amount.")
				.translation(MOD_ID + ".configuration.offset.min.min_offset_z")
				.defineInRange("min_offset_z", -3.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("max");
			
			this.maxOffsetX = builder
				.comment("When x-offset is limited this is the maximum amount.")
				.translation(MOD_ID + ".configuration.offset.max.max_offset_x")
				.defineInRange("max_offset_x", 3.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.maxOffsetY = builder
				.comment("When y-offset is limited this is the maximum amount.")
				.translation(MOD_ID + ".configuration.offset.max.max_offset_y")
				.defineInRange("max_offset_y", 1.5D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.maxOffsetZ = builder
				.comment("When z-offset is limited this is the maximum amount.")
				.translation(MOD_ID + ".configuration.offset.max.max_offset_z")
				.defineInRange("max_offset_z", 5.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("limits");
			
			this.unlimitedOffsetX = builder
				.comment("Whether or not x-offset adjustment has limits.")
				.translation(MOD_ID + ".configuration.offset.limits.unlimited_offset_x")
				.define("unlimited_offset_x", false);
			
			this.unlimitedOffsetY = builder
				.comment("Whether or not y-offset adjustment has limits.")
				.translation(MOD_ID + ".configuration.offset.limits.unlimited_offset_y")
				.define("unlimited_offset_y", false);
			
			this.unlimitedOffsetZ = builder
				.comment("Whether or not z-offset adjustment has limits.")
				.translation(MOD_ID + ".configuration.offset.limits.unlimited_offset_z")
				.define("unlimited_offset_z", false);
			
			builder.pop();
			builder.push("multiplier");
			builder.push("passenger");
			
			this.passengerOffsetXMultiplier = builder
				.comment("x-offset multiplier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.multiplier.passenger.multiplier_offset_x")
				.defineInRange("multiplier_offset_x", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.passengerOffsetYMultiplier = builder
				.comment("y-offset multiplier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.multiplier.passenger.multiplier_offset_y")
				.defineInRange("multiplier_offset_y", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.passengerOffsetZMultiplier = builder
				.comment("z-offset multiplier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.multiplier.passenger.multiplier_offset_z")
				.defineInRange("multiplier_offset_z", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("sprint");
			
			this.sprintOffsetXMultiplier = builder
				.comment("x-offset multiplier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.multiplier.sprint.multiplier_offset_x")
				.defineInRange("multiplier_offset_x", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.sprintOffsetYMultiplier = builder
				.comment("y-offset multiplier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.multiplier.sprint.multiplier_offset_y")
				.defineInRange("multiplier_offset_y", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.sprintOffsetZMultiplier = builder
				.comment("z-offset multiplier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.multiplier.sprint.multiplier_offset_z")
				.defineInRange("multiplier_offset_z", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("aiming");
			
			this.aimingOffsetXMultiplier = builder
				.comment("x-offset multiplier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.multiplier.aiming.multiplier_offset_x")
				.defineInRange("multiplier_offset_x", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.aimingOffsetYMultiplier = builder
				.comment("y-offset multiplier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.multiplier.aiming.multiplier_offset_y")
				.defineInRange("multiplier_offset_y", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.aimingOffsetZMultiplier = builder
				.comment("z-offset multiplier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.multiplier.aiming.multiplier_offset_z")
				.defineInRange("multiplier_offset_z", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("fall_flying");
			
			this.fallFlyingOffsetXMultiplier = builder
				.comment("x-offset multiplier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.multiplier.fall_flying.multiplier_offset_x")
				.defineInRange("multiplier_offset_x", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.fallFlyingOffsetYMultiplier = builder
				.comment("y-offset multiplier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.multiplier.fall_flying.multiplier_offset_y")
				.defineInRange("multiplier_offset_y", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.fallFlyingOffsetZMultiplier = builder
				.comment("z-offset multiplier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.multiplier.fall_flying.multiplier_offset_z")
				.defineInRange("multiplier_offset_z", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("climbing");
			
			this.climbingOffsetXMultiplier = builder
				.comment("x-offset multiplier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.multiplier.climbing.multiplier_offset_x")
				.defineInRange("multiplier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.climbingOffsetYMultiplier = builder
				.comment("y-offset multiplier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.multiplier.climbing.multiplier_offset_y")
				.defineInRange("multiplier_offset_y", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.climbingOffsetZMultiplier = builder
				.comment("z-offset multiplier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.multiplier.climbing.multiplier_offset_z")
				.defineInRange("multiplier_offset_z", 1.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.pop();
			builder.push("modifiers");
			builder.push("passenger");
			
			this.passengerOffsetXModifier = builder
				.comment("x-offset modifier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.modifier.passenger.modifier_offset_x")
				.defineInRange("modifier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.passengerOffsetYModifier = builder
				.comment("y-offset modifier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.modifier.passenger.modifier_offset_y")
				.defineInRange("modifier_offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.passengerOffsetZModifier = builder
				.comment("z-offset modifier for when the player is a passenger.")
				.translation(MOD_ID + ".configuration.offset.modifier.passenger.modifier_offset_z")
				.defineInRange("modifier_offset_z", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("sprint");
			
			this.sprintOffsetXModifier = builder
				.comment("x-offset modifier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.modifier.sprint.modifier_offset_x")
				.defineInRange("modifier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.sprintOffsetYModifier = builder
				.comment("y-offset modifier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.modifier.sprint.modifier_offset_y")
				.defineInRange("modifier_offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.sprintOffsetZModifier = builder
				.comment("z-offset modifier for when the player is sprinting.")
				.translation(MOD_ID + ".configuration.offset.modifier.sprint.modifier_offset_z")
				.defineInRange("modifier_offset_z", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("aiming");
			
			this.aimingOffsetXModifier = builder
				.comment("x-offset modifier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.modifier.aiming.modifier_offset_x")
				.defineInRange("modifier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.aimingOffsetYModifier = builder
				.comment("y-offset modifier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.modifier.aiming.modifier_offset_y")
				.defineInRange("modifier_offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.aimingOffsetZModifier = builder
				.comment("z-offset modifier for when the player is aiming.")
				.translation(MOD_ID + ".configuration.offset.modifier.aiming.modifier_offset_z")
				.defineInRange("modifier_offset_z", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("fall_flying");
			
			this.fallFlyingOffsetXModifier = builder
				.comment("x-offset modifier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.modifier.fall_flying.modifier_offset_x")
				.defineInRange("modifier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.fallFlyingOffsetYModifier = builder
				.comment("y-offset modifier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.modifier.fall_flying.modifier_offset_y")
				.defineInRange("modifier_offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.fallFlyingOffsetZModifier = builder
				.comment("z-offset modifier for when using Elytra.")
				.translation(MOD_ID + ".configuration.offset.modifier.fall_flying.modifier_offset_z")
				.defineInRange("modifier_offset_z", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.push("climbing");
			
			this.climbingOffsetXModifier = builder
				.comment("x-offset modifier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.modifier.climbing.modifier_offset_x")
				.defineInRange("modifier_offset_x", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.climbingOffsetYModifier = builder
				.comment("y-offset modifier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.modifier.climbing.modifier_offset_y")
				.defineInRange("modifier_offset_y", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.climbingOffsetZModifier = builder
				.comment("z-offset modifier for when the player is climbing.")
				.translation(MOD_ID + ".configuration.offset.modifier.climbing.modifier_offset_z")
				.defineInRange("modifier_offset_z", 0.0D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			builder.pop();
			builder.pop();
			builder.pop();
			
			this.keepCameraOutOfHeadMultiplier = builder
				.comment("The distance multiplier on whether or not to hide the player model if the camera gets too close to it. Set to 0 to disable.")
				.translation(MOD_ID + ".configuration.camera.keep_camera_out_of_head_distance_multiplier")
				.defineInRange("keep_camera_out_of_head_distance_multiplier", 0.75D, 0D, Double.MAX_VALUE);
			
			this.cameraStepSize = builder
				.comment("Size of the camera adjustment per step.")
				.translation(MOD_ID + ".configuration.camera.camera_step_size")
				.defineInRange("camera_step_size", 0.025D, -Double.MAX_VALUE, Double.MAX_VALUE);
			
			this.cameraTransitionSpeedMultiplier = builder
				.comment("The speed multiplier at which the camera transitions between positions.")
				.translation(MOD_ID + ".configuration.camera.camera_transition_speed_multiplier")
				.defineInRange("camera_transition_speed_multiplier", 0.25D, 0.05D, 1.0D);
			
			this.centerCameraWhenLookingDownAngle = builder
				.comment("The angle at which the camera will be centered when looking down. Set to 0 to disable.")
				.translation(MOD_ID + ".configuration.camera.center_camera_when_looking_down_angle")
				.defineInRange("center_camera_when_looking_down_angle", 1D, 0D, 90D);
			
			this.dynamicallyAdjustOffsets = builder
				.comment("Whether or not to dynamically adjust camera offsets depending on space constraints.")
				.translation(MOD_ID + ".configuration.camera.dynamically_adjust_offsets")
				.define("dynamically_adjust_offsets", true);
			
			this.isCameraDecoupled = builder
				.comment("Whether or not to decouple the camera rotation from the player rotation.")
				.translation(MOD_ID + ".configuration.camera.decoupled_camera")
				.define("decoupled_camera", true);
			
			this.orientCameraOnTeleport = builder
				.comment("Whether or not to orient the camera rotation when the player is teleported. This includes passenger (dis-)mounting and traveling through portals.")
				.translation(MOD_ID + ".configuration.camera.orient_camera_on_teleport")
				.define("orient_camera_on_teleport", true);
			
			this.isFovOverrideEnabled = builder
				.comment("Whether or not to apply the FOV override when in shoulder surfing perspective.")
				.translation(MOD_ID + ".configuration.camera.fov_override_enabled")
				.define("fov_override_enabled", false);
			
			this.fovOverride = builder
				.comment("The camera FOV when in shoulder surfing perspective. Depends on config option 'fov_override_enabled'.")
				.translation(MOD_ID + ".configuration.camera.fov_override")
				.defineInRange("fov_override", 70.0D, 30.0D, 110.0D);
			
			this.viewBobbingMode = builder
				.comment("Whether to apply view bobbing in shoulder surfing perspective. Set to INHERIT to inherit vanilla setting.")
				.translation(MOD_ID + ".configuration.camera.view_bobbing_mode")
				.defineEnum("view_bobbing_mode", ViewBobbingMode.INHERIT, ViewBobbingMode.values());
			
			builder.push("camera_drag");
			
			this.cameraDragXMultiplier = builder
				.comment("x-axis multiplier for camera drag.")
				.translation(MOD_ID + ".configuration.camera.camera_drag.multiplier_axis_x")
				.defineInRange("multiplier_axis_x", 0.0D, 0, 5);
			
			this.cameraDragYMultiplier = builder
				.comment("y-axis multiplier for camera drag.")
				.translation(MOD_ID + ".configuration.camera.camera_drag.multiplier_axis_y")
				.defineInRange("multiplier_axis_y", 0.0D, 0, 5);
			
			this.cameraDragZMultiplier = builder
				.comment("z-axis multiplier for camera drag.")
				.translation(MOD_ID + ".configuration.camera.camera_drag.multiplier_axis_z")
				.defineInRange("multiplier_axis_z", 0.0D, 0, 5);
			
			builder.pop();
			builder.push("camera_sway");
			
			this.cameraSwayXMaxAngle = builder
				.comment("The maximum x-axis angle in degrees. Set to 0 to disable.")
				.translation(MOD_ID + ".configuration.camera.camera_sway.max_angle_axis_x")
				.defineInRange("max_angle_axis_x", 0.0D, -30, 30);
			
			this.cameraSwayZMaxAngle = builder
				.comment("The maximum x-axis angle in degrees. Set to 0 to disable.")
				.translation(MOD_ID + ".configuration.camera.camera_sway.max_angle_axis_z")
				.defineInRange("max_angle_axis_z", 0.0D, -30, 30);
			
			this.cameraSwayXMaxVelocity = builder
				.comment("The velocity of the player in blocks per second, where the maximum camera x-axis sway is applied.")
				.translation(MOD_ID + ".configuration.camera.camera_sway.max_velocity_axis_x")
				.defineInRange("max_velocity_axis_x", 5, 0.05, 1000);
			
			this.cameraSwayZMaxVelocity = builder
				.comment("The velocity of the player in blocks per second, where the maximum camera z-axis sway is applied.")
				.translation(MOD_ID + ".configuration.camera.camera_sway.max_velocity_axis_z")
				.defineInRange("max_velocity_axis_z", 5, 0.05, 1000);
			
			builder.pop();
			builder.pop();
			builder.push("perspective");
			
			this.defaultPerspective = builder
				.comment("The default perspective when you load the game.")
				.translation(MOD_ID + ".configuration.perspective.default_perspective")
				.defineEnum("default_perspective", Perspective.SHOULDER_SURFING, Perspective.values());
			
			this.rememberLastPerspective = builder
				.comment("Whether or not to remember the last perspective used.")
				.translation(MOD_ID + ".configuration.perspective.remember_last_perspective")
				.define("remember_last_perspective", true);
			
			this.replaceDefaultPerspective = builder
				.comment("Whether or not to replace the default third person perspective.")
				.translation(MOD_ID + ".configuration.perspective.replace_default_perspective")
				.define("replace_default_perspective", false);
			
			this.isFirstPersonEnabled = builder
				.comment("Whether or not the first person perspective is enabled.")
				.translation(MOD_ID + ".configuration.perspective.first_person_enabled")
				.define("first_person_enabled", true);
			
			this.isThirdPersonFrontEnabled = builder
				.comment("Whether or not the third person front perspective is enabled.")
				.translation(MOD_ID + ".configuration.perspective.third_person_front_enabled")
				.define("third_person_front_enabled", true);
			
			this.isThirdPersonBackEnabled = builder
				.comment("Whether or not the third person back perspective is enabled.")
				.translation(MOD_ID + ".configuration.perspective.third_person_back_enabled")
				.define("third_person_back_enabled", true);
			
			builder.pop();
			builder.push("player");
			
			this.playerTransparency = builder
				.comment("Whether or not to adjust the player model transparency when view is obstructed. Changing this value may require a game restart to take full effect.")
				.translation(MOD_ID + ".configuration.player.adjust_player_transparency")
				.gameRestart()
				.define("adjust_player_transparency", true);
			
			this.turnPlayerTransparentWhenAiming = builder
				.comment("Whether or not to turn the player model transparent when aiming. This config option only applies when adjust player transparency is enabled.")
				.translation(MOD_ID + ".configuration.player.turn_player_transparent_when_aiming")
				.define("turn_player_transparent_when_aiming", false);
			
			this.hidePlayerWhenLookingUpAngle = builder
				.comment("The angle at which the player will no longer be rendered when looking up. Set to 0 to disable.")
				.translation(MOD_ID + ".configuration.player.hide_player_when_looking_up_angle")
				.defineInRange("hide_player_when_looking_up_angle", 0D, 0D, 90D);
			
			this.playerXRotFollowsCamera = builder
				.comment("Whether the x-rot of the player should follow the camera x-rot. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.player_x_rot_follows_camera")
				.define("player_x_rot_follows_camera", false);
			
			this.playerYRotFollowsCamera = builder
				.comment("Whether the y-rot of the player should follow the camera y-rot. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.player_y_rot_follows_camera")
				.define("player_y_rot_follows_camera", false);
			
			this.playerYRotFollowAngleLimit = builder
				.comment("The maximum angle to which the player y-rot follows the camera y-rot. This config option only applies when player y-rot follows camera option is enabled.")
				.translation(MOD_ID + ".configuration.player.player_y_rot_follow_angle_limit")
				.defineInRange("player_y_rot_follow_angle_limit", 90D, 0D, 180D);
			
			builder.push("turning");
			
			this.turningModeWhenUsingItem = builder
				.comment("Whether to turn the player when using an item. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.turning.when_using_item")
				.defineEnum("when_using_item", TurningMode.ALWAYS, TurningMode.values());
			
			this.turningModeWhenAttacking = builder
				.comment("Whether to turn the player when attacking. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.turning.when_attacking")
				.defineEnum("when_attacking", TurningMode.REQUIRES_TARGET, TurningMode.values());
			
			this.turningModeWhenInteraction = builder
				.comment("Whether to turn the player when interacting with blocks. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.turning.when_interacting")
				.defineEnum("when_interacting", TurningMode.ALWAYS, TurningMode.values());
			
			this.turningModeWhenPicking = builder
				.comment("Whether to turn the player when picking blocks or entities. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.turning.when_picking")
				.defineEnum("when_picking", TurningMode.ALWAYS, TurningMode.values());
			
			this.turningLockTime = builder
				.comment("The time in ticks the player will remain turned after the interaction has ended. Set to 0 to disable. This config option only applies when camera is decoupled.")
				.translation(MOD_ID + ".configuration.player.turning.turning_lock_time")
				.defineInRange("turning_lock_time", 4, 0, Integer.MAX_VALUE);
			
			builder.pop();
			builder.pop();
			builder.push("object_picker");
			
			this.customRaytraceDistance = builder
				.comment("The raytrace distance used for the dynamic crosshair.")
				.translation(MOD_ID + ".configuration.object_picker.custom_raytrace_distance")
				.defineInRange("custom_raytrace_distance", 400, 0, Double.MAX_VALUE);
			
			this.useCustomRaytraceDistance = builder
				.comment("Whether or not to use the custom raytrace distance used for the dynamic crosshair.")
				.translation(MOD_ID + ".configuration.object_picker.use_custom_raytrace_distance")
				.define("use_custom_raytrace_distance", true);
			
			builder.push("pick_origin");
			
			this.entityPickOrigin = builder
				.comment("The origin where the entity pick starts when using the static crosshair.")
				.translation(MOD_ID + ".configuration.object_picker.pick_origin.entity_pick_origin")
				.defineEnum("entity_pick_origin", PickOrigin.PLAYER, PickOrigin.values());
			
			this.blockPickOrigin = builder
				.comment("The origin where the block pick starts when using the static crosshair.")
				.translation(MOD_ID + ".configuration.object_picker.pick_origin.block_pick_origin")
