//   ▄████████▄      +        ███ +  ▄█████████ ███     +
//  ███▀    ▀███ +         +  ███    ███▀  +    ███ +       +
//  ███  +   ███ ███   ███ █████████ ███        ███  ███   ███
//  ███     +███ ███   ███    ███    ███▐██████ ███  ███   ███
//  ███ +    ███ ███+  ███   +███    ███     +  ███  ███ + ███
//  ███▄    ▄███ ███▄  ███    ███ +  ███  +     ███  ███▄  ███
//   ▀████████▀ + ▀███████    ███▄   ███▄       ▀████ ▀███████
//       +                  +                +             ███
//  +   ▀████████████████████████████████████████████████████▀
//
// This module manages the heads-up display and augmented reality overlays.

use crate::prelude::*;
use bevy::diagnostic::{DiagnosticsStore, FrameTimeDiagnosticsPlugin};
use bevy::pbr::{NotShadowCaster, NotShadowReceiver};
use bevy::prelude::*;
use bevy::scene::SceneInstance;
use bevy::transform::TransformSystem;
use bevy_xpbd_3d::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::VecDeque;
use std::time::SystemTime;

pub const DASHBOARD_ICON_SIZE: f32 = 64.0;
pub const HUD_REFRESH_TIME: f32 = 0.1;
pub const LOG_MAX_TIME_S: f64 = 30.0;
pub const LOG_MAX_ROWS: usize = 30;
pub const LOG_MAX: usize = LOG_MAX_ROWS;
pub const MAX_CHOICES: usize = 10;
pub const SPEEDOMETER_WIDTH: f32 = 20.0;
pub const SPEEDOMETER_HEIGHT: f32 = 10.0;
pub const AMBIENT_LIGHT: [f32; 4] = [0.0, 20.0, 60.0, 150.0];
//pub const REPLY_NUMBERS: [char; 10] = ['❶', '❷', '❸', '❹', '❺', '❻', '❼', '❽', '❾', '⓿'];
//pub const REPLY_NUMBERS: [char; 10] = ['①', '②', '③', '④', '⑤', '⑥', '⑦', '⑧', '⑨', '⑩'];
pub const REPLY_NUMBERS: [char; 10] = ['➀', '➁', '➂', '➃', '➄', '➅', '➆', '➇', '➈', '➉'];

pub const DASHBOARD_DEF: &[(Dashboard, &str)] = &[
    (Dashboard::Flashlight, "highbeams"),
    (Dashboard::Leak, "leak"),
    (Dashboard::RotationStabiliser, "rotation_stabiliser"),
    (Dashboard::CruiseControl, "cruise_control"),
    (Dashboard::Battery, "battery"),
    (Dashboard::Radioactivity, "radioactivity"),
];

// Player avatars: [(Avatar, model name, scale, in-game name)]
pub const PLAYER_AR_AVATARS: &[(Avatar, &str, f32, &str)] = &[
    (Avatar::None, "", 1.0, "No Avatar"),
    (Avatar::ChefHat, "suit_ar_chefhat", 1.0, "Chef Hat"),
    (Avatar::Hoodie, "suit_ar_hoodie", 1.0, "Hoodie"),
    (Avatar::HoodieUp, "suit_ar_hoodie_up", 1.0, "Hoodie Up"),
    (Avatar::Skirt, "suit_ar_skirt", 1.0, "Skirt"),
    (
        Avatar::SkirtTartan,
        "suit_ar_skirt_tartan",
        1.0,
        "Tartan Skirt",
    ),
    (Avatar::Dress, "suit_ar_dress", 1.0, "Dress"),
    (Avatar::Nekomimi, "suit_ar_nekomimi", 1.0, "Cat Ears"),
    (Avatar::Wings, "suit_ar_wings", 1.0, "Wings"),
    (Avatar::Armor, "suit_ar_armor", 1.0, "Armor"),
    (Avatar::Asteroid, "metis", 1.3, "Asteroid"),
];

pub const POINTERS: &[(Pointer, Option<&str>, &str)] = &[
    (Pointer::None, None, "Off"),
    (Pointer::Tri, Some("sprites/pointer_tri.png"), "Default"),
    (Pointer::Dot, Some("sprites/pointer_dot.png"), "Dot"),
];

pub struct HudPlugin;
impl Plugin for HudPlugin {
    fn build(&self, app: &mut App) {
        app.add_systems(Startup, setup);
        app.add_systems(
            Update,
            (
                update_hud.run_if(game_running),
                update_dashboard.run_if(game_running),
                update_speedometer.run_if(game_running),
                update_gauges.run_if(game_running),
                handle_input.run_if(game_running).run_if(in_control),
                handle_target_event.run_if(game_running),
            ),
        );
        app.add_systems(
            PostUpdate,
            (
                update_overlay_visibility,
                update_avatar.run_if(on_event::<UpdateAvatarEvent>()),
                update_pointer.run_if(on_event::<UpdatePointerEvent>()),
                update_ar_overlays
                    .run_if(game_running)
                    .after(camera::position_to_transform)
                    .in_set(sync::SyncSet::PositionToTransform),
                update_poi_overlays
                    .run_if(game_running)
                    .after(camera::position_to_transform)
                    .in_set(sync::SyncSet::PositionToTransform),
                update_target_selectagon
                    .run_if(game_running)
                    .after(PhysicsSet::Sync)
                    .after(camera::apply_input_to_player)
                    .before(TransformSystem::TransformPropagate),
            ),
        );
        app.insert_resource(AugmentedRealityState {
            overlays_visible: false,
        });
        app.insert_resource(Log {
            logs: VecDeque::with_capacity(LOG_MAX),
            needs_rerendering: true,
        });
        app.insert_resource(FPSUpdateTimer(Timer::from_seconds(
            HUD_REFRESH_TIME,
            TimerMode::Repeating,
        )));
        app.add_event::<TargetEvent>();
        app.add_event::<UpdateAvatarEvent>();
        app.add_event::<UpdatePointerEvent>();
        app.add_event::<UpdateOverlayVisibility>();
    }
}

#[derive(Event)]
pub struct TargetEvent(pub Option<Entity>);
#[derive(Event)]
pub struct UpdateOverlayVisibility;
#[derive(Event)]
pub struct UpdateAvatarEvent;
#[derive(Event)]
pub struct UpdatePointerEvent;
#[derive(Component)]
struct NodeHud;
#[derive(Component)]
struct NodeConsole;
#[derive(Component)]
struct NodeChoiceText;
#[derive(Component)]
struct NodeSpeedometerText;
#[derive(Component)]
struct NodeCurrentChatLine;
#[derive(Component)]
struct PointerComponent(pub Pointer);
#[derive(Component)]
struct Speedometer;
#[derive(Component)]
struct Speedometer2;
#[derive(Component)]
struct GaugeLength(f32);
#[derive(Component)]
pub struct ToggleableHudElement;
#[derive(Component)]
pub struct ToggleableHudMapElement;
#[derive(Component)]
struct Selectagon;
#[derive(Component)]
struct PlayerAvatar;
#[derive(Component)]
pub struct IsTargeted;
#[derive(Component)]
pub struct PointOfInterestMarker(pub Entity);

#[derive(Component, Debug, Copy, Clone)]
pub enum Dashboard {
    Leak,
    Flashlight,
    RotationStabiliser,
    CruiseControl,
    Radioactivity,
    Battery,
}

#[derive(Component, Debug, Copy, Clone)]
enum Gauge {
    Health,
    Power,
    Oxygen,
    //Integrity,
}

#[derive(Resource)]
pub struct AugmentedRealityState {
    pub overlays_visible: bool,
}

#[derive(Component)]
pub struct AugmentedRealityOverlayBroadcaster;
#[derive(Component)]
pub struct AugmentedRealityOverlay {
    pub owner: Entity,
    pub scale: f32,
}

#[derive(Resource)]
struct FPSUpdateTimer(Timer);

#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
pub enum Pointer {
    None,
    #[default]
    Tri,
    Dot,
}

pub enum Avatar {
    None,
    ChefHat,
    Nekomimi,
    Hoodie,
    HoodieUp,
    Skirt,
    SkirtTartan,
    Dress,
    Wings,
    Asteroid,
    Bra,
    Armor,
}

#[derive(Clone)]
pub enum LogLevel {
    Achievement,
    Always,
    Warning,
    //Error,
    Info,
    //Debug,
    Chat,
    Send,
    //Ping,
}

impl LogLevel {
    pub fn is_subtitle(&self) -> bool {
        match self {
            LogLevel::Chat => true,
            LogLevel::Info => true,
            _ => false,
        }
    }
}

#[derive(Clone)]
pub struct Message {
    text: String,
    sender: String,
    level: LogLevel,
    time: f64,
}
impl Message {
    pub fn get_freshness(&self) -> f64 {
        if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
            return (1.0 - (epoch.as_secs_f64() - self.time) / LOG_MAX_TIME_S).clamp(0.0, 1.0);
        }
        return 1.0;
    }
    pub fn format(&self) -> String {
        if self.sender.is_empty() {
            return self.text.clone() + "\n";
        } else {
            return format!("{}: {}\n", self.sender, self.text);
        }
    }
}

#[derive(Component)]
pub struct IsClickable {
    pub name: Option<String>,
    pub pronoun: Option<String>,
    pub distance: Option<f64>,
}
impl Default for IsClickable {
    fn default() -> Self {
        Self {
            name: None,
            pronoun: None,
            distance: None,
        }
    }
}

#[derive(Resource)]
pub struct Log {
    logs: VecDeque<Message>,
    needs_rerendering: bool,
}

impl Log {
    pub fn info(&mut self, message: String) {
        self.add(message, "System".to_string(), LogLevel::Info);
    }

    pub fn chat(&mut self, message: String, sender: String) {
        self.add(message, sender, LogLevel::Chat);
    }

    pub fn send(&mut self, message: String) {
        self.add(message, "Me".to_string(), LogLevel::Send);
    }

    pub fn warning(&mut self, message: String) {
        self.add(message, "WARNING".to_string(), LogLevel::Warning);
    }

    pub fn add(&mut self, text: String, sender: String, level: LogLevel) {
        if self.logs.len() == LOG_MAX {
            self.logs.pop_front();
        }
        if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
            self.logs.push_back(Message {
                text,
                sender,
                level,
                time: epoch.as_secs_f64(),
            });
            self.needs_rerendering = true;
        }
    }

    #[allow(dead_code)]
    pub fn remove_old(&mut self) {
        if let Some(message) = self.logs.front() {
            if let Ok(epoch) = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH) {
                if epoch.as_secs_f64() - message.time > LOG_MAX_TIME_S {
                    self.logs.pop_front();
                }
            }
        }
    }

    pub fn clear(&mut self) {
        self.logs.clear();
    }

    pub fn get_subtitle_line(&self) -> Option<Message> {
        let messages: Vec<&Message> = self
            .logs
            .iter()
            .filter(|msg: &&Message| msg.level.is_subtitle())
            .rev()
            .take(1)
            .collect();
        if messages.len() > 0 {
            return Some(messages[0].clone());
        }
        return None;
    }
}

pub fn setup(
    mut commands: Commands,
    settings: Res<Settings>,
    prefs: Res<Preferences>,
    asset_server: Res<AssetServer>,
    mut ew_updateoverlays: EventWriter<UpdateOverlayVisibility>,
) {
    let visibility = if settings.hud_active {
        Visibility::Inherited
    } else {
        Visibility::Hidden
    };
    let font_handle = asset_server.load(FONT);
    let style_conversations = TextStyle {
        font: font_handle.clone(),
        font_size: settings.font_size_conversations,
        color: settings.hud_color_subtitles,
        ..default()
    };
    let style_fps = TextStyle {
        font: font_handle.clone(),
        font_size: settings.font_size_fps,
        color: settings.hud_color_fps,
        ..default()
    };
    let style_console = TextStyle {
        font: font_handle.clone(),
        font_size: settings.font_size_console,
        color: settings.hud_color_console,
        ..default()
    };
    let style_choices = TextStyle {
        font: font_handle.clone(),
        font_size: settings.font_size_choices,
        color: settings.hud_color_choices,
        ..default()
    };
    let style_speedometer = TextStyle {
        font: font_handle.clone(),
        font_size: settings.font_size_speedometer,
        color: settings.hud_color_speedometer,
        ..default()
    };
    let style = TextStyle {
        font: font_handle,
        font_size: settings.font_size_hud,
        color: settings.hud_color,
        ..default()
    };

    // Add Statistics HUD
    let mut bundle_fps = TextBundle::from_sections([
        TextSection::new("", style),     // Target
        TextSection::new("", style_fps), // Frames per second
    ])
    .with_style(Style {
        position_type: PositionType::Absolute,
        top: Val::VMin(2.0),
        left: Val::VMin(3.0),
        ..default()
    })
    .with_text_justify(JustifyText::Left);
    bundle_fps.visibility = visibility;
    commands.spawn((NodeHud, ToggleableHudElement, bundle_fps));

    // Add Console
    // This one is intentionally NOT a ToggleableHudElement. Instead, console entries
    // are filtered based on whether the hud is active or not.  LogLevel::Always is
    // even shown when hud is inactive.
    let bundle_chatbox = TextBundle::from_sections(
        (0..LOG_MAX_ROWS).map(|_| TextSection::new("", style_console.clone())),
    )
    .with_style(Style {
        position_type: PositionType::Absolute,
        top: Val::VMin(0.0),
        right: Val::VMin(0.0),
        ..default()
    })
    .with_text_justify(JustifyText::Right);
    commands
        .spawn((NodeBundle {
            style: Style {
                width: Val::Percent(50.0),
                align_items: AlignItems::Start,
                position_type: PositionType::Absolute,
                top: Val::VMin(2.0),
                right: Val::VMin(3.0),
                ..default()
            },
            ..default()
        },))
        .with_children(|parent| {
            parent.spawn((bundle_chatbox, NodeConsole));
        });

    // Add Pointer
    for (pointer_enum, sprite, _) in POINTERS {
        if sprite.is_none() {
            continue;
        }
        let sprite = sprite.unwrap();
        let pointer_handle: Handle<Image> = asset_server.load(sprite.to_string());
        commands
            .spawn((
                NodeBundle {
                    style: style_centered(),
                    visibility,
                    ..default()
                },
                ToggleableHudElement,
            ))
            .with_children(|builder| {
                builder.spawn((
                    ImageBundle {
                        image: UiImage::new(pointer_handle),
                        style: Style {
                            width: Val::VMin(5.0),
                            height: Val::VMin(5.0),
                            ..Default::default()
                        },
                        visibility: bool2vis(prefs.pointer == *pointer_enum),
                        ..Default::default()
                    },
                    PointerComponent(pointer_enum.clone()),
                ));
            });
    }

    // HP/O2/Suit Integrity/Power Gauges
    let gauges_handle: Handle<Image> = asset_server.load("sprites/gauge_horizontal.png");
    let gauges = [
        ("sprites/gauge_heart.png", Gauge::Health),
        ("sprites/gauge_battery.png", Gauge::Power),
        ("sprites/gauge_o2.png", Gauge::Oxygen),
        //("sprites/gauge_suit.png", Gauge::Integrity),
    ];
    let icon_size = 24.0;
    let gauge_bar_padding_left = 4.0;
    for (i, (sprite, gauge)) in gauges.iter().enumerate() {
        let bar_length = if i == 0 { 32.0 * 8.0 } else { 32.0 * 5.0 };

        // The bar with variable width
        commands
            .spawn((
                NodeBundle {
                    style: Style {
                        width: Val::Percent(30.0),
                        height: Val::Percent(100.0),
                        bottom: Val::Px(20.0 + 24.0 * i as f32),
                        left: Val::VMin(2.0),
                        align_items: AlignItems::End,
                        overflow: Overflow::clip(),
                        ..default()
                    },
                    visibility,
                    ..default()
                },
                ToggleableHudElement,
            ))
            .with_children(|builder| {
                builder.spawn((
                    NodeBundle {
                        style: Style {
                            width: Val::Px(118.0),
                            height: Val::Px(10.0),
                            bottom: Val::Px(8.0),
                            left: Val::Px(gauge_bar_padding_left + icon_size),
                            ..Default::default()
                        },
                        visibility,
                        background_color: settings.hud_color.into(),
                        ..Default::default()
                    },
                    gauge.clone(),
                    GaugeLength(bar_length),
                    ToggleableHudElement,
                ));
            });

        // The decorator sprites surrounding the bar
        commands
            .spawn((
                NodeBundle {
                    style: Style {
                        width: Val::Percent(30.0),
                        height: Val::Percent(100.0),
                        bottom: Val::Px(20.0 + 24.0 * i as f32),
                        left: Val::VMin(2.0),
                        align_items: AlignItems::End,
                        overflow: Overflow::clip(),
                        ..default()
                    },
                    visibility,
                    ..default()
                },
                ToggleableHudElement,
            ))
            .with_children(|builder| {
                // The gauge symbol
                builder.spawn((
                    ImageBundle {
                        image: UiImage::new(asset_server.load(sprite.to_string())),
                        style: Style {
                            width: Val::Px(icon_size),
                            height: Val::Px(icon_size),
                            ..Default::default()
                        },
                        visibility,
                        ..Default::default()
                    },
                    ToggleableHudElement,
                ));
                // The gauge bar border
                builder.spawn((
                    ImageBundle {
                        image: UiImage::new(gauges_handle.clone()),
                        style: Style {
                            width: Val::Px(bar_length),
                            height: Val::Px(10.0),
                            bottom: Val::Px(8.0),
                            left: Val::Px(gauge_bar_padding_left),
                            ..Default::default()
                        },
                        visibility,
                        ..Default::default()
                    },
                    ToggleableHudElement,
                ));
            });
    }

    // Car-Dashboard-Style icons
    let style_dashboard = Style {
        width: Val::Px(DASHBOARD_ICON_SIZE),
        height: Val::Px(DASHBOARD_ICON_SIZE),
        ..Default::default()
    };
    commands
        .spawn((
            NodeBundle {
                style: Style {
                    width: Val::Percent(30.0),
                    height: Val::Percent(100.0),
                    bottom: Val::Px(40.0 + icon_size * gauges.len() as f32),
                    left: Val::VMin(4.0),
                    align_items: AlignItems::End,
                    overflow: Overflow::clip(),
                    ..default()
                },
                visibility,
                ..default()
            },
            ToggleableHudElement,
        ))
        .with_children(|builder| {
            for (component, filename) in DASHBOARD_DEF {
                builder.spawn((
                    *component,
                    ImageBundle {
                        image: UiImage::new(
                            asset_server.load(format!("sprites/dashboard_{}.png", filename)),
                        ),
                        style: style_dashboard.clone(),
                        visibility: Visibility::Hidden,
                        ..Default::default()
                    },
                ));
            }
        });

    // Add Speedometer
    let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer.png");
    commands
        .spawn((
            NodeBundle {
                style: Style {
                    width: Val::VMin(0.0),
                    height: Val::Percent(100.0),
                    left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
                    align_items: AlignItems::End,
                    overflow: Overflow::clip(),
                    ..default()
                },
                visibility,
                ..default()
            },
            Speedometer,
            ToggleableHudElement,
        ))
        .with_children(|builder| {
            builder.spawn((ImageBundle {
                image: UiImage::new(speedometer_handle),
                style: Style {
                    width: Val::Vw(SPEEDOMETER_WIDTH),
                    height: Val::VMin(SPEEDOMETER_HEIGHT),
                    ..Default::default()
                },
                ..Default::default()
            },));
        });
    let speedometer_handle: Handle<Image> = asset_server.load("sprites/speedometer_white.png");
    commands
        .spawn((
            NodeBundle {
                style: Style {
                    width: Val::VMin(0.0),
                    height: Val::Percent(100.0),
                    left: Val::Vw(100.0 - SPEEDOMETER_WIDTH),
                    align_items: AlignItems::End,
                    overflow: Overflow::clip(),
                    ..default()
                },
                visibility,
                ..default()
            },
            Speedometer2,
            ToggleableHudElement,
        ))
        .with_children(|builder| {
            builder.spawn((ImageBundle {
                image: UiImage::new(speedometer_handle),
                style: Style {
                    width: Val::Vw(SPEEDOMETER_WIDTH),
                    height: Val::VMin(SPEEDOMETER_HEIGHT),
                    ..Default::default()
                },
                ..Default::default()
            },));
        });
    let mut bundle_speedometer_text = TextBundle::from_sections([
        TextSection::new("", style_speedometer.clone()), // speed relative to target
        TextSection::new("", style_speedometer.clone()), // speed relative to target
        TextSection::new("", style_speedometer.clone()), // speed relative to orbit
    ])
    .with_style(Style {
        position_type: PositionType::Absolute,
        left: Val::Vw(100.0 - SPEEDOMETER_WIDTH + 2.0),
        bottom: Val::VMin(4.0),
        ..default()
    })
    .with_text_justify(JustifyText::Left);
    bundle_speedometer_text.visibility = visibility;
    commands.spawn((
        NodeSpeedometerText,
        ToggleableHudElement,
        bundle_speedometer_text,
    ));

    // Chat "subtitles" and choices
    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Vw(100.0),
                align_items: AlignItems::Center,
                flex_direction: FlexDirection::Column,
                position_type: PositionType::Absolute,
                bottom: Val::Vh(2.0),
                left: Val::Px(0.0),
                ..default()
            },
            ..default()
        })
        .with_children(|builder| {
            builder.spawn((
                TextBundle {
                    text: Text {
                        sections: vec![TextSection::new("", style_conversations)],
                        justify: JustifyText::Center,
                        ..default()
                    },
                    style: Style {
                        max_width: Val::Percent(50.0),
                        margin: UiRect {
                            bottom: Val::Vh(1.0),
                            ..default()
                        },
                        ..default()
                    },
                    ..default()
                },
                NodeCurrentChatLine,
            ));
            let choice_sections =
                (0..MAX_CHOICES).map(|_| TextSection::new("", style_choices.clone()));
            builder.spawn((
                TextBundle {
                    text: Text {
                        sections: choice_sections.collect(),
                        ..default()
                    },
                    ..default()
                },
                NodeChoiceText,
            ));
        });

    // Selectagon
    let mut entitycmd = commands.spawn((
        Selectagon,
        NotShadowCaster,
        NotShadowReceiver,
        SpatialBundle {
            visibility: Visibility::Hidden,
            ..default()
        },
    ));
    load_asset("selectagon", &mut entitycmd, &*asset_server);

    ew_updateoverlays.send(UpdateOverlayVisibility);
}

fn update_dashboard(
    timer: ResMut<FPSUpdateTimer>,
    mut q_dashboard: Query<(&mut Visibility, &Dashboard)>,
    q_player: Query<(&actor::Suit, &actor::Battery, &actor::LifeForm), With<actor::Player>>,
    settings: Res<Settings>,
) {
    if !settings.hud_active || !timer.0.just_finished() {
        return;
    }
    let player = q_player.get_single();
    if player.is_err() {
        return;
    }
    let (suit, battery, lifeform) = player.unwrap();

    for (mut vis, icon) in &mut q_dashboard {
        *vis = bool2vis(match icon {
            Dashboard::Flashlight => settings.flashlight_active,
            Dashboard::Leak => suit.integrity < 0.5,
            Dashboard::Battery => battery.overloaded_recovering,
            Dashboard::RotationStabiliser => !settings.rotation_stabilizer_active,
            Dashboard::CruiseControl => settings.cruise_control_active,
            Dashboard::Radioactivity => lifeform.is_radioactively_damaged,
        });
    }
}

fn update_speedometer(
    timer: ResMut<FPSUpdateTimer>,
    settings: Res<Settings>,
    jupiter_pos: Res<game::JupiterPos>,
    q_camera: Query<(&LinearVelocity, &Position), With<actor::PlayerCamera>>,
    q_player: Query<&actor::ExperiencesGForce, With<actor::Player>>,
    q_target: Query<&LinearVelocity, With<IsTargeted>>,
    mut q_speedometer: Query<&mut Style, (With<Speedometer>, Without<Speedometer2>)>,
    mut q_speedometer2: Query<&mut Style, (With<Speedometer2>, Without<Speedometer>)>,
    mut q_node_speed: Query<&mut Text, With<NodeSpeedometerText>>,
) {
    if !settings.hud_active || !timer.0.just_finished() {
        return;
    }
    if let Ok((cam_v, pos)) = q_camera.get_single() {
        let orbital_v = nature::orbital_velocity(pos.0 - jupiter_pos.0, nature::JUPITER_MASS);
        let speed = (cam_v.0 - orbital_v).length();

        let speedometer_split = 5_000.0;
        if let Ok(mut speedometer) = q_speedometer.get_single_mut() {
            let custom_c = speedometer_split;
            let fraction = nature::inverse_lorentz_factor_custom_c(
                (custom_c - speed).clamp(0.0, custom_c),
                custom_c,
            )
            .clamp(0.0, 1.0) as f32;
            let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
            speedometer.width = Val::Vw(wid);
        }
        if let Ok(mut speedometer2) = q_speedometer2.get_single_mut() {
            let custom_c = nature::C - speedometer_split;
            let fraction = nature::inverse_lorentz_factor_custom_c(
                (custom_c - speed + speedometer_split).clamp(0.0, custom_c),
                custom_c,
            )
            .clamp(0.0, 1.0) as f32;
            let wid = (fraction * SPEEDOMETER_WIDTH).clamp(0.0, 100.0);
            speedometer2.width = Val::Vw(wid);
        }
        if let Ok(mut speed_text) = q_node_speed.get_single_mut() {
            // G forces
            speed_text.sections[0].value = if let Ok(gforce) = q_player.get_single() {
                if gforce.gforce > 0.0001 {
                    format!("{:.1}g\n", gforce.gforce)
                } else {
                    "".to_string()
                }
            } else {
                "".to_string()
            };

            // Velocity relative to target
            speed_text.sections[1].value = if let Ok(target_v) = q_target.get_single() {
                let delta_v = (target_v.0 - cam_v.0).length();
                if delta_v > 0.0001 {
                    format!("Δv {}\n", nature::readable_speed(delta_v))
                } else {
                    "".to_string()
                }
            } else {
                "".to_string()
            };

            // "Absolute velocity", or velocity relative to orbit
            speed_text.sections[2].value = if speed > 0.0001 {
                nature::readable_speed(speed)
            } else {
                "".to_string()
            };
        }
    }
}

fn update_gauges(
    timer: ResMut<FPSUpdateTimer>,
    q_player: Query<(&actor::HitPoints, &actor::Suit, &actor::Battery), With<actor::Player>>,
    mut q_gauges: Query<(&mut Style, &mut BackgroundColor, &Gauge, &GaugeLength)>,
    settings: Res<Settings>,
) {
    if !settings.hud_active || !timer.0.just_finished() {
        return;
    }
    let player = q_player.get_single();
    if player.is_err() {
        return;
    }
    let (hp, suit, battery) = player.unwrap();

    for (mut style, mut bg, gauge, len) in &mut q_gauges {
        let value: f32 = match gauge {
            Gauge::Health => hp.current / hp.max,
            Gauge::Oxygen => (suit.oxygen / suit.oxygen_max).powf(0.5),
            //Gauge::Integrity => suit.integrity,
            Gauge::Power => battery.power / battery.capacity,
        };
        if value < 0.5 {
            *bg = settings.hud_color_alert.into();
        } else {
            *bg = settings.hud_color.into();
        }
        style.width = Val::Px(len.0 * value);
    }
}

fn update_hud(
    diagnostics: Res<DiagnosticsStore>,
    time: Res<Time>,
    mut log: ResMut<Log>,
    q_camera: Query<(&Position, &LinearVelocity), With<actor::PlayerCamera>>,
    mut timer: ResMut<FPSUpdateTimer>,
    q_choices: Query<&chat::Choice>,
    q_chat: Query<&chat::Chat>,
    mut q_node_hud: Query<&mut Text, With<NodeHud>>,
    mut q_node_console: Query<
        &mut Text,
        (With<NodeConsole>, Without<NodeHud>, Without<NodeChoiceText>),
    >,
    mut q_node_choice: Query<
        &mut Text,
        (With<NodeChoiceText>, Without<NodeHud>, Without<NodeConsole>),
    >,
    mut q_node_currentline: Query<
        &mut Text,
        (
            With<NodeCurrentChatLine>,
            Without<NodeHud>,
            Without<NodeConsole>,
            Without<NodeChoiceText>,
        ),
    >,
    settings: Res<Settings>,
    q_target: Query<(&IsClickable, Option<&Position>, Option<&LinearVelocity>), With<IsTargeted>>,
) {
    if timer.0.tick(time.delta()).just_finished() || log.needs_rerendering {
        let q_camera_result = q_camera.get_single();
        let mut freshest_line: f64 = 0.0;
        if settings.hud_active && q_camera_result.is_ok() {
            let (pos, _) = q_camera_result.unwrap();
            for mut text in &mut q_node_hud {
                if let Some(fps) = diagnostics.get(&FrameTimeDiagnosticsPlugin::FPS) {
                    if let Some(value) = fps.smoothed() {
                        // Update the value of the second section
                        text.sections[1].value = format!("{value:.0}");
                    }
                }

                // Target display
                let dist_scalar: f64;
                let mut target_multiple = false;
                let mut target_error = false;
                if let Ok((
                    IsClickable {
                        distance: Some(dist),
                        ..
                    },
                    _,
                    _,
                )) = q_target.get_single()
                {
                    dist_scalar = *dist;
                } else {
                    let target: Option<DVec3>;
                    if let Ok((_, Some(targetpos), _)) = q_target.get_single() {
                        target = Some(targetpos.0);
                    } else if q_target.is_empty() {
                        target = Some(DVec3::new(0.0, 0.0, 0.0));
                    } else if q_target.iter().len() > 1 {
                        target_multiple = true;
                        target = None;
                    } else {
                        target_error = true;
                        target = None;
                    }
                    if let Some(target_pos) = target {
                        let dist = pos.0 - target_pos;
                        dist_scalar = dist.length();
                    } else {
                        dist_scalar = 0.0;
                    }
                }

                if target_multiple {
                    text.sections[0].value = "ERROR: MULTIPLE TARGETS\n\n".to_string();
                } else if target_error {
                    text.sections[0].value = "ERROR: FAILED TO AQUIRE TARGET\n\n".to_string();
                } else if let Ok((clickable, _, _)) = q_target.get_single() {
                    let distance = if dist_scalar.is_nan() {
                        "UNKNOWN".to_string()
                    } else if dist_scalar != 0.0 {
                        nature::readable_distance(dist_scalar)
                    } else {
                        "ERROR".to_string()
                    };
                    let target_name = clickable.name.clone().unwrap_or("Unnamed".to_string());
                    let pronoun = if let Some(pronoun) = &clickable.pronoun {
                        format!("Pronoun: {pronoun}\n")
                    } else {
                        "".to_string()
                    };
                    text.sections[0].value =
                        format!("Target: {target_name}\n{pronoun}Distance: {distance}\n\n");
                } else {
                    text.sections[0].value = "".to_string();
                }
            }
        }

        let chat = q_node_console.get_single_mut();
        if chat.is_err() {
            error!("Couldn't find HUD UI text section");
            return;
        }
        let mut chat = chat.unwrap();

        let choicebox = q_node_choice.get_single_mut();
        if choicebox.is_err() {
            error!("Couldn't find HUD UI text section");
            return;
        }
        let mut choicebox = choicebox.unwrap();

        let node_currentline = q_node_currentline.get_single_mut();
        if node_currentline.is_err() {
            error!("Couldn't find HUD UI text section");
            return;
        }
        let mut node_currentline = node_currentline.unwrap();

        let mut row = 0;

        // Chat Log and System Log
        let logfilter = if settings.hud_active {
            |_msg: &&Message| true
        } else {
            |msg: &&Message| match msg.level {
                LogLevel::Always => true,
                LogLevel::Warning => true,
                LogLevel::Achievement => true,
                _ => false,
            }
        };
        let messages: Vec<&Message> = log
            .logs
            .iter()
            .filter(logfilter)
            .rev()
            .take(LOG_MAX_ROWS)
            .collect();
        //messages.reverse();
        for msg in &messages {
            chat.sections[row].value = msg.format();
            let freshness = msg.get_freshness();
            let opacity: f32 = (freshness.powf(1.5) as f32).clamp(0.0, 1.0);
            freshest_line = freshest_line.max(freshness);
            chat.sections[row].style.color = match msg.level {
                LogLevel::Achievement => settings.hud_color_console_achievement,
                LogLevel::Warning => settings.hud_color_console_warn,
                LogLevel::Info => settings.hud_color_console_system,
                LogLevel::Send => settings.hud_color_console_send,
                _ => settings.hud_color_console,
            };
            chat.sections[row].style.color.set_alpha(opacity);
            row += 1;
        }

        // Display the last chat line as "subtitles"
        if !q_chat.is_empty() {
            if let Some(message) = log.get_subtitle_line() {
                node_currentline.sections[0].value = message.format();
            } else {
                node_currentline.sections[0].value = "".to_string();
            }
        } else {
            node_currentline.sections[0].value = "".to_string();
        }

        // Blank the remaining rows
        while row < LOG_MAX_ROWS {
            chat.sections[row].value = "".to_string();
            row += 1;
        }

        // Choices
        row = 0;
        let mut choices: Vec<String> = Vec::new();
        let mut count = 0;
        for choice in &q_choices {
            if count > 9 {
                break;
            }
            let press_this = REPLY_NUMBERS[choice.key];
            let reply = &choice.text;
            //let recipient = &choice.recipient;
            // TODO: indicate recipients if there's more than one
            choices.push(format!("{press_this}  {reply}"));
            count += 1;
        }
        for choice in choices {
            if row >= MAX_CHOICES {
                break;
            }
            choicebox.sections[row].value = choice + "\n";
            row += 1;
        }

        while row < MAX_CHOICES {
            choicebox.sections[row].value = if row < 4 {
                " \n".to_string()
            } else {
                "".to_string()
            };
            row += 1;
        }

        log.needs_rerendering = false;

        //if q_choices.is_empty() && freshest_line < 0.2 {
        log.remove_old();
        //}
    }
}

fn handle_input(
    keyboard_input: Res<ButtonInput<KeyCode>>,
    mouse_input: Res<ButtonInput<MouseButton>>,
    settings: Res<Settings>,
    mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
    mut ew_target: EventWriter<TargetEvent>,
    mut ew_game: EventWriter<GameEvent>,
    q_objects: Query<
        (Entity, &Transform),
        (
            With<IsClickable>,
            Without<IsTargeted>,
            Without<actor::PlayerDrivesThis>,
            Without<actor::Player>,
        ),
    >,
    q_camera: Query<&Transform, With<Camera>>,
) {
    if keyboard_input.just_pressed(settings.key_togglehud) {
        ew_game.send(GameEvent::SetAR(Turn::Toggle));
        ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Switch));
    }
    if settings.hud_active && mouse_input.just_pressed(settings.key_selectobject) {
        if let Ok(camtrans) = q_camera.get_single() {
            let objects: Vec<(Entity, &Transform)> = q_objects.iter().collect();
            if let (Some(new_target), _dist) =
                camera::find_closest_target::<Entity>(objects, camtrans)
            {
                ew_target.send(TargetEvent(Some(new_target)));
            } else {
                ew_target.send(TargetEvent(None));
            }
        }
    }
}

fn handle_target_event(
    mut commands: Commands,
    mut er_target: EventReader<TargetEvent>,
    mut ew_sfx: EventWriter<audio::PlaySfxEvent>,
    mut ew_achievement: EventWriter<game::AchievementEvent>,
    q_target: Query<Entity, With<IsTargeted>>,
    q_ids: Query<&actor::Identifier>,
) {
    let mut play_sfx = false;

    for TargetEvent(target) in er_target.read() {
        for old_target in &q_target {
            commands.entity(old_target).remove::<IsTargeted>();
            play_sfx = true;
        }
        if let Some(entity) = target {
            // TODO: This can panic if the entity despawns in the meantime
            commands.entity(*entity).insert(IsTargeted);
            play_sfx = true;

            if let Ok(id) = q_ids.get(*entity) {
                if id.0 == cmd::ID_EARTH {
                    ew_achievement.send(game::AchievementEvent::FindEarth);
                }
            }
        }
        if play_sfx {
            ew_sfx.send(audio::PlaySfxEvent(audio::Sfx::Click3));
        }
        break; // Only accept a single event per frame
    }
}

fn update_target_selectagon(
    settings: Res<Settings>,
    mut q_selectagon: Query<
        (&mut Transform, &mut Visibility),
        (With<Selectagon>, Without<IsTargeted>, Without<Camera>),
    >,
    q_target: Query<&Transform, (With<IsTargeted>, Without<Camera>, Without<Selectagon>)>,
    q_camera: Query<&Transform, (With<Camera>, Without<IsTargeted>, Without<Selectagon>)>,
) {
    if !settings.hud_active || q_camera.is_empty() {
        return;
    }
    let camera_trans = q_camera.get_single().unwrap();

    if let Ok((mut selectagon_trans, mut selectagon_vis)) = q_selectagon.get_single_mut() {
        if let Ok(target_trans) = q_target.get_single() {
            match *selectagon_vis {
                Visibility::Hidden => {
                    *selectagon_vis = Visibility::Visible;
                }
                _ => {}
            }
            selectagon_trans.translation = target_trans.translation;
            selectagon_trans.scale = target_trans.scale;
            selectagon_trans.look_at(camera_trans.translation, camera_trans.up());

            // Enlarge Selectagon to a minimum angular diameter
            let (angular_diameter, _, _) =
                camera::calc_angular_diameter(&selectagon_trans, camera_trans);
            let min_angular_diameter = 2.0f32.to_radians();
            if angular_diameter < min_angular_diameter {
                selectagon_trans.scale *= min_angular_diameter / angular_diameter;
            }
        } else {
            match *selectagon_vis {
                Visibility::Hidden => {}
                _ => {
                    *selectagon_vis = Visibility::Hidden;
                }
            }
        }
    }
}

fn update_ar_overlays(
    q_owners: Query<
        (Entity, &Transform, &Visibility),
        (
            With<AugmentedRealityOverlayBroadcaster>,
            Without<AugmentedRealityOverlay>,
        ),
    >,
    mut q_overlays: Query<(
        &mut Transform,
        &mut Visibility,
        &mut AugmentedRealityOverlay,
    )>,
    settings: ResMut<Settings>,
    mut state: ResMut<AugmentedRealityState>,
) {
    let (need_activate, need_clean, need_update);
    if settings.hud_active {
        need_activate = !state.overlays_visible;
        need_clean = false;
    } else {
        need_activate = false;
        need_clean = state.overlays_visible;
    }
    need_update = settings.hud_active;
    state.overlays_visible = settings.hud_active;

    if need_update || need_clean || need_activate {
        for (mut trans, mut vis, ar) in &mut q_overlays {
            for (owner_id, owner_trans, owner_vis) in &q_owners {
                if owner_id == ar.owner {
                    *trans = *owner_trans;
                    if ar.scale != 1.0 {
                        trans.scale *= ar.scale;
                    }
                    if need_clean {
                        *vis = Visibility::Hidden;
                    } else {
                        *vis = *owner_vis;
                    }
                    break;
                }
            }
        }
    }
}

fn update_poi_overlays(
    mut q_marker: Query<(&mut Transform, &PointOfInterestMarker)>,
    q_parent: Query<&Transform, Without<PointOfInterestMarker>>,
    q_camera: Query<&Transform, (With<Camera>, Without<PointOfInterestMarker>)>,
    settings: ResMut<Settings>,
) {
    if !settings.hud_active || !settings.map_active || q_camera.is_empty() {
        return;
    }
    let camera_trans = q_camera.get_single().unwrap();
    for (mut trans, marker) in &mut q_marker {
        if let Ok(parent_trans) = q_parent.get(marker.0) {
            // Enlarge POI marker to a minimum angular diameter
            trans.translation = parent_trans.translation;
            trans.scale = Vec3::splat(1.0);
            let (angular_diameter, _, _) = camera::calc_angular_diameter(&trans, camera_trans);
            let min_angular_diameter = 3.0f32.to_radians();
            if angular_diameter < min_angular_diameter {
                trans.scale *= min_angular_diameter / angular_diameter;
            }
            trans.look_at(camera_trans.translation, camera_trans.up());
        }
    }
}

fn update_overlay_visibility(
    mut q_marker: Query<&mut Visibility, With<PointOfInterestMarker>>,
    mut q_hudelement: Query<
        &mut Visibility,
        (With<ToggleableHudElement>, Without<PointOfInterestMarker>),
    >,
    mut q_selectagon: Query<
        &mut Visibility,
        (
            With<Selectagon>,
            Without<ToggleableHudElement>,
            Without<PointOfInterestMarker>,
        ),
    >,
    q_target: Query<
        &IsTargeted,
        (
            Without<Camera>,
            Without<Selectagon>,
            Without<PointOfInterestMarker>,
            Without<ToggleableHudElement>,
        ),
    >,
    mut ambient_light: ResMut<AmbientLight>,
    er_target: EventReader<UpdateOverlayVisibility>,
    settings: Res<Settings>,
    prefs: Res<Preferences>,
) {
    if er_target.is_empty() {
        return;
    }
    let check = {
        |check: bool| {
            if check {
                Visibility::Inherited
            } else {
                Visibility::Hidden
            }
        }
    };
    let show_poi = check(settings.hud_active && settings.map_active);
    let show_hud = check(settings.hud_active);
    let show_selectagon = check(settings.hud_active && !q_target.is_empty());
    for mut vis in &mut q_marker {
        *vis = show_poi;
    }
    for mut vis in &mut q_hudelement {
        *vis = show_hud;
    }
    for mut vis in &mut q_selectagon {
        *vis = show_selectagon;
    }

    ambient_light.brightness = if settings.hud_active {
        AMBIENT_LIGHT[prefs.light_amp]
    } else {
        AMBIENT_LIGHT[0]
    };
}

fn update_pointer(
    prefs: ResMut<Preferences>,
    mut q_pointer: Query<(&PointerComponent, &mut Visibility)>,
) {
    for (pointer, mut vis) in &mut q_pointer {
        *vis = if pointer.0 == prefs.pointer {
            Visibility::Inherited
        } else {
            Visibility::Hidden
        }
    }
}

fn update_avatar(
    mut commands: Commands,
    mut settings: ResMut<Settings>,
    mut prefs: ResMut<var::Preferences>,
    asset_server: Res<AssetServer>,
    q_avatar: Query<(Entity, &SceneInstance), With<PlayerAvatar>>,
    q_player: Query<Entity, With<actor::Player>>,
    mut ew_updatemenu: EventWriter<menu::UpdateMenuEvent>,
    mut scene_spawner: ResMut<SceneSpawner>,
) {
    if settings.ar_avatar >= PLAYER_AR_AVATARS.len() {
        settings.ar_avatar = settings.ar_avatar % PLAYER_AR_AVATARS.len();
    }
    prefs.avatar = settings.ar_avatar;
    prefs.save();
    ew_updatemenu.send(menu::UpdateMenuEvent);

    let ava = if let Some(ava) = PLAYER_AR_AVATARS.get(settings.ar_avatar) {
        ava
    } else {
        error!("Avatar index out of bounds!");
        return;
    };
    let model_name = ava.1;
    let model_scale = ava.2;

    for (entity, sceneinstance) in &q_avatar {
        commands.entity(entity).despawn();
        scene_spawner.despawn_instance(**sceneinstance);
    }

    if model_name.is_empty() {
        // No avatar selected.
        return;
    }

    if let Ok(player_entity) = q_player.get_single() {
        let mut entitycmd = commands.spawn((
            hud::AugmentedRealityOverlay {
                owner: player_entity,
                scale: model_scale,
            },
            world::DespawnOnPlayerDeath,
            PlayerAvatar,
            SpatialBundle {
                visibility: bool2vis(settings.hud_active),
                ..default()
            },
            NotShadowCaster,
        ));
        load_asset(model_name, &mut entitycmd, &*asset_server);
    }
}