//   ▄████████▄      +        ███ +  ▄█████████ ███     +
//  ███▀    ▀███ +         +  ███    ███▀  +    ███ +       +
//  ███  +   ███ ███   ███ █████████ ███        ███  ███   ███
//  ███     +███ ███   ███    ███    ███▐██████ ███  ███   ███
//  ███ +    ███ ███+  ███   +███    ███     +  ███  ███ + ███
//  ███▄    ▄███ ███▄  ███    ███ +  ███  +     ███  ███▄  ███
//   ▀████████▀ + ▀███████    ███▄   ███▄       ▀████ ▀███████
//       +                  +                +             ███
//  +   ▀████████████████████████████████████████████████████▀
//
// This module manages variables, settings, as well as evaluating
// "if"-conditions in chats.

use crate::prelude::*;
use bevy::prelude::*;
use bevy::window::WindowMode;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::env;
use std::fs;
use toml_edit::DocumentMut;

pub const SCOPE_SEPARATOR: &str = "$";

pub const TOKEN_EQUALS: &str = "==";
pub const TOKEN_EQUALS_NOT: &str = "!=";
pub const TOKEN_GREATER_THAN: &str = ">";
pub const TOKEN_LESS_THAN: &str = "<";
pub const TOKEN_GREATER_EQUALS: &str = ">=";
pub const TOKEN_LESS_EQUALS: &str = "<=";
pub const TOKEN_NEGATE: &str = "~";

pub const DEFAULT_CHAT_SPEED: f32 = 10.0;
pub const DEFAULT_CONFIG_TOML: &str = include_str!("data/outfly.toml");

#[derive(Resource)]
pub struct Settings {
    pub dev_mode: bool,
    pub god_mode: bool,
    pub version: String,
    pub alive: bool,
    pub mute_sfx: bool,
    pub noise_cancellation_mode: usize,
    pub noise_cancellation_modes: Vec<String>,
    pub radio_mode: usize,
    pub radio_modes: Vec<String>, // see also: settings.is_radio_playing()
    pub volume_sfx: u8,
    pub volume_music: u8,
    pub mouse_sensitivity: f32,
    pub fov: f32,
    pub fov_highspeed: f32,
    pub zoom_fov: f32,
    pub zoom_sensitivity_factor: f32,
    pub font_size_hud: f32,
    pub font_size_fps: f32,
    pub font_size_conversations: f32,
    pub font_size_choices: f32,
    pub font_size_console: f32,
    pub font_size_speedometer: f32,
    pub font_size_deathtext: f32,
    pub font_size_deathsubtext: f32,
    pub font_size_deathpoem: f32,
    pub font_size_death_achievements: f32,
    pub font_size_achievement: f32,
    pub font_size_achievement_header: f32,
    pub font_size_keybindings: f32,
    pub font_size_version: f32,
    pub hud_color: Color,
    pub hud_color_fps: Color,
    pub hud_color_console: Color,
    pub hud_color_console_warn: Color,
    pub hud_color_console_system: Color,
    pub hud_color_console_achievement: Color,
    pub hud_color_alert: Color,
    pub hud_color_subtitles: Color,
    pub hud_color_choices: Color,
    pub hud_color_speedometer: Color,
    pub hud_color_deathpoem: Color,
    pub hud_color_achievement: Color,
    pub hud_color_achievement_header: Color,
    pub hud_color_achievement_accomplished: Color,
    pub hud_color_death: Color,
    pub hud_color_death_achievements: Color,
    pub hud_color_keybindings: Color,
    pub hud_color_version: Color,
    pub chat_speed: f32,
    pub ar_avatar: usize,
    pub flashlight_active: bool,
    pub hud_active: bool,
    pub map_active: bool,
    pub deathscreen_active: bool,
    pub menu_active: bool,
    pub death_cause: String,
    pub is_zooming: bool,
    pub third_person: bool,
    pub rotation_stabilizer_active: bool,
    pub cruise_control_active: bool,
    pub shadows_sun: bool,
    pub shadows_pointlights: bool,
    pub shadowmap_resolution: usize,
    pub large_moons: bool,
    pub key_selectobject: MouseButton,
    pub key_zoom: MouseButton,
    pub key_map: KeyCode,
    pub key_map_zoom_out: KeyCode,
    pub key_map_zoom_in: KeyCode,
    //pub key_map_zoom_out_wheel: MouseButton,
    //pub key_map_zoom_in_wheel: MouseButton,
    pub key_togglehud: KeyCode,
    pub key_menu: KeyCode,
    pub key_fullscreen: KeyCode,
    pub key_help: KeyCode,
    pub key_forward: KeyCode,
    pub key_back: KeyCode,
    pub key_left: KeyCode,
    pub key_right: KeyCode,
    pub key_up: KeyCode,
    pub key_down: KeyCode,
    pub key_run: KeyCode,
    pub key_stop: KeyCode,
    pub key_interact: KeyCode,
    pub key_vehicle: KeyCode,
    pub key_camera: KeyCode,
    pub key_flashlight: KeyCode,
    pub key_cruise_control: KeyCode,
    pub key_rotate: KeyCode,
    pub key_rotation_stabilizer: KeyCode,
    pub key_mouseup: KeyCode,
    pub key_mousedown: KeyCode,
    pub key_mouseleft: KeyCode,
    pub key_mouseright: KeyCode,
    pub key_rotateleft: KeyCode,
    pub key_rotateright: KeyCode,
    pub key_reply1: KeyCode,
    pub key_reply2: KeyCode,
    pub key_reply3: KeyCode,
    pub key_reply4: KeyCode,
    pub key_reply5: KeyCode,
    pub key_reply6: KeyCode,
    pub key_reply7: KeyCode,
    pub key_reply8: KeyCode,
    pub key_reply9: KeyCode,
    pub key_reply10: KeyCode,
    pub key_cheat_god_mode: KeyCode,
    pub key_cheat_stop: KeyCode,
    pub key_cheat_speed: KeyCode,
    pub key_cheat_speed_backward: KeyCode,
    pub key_cheat_teleport: KeyCode,
    pub key_cheat_pizza: KeyCode,
    pub key_cheat_farview1: KeyCode,
    pub key_cheat_farview2: KeyCode,
    pub key_cheat_adrenaline_zero: KeyCode,
    pub key_cheat_adrenaline_mid: KeyCode,
    pub key_cheat_adrenaline_max: KeyCode,
    pub key_cheat_die: KeyCode,
}

impl Default for Settings {
    fn default() -> Self {
        let dev_mode = cfg!(feature = "dev_mode") && env::var("CARGO").is_ok();
        let version = if let Some(version) = option_env!("CARGO_PKG_VERSION") {
            version.to_string()
        } else {
            "".to_string()
        };

        Settings {
            dev_mode,
            god_mode: false,
            version,
            alive: true,
            mute_sfx: false,
            noise_cancellation_mode: 0,
            noise_cancellation_modes: vec![
                "Off".to_string(),
                "Ambience".to_string(),
                "Mechanical".to_string(),
                "Max".to_string(),
            ],
            radio_mode: 1,
            radio_modes: vec![
                // see also: settings.is_radio_playing()
                "Off".to_string(),
                "Cinematic Frequency".to_string(),
            ],
            volume_sfx: 100,
            volume_music: 100,
            mouse_sensitivity: 0.4,
            fov: 50.0,
            fov_highspeed: 25.0,
            zoom_fov: 15.0,
            zoom_sensitivity_factor: 0.25,
            font_size_hud: 24.0,
            font_size_fps: 14.0,
            font_size_conversations: 32.0,
            font_size_choices: 28.0,
            font_size_console: 20.0,
            font_size_speedometer: 34.0,
            font_size_deathtext: 64.0,
            font_size_deathsubtext: 32.0,
            font_size_deathpoem: 18.0,
            font_size_death_achievements: 24.0,
            font_size_achievement: 24.0,
            font_size_achievement_header: 32.0,
            font_size_keybindings: 20.0,
            font_size_version: 20.0,
            hud_color: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
            hud_color_fps: Srgba::hex("#181818").unwrap().into(),
            hud_color_console: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
            hud_color_console_achievement: Srgba::hex(COLOR_SUCCESS).unwrap().into(),
            hud_color_console_warn: Srgba::hex(COLOR_WARNING).unwrap().into(),
            hud_color_console_system: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
            hud_color_alert: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
            hud_color_subtitles: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
            hud_color_choices: Srgba::hex(COLOR_BODY).unwrap().into(),
            hud_color_speedometer: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
            hud_color_deathpoem: Srgba::hex("#CC2200").unwrap().into(),
            hud_color_achievement: Srgba::hex(COLOR_DIM).unwrap().into(),
            hud_color_achievement_accomplished: Srgba::hex(COLOR_SUCCESS).unwrap().into(),
            hud_color_achievement_header: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
            hud_color_death: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
            hud_color_death_achievements: Srgba::hex(COLOR_SECONDARY).unwrap().into(),
            hud_color_keybindings: Srgba::hex(COLOR_DIM).unwrap().into(),
            hud_color_version: Srgba::hex(COLOR_PRIMARY).unwrap().into(),
            chat_speed: DEFAULT_CHAT_SPEED * if dev_mode { 2.5 } else { 1.0 },
            ar_avatar: 0,
            flashlight_active: false,
            hud_active: true,
            map_active: false,
            deathscreen_active: false,
            menu_active: false,
            death_cause: "Unknown".to_string(),
            is_zooming: false,
            third_person: true,
            rotation_stabilizer_active: true,
            cruise_control_active: false,
            shadows_sun: true,
            shadows_pointlights: false,
            shadowmap_resolution: 2048,
            large_moons: false,
            key_selectobject: MouseButton::Left,
            key_zoom: MouseButton::Right,
            key_map: KeyCode::KeyM,
            key_map_zoom_out: KeyCode::ShiftLeft,
            key_map_zoom_in: KeyCode::ControlLeft,
            //key_map_zoom_out_wheel: KeyCode::Shift,
            //key_map_zoom_in_wheel: KeyCode::Shift,
            key_togglehud: KeyCode::Tab,
            key_menu: KeyCode::Escape,
            key_fullscreen: KeyCode::F11,
            key_help: KeyCode::F1,
            key_forward: KeyCode::KeyW,
            key_back: KeyCode::KeyS,
            key_left: KeyCode::KeyA,
            key_right: KeyCode::KeyD,
            key_up: KeyCode::ShiftLeft,
            key_down: KeyCode::ControlLeft,
            key_run: KeyCode::KeyR,
            key_stop: KeyCode::Space,
            key_interact: KeyCode::KeyE,
            key_vehicle: KeyCode::KeyQ,
            key_camera: KeyCode::KeyC,
            key_flashlight: KeyCode::KeyF,
            key_cruise_control: KeyCode::KeyT,
            key_rotate: KeyCode::KeyR,
            key_rotation_stabilizer: KeyCode::KeyY,
            key_mouseup: KeyCode::KeyI,
            key_mousedown: KeyCode::KeyK,
            key_mouseleft: KeyCode::KeyJ,
            key_mouseright: KeyCode::KeyL,
            key_rotateleft: KeyCode::KeyU,
            key_rotateright: KeyCode::KeyO,
            key_reply1: KeyCode::Digit1,
            key_reply2: KeyCode::Digit2,
            key_reply3: KeyCode::Digit3,
            key_reply4: KeyCode::Digit4,
            key_reply5: KeyCode::Digit5,
            key_reply6: KeyCode::Digit6,
            key_reply7: KeyCode::Digit7,
            key_reply8: KeyCode::Digit8,
            key_reply9: KeyCode::Digit9,
            key_reply10: KeyCode::Digit0,
            key_cheat_god_mode: KeyCode::KeyG,
            key_cheat_stop: KeyCode::KeyZ,
            key_cheat_speed: KeyCode::KeyV,
            key_cheat_speed_backward: KeyCode::KeyB,
            key_cheat_teleport: KeyCode::KeyX,
            key_cheat_pizza: KeyCode::F9,
            key_cheat_farview1: KeyCode::F10,
            key_cheat_farview2: KeyCode::F12,
            key_cheat_adrenaline_zero: KeyCode::F5,
            key_cheat_adrenaline_mid: KeyCode::F6,
            key_cheat_adrenaline_max: KeyCode::F8,
            key_cheat_die: KeyCode::F4,
        }
    }
}

impl Settings {
    #[allow(dead_code)]
    pub fn reset(&mut self) {
        println!("Resetting settings!");
        *self = Self::default();
    }

    pub fn reset_player_settings(&mut self) {
        println!("Resetting player settings!");
        let default = Self::default();
        self.rotation_stabilizer_active = default.rotation_stabilizer_active;
        self.is_zooming = default.is_zooming;
        self.flashlight_active = default.flashlight_active;
        self.cruise_control_active = default.cruise_control_active;
        self.map_active = default.map_active;
    }

    pub fn get_reply_keys(&self) -> [KeyCode; 10] {
        return [
            self.key_reply1,
            self.key_reply2,
            self.key_reply3,
            self.key_reply4,
            self.key_reply5,
            self.key_reply6,
            self.key_reply7,
            self.key_reply8,
            self.key_reply9,
            self.key_reply10,
        ];
    }

    pub fn in_control(&self) -> bool {
        return self.alive && !self.menu_active;
    }

    pub fn is_radio_playing(&self, sfx: audio::Sfx) -> Option<bool> {
        let radio = self.radio_mode;
        let ambience = self.noise_cancellation_mode != 1 && self.noise_cancellation_mode != 3;
        match sfx {
            audio::Sfx::BGM => Some(radio == 1),
            audio::Sfx::BGMActualJupiterRecording => Some(radio == 0 && ambience),
            _ => None,
        }
    }

    pub fn set_noise_cancellation_mode(&mut self, value: usize) {
        let value = if value >= self.noise_cancellation_modes.len() {
            warn!("Attempting to set too large noise cancellation mode: {value}");
            0
        } else {
            value
        };
        self.noise_cancellation_mode = value;
        self.mute_sfx = value >= 2;
    }
}

#[derive(Resource, Default, Debug)]
pub struct AchievementTracker {
    pub repair_suit: bool,
    pub drink_a_pizza: bool,
    pub in_jupiters_shadow: bool,
    pub find_earth: bool,

    pub ride_every_vehicle: bool,
    pub vehicles_ridden: HashSet<String>,
    pub all_vehicles: HashSet<String>,

    pub talk_to_everyone: bool,
    pub people_talked_to: HashSet<String>,
    pub all_people: HashSet<String>,
}

impl AchievementTracker {
    pub fn to_bool_vec(&self) -> Vec<bool> {
        vec![
            self.repair_suit,
            self.drink_a_pizza,
            self.ride_every_vehicle,
            self.talk_to_everyone,
            self.find_earth,
            self.in_jupiters_shadow,
        ]
    }
    pub fn achieve_all(&mut self) {
        self.repair_suit = true;
        self.drink_a_pizza = true;
        self.ride_every_vehicle = true;
        self.talk_to_everyone = true;
        self.find_earth = true;
        self.in_jupiters_shadow = true;
    }
    pub fn to_textsections(&self) -> Vec<String> {
        fn collectible(current: usize, total: usize) -> String {
            if current < total {
                format!(" ({}/{})", current, total)
            } else {
                "".to_string()
            }
        }
        let ride = collectible(self.vehicles_ridden.len(), self.all_vehicles.len());
        let talk = collectible(self.people_talked_to.len(), self.all_people.len());
        vec![
            "Repair Your Suit\n".to_string(),
            "Enjoy A Pizza\n".to_string(),
            format!("Ride Every Vehicle{ride}\n"),
            format!("Talk To Everyone{talk}\n"),
            "Find Earth\n".to_string(),
            "Enter Jupiter's Shadow\n".to_string(),
        ]
    }
    pub fn to_overview(&self) -> Vec<(bool, String)> {
        vec![
            (self.repair_suit, "repair your suit".into()),
            (self.drink_a_pizza, "enjoy a pizza".into()),
            (self.ride_every_vehicle, "ride every vehicle".into()),
            (self.talk_to_everyone, "talk to everyone".into()),
            (self.find_earth, "find Earth".into()),
            (self.in_jupiters_shadow, "enter Jupiter's shadow".into()),
        ]
    }
    pub fn to_summary(&self) -> String {
        let list = self.to_overview();
        let count = list.iter().filter(|(achieved, _)| *achieved).count();
        if count == 0 {
            return "".to_string();
        }
        let mut summary = "\n\n\nYou managed to ".to_string();
        for (i, (_, text)) in list.iter().filter(|(achieved, _)| *achieved).enumerate() {
            summary += text.as_str();
            if i + 2 == count {
                summary += ", and ";
            } else if i + 1 == count {
                summary += " before you perished.";
                if count == list.len() {
                    summary += "\nA truly astounding achievement, a glimmer in the void, before it all fades, into nothingness.";
                }
            } else {
                summary += ", ";
            }
        }
        summary
    }
}

#[derive(Resource, Serialize, Deserialize, Debug, Default)]
#[serde(default)]
pub struct Preferences {
    pub fullscreen_mode: String,
    pub fullscreen_on: bool,
    pub render_mode: String,
    pub augmented_reality: bool,
    pub radio_station: usize,
    pub noise_cancellation_mode: usize,
    pub third_person: bool,
    pub shadows_sun: bool,
    pub avatar: usize,

    #[serde(skip)]
    pub source_file: Option<String>,
}

impl Preferences {
    pub fn get_fullscreen_mode(&self) -> WindowMode {
        match self.fullscreen_mode.as_str() {
            "legacy" => WindowMode::Fullscreen,
            "sized" => WindowMode::SizedFullscreen,
            _ => WindowMode::BorderlessFullscreen,
        }
    }
    pub fn get_window_mode(&self) -> WindowMode {
        match self.fullscreen_on {
            true => self.get_fullscreen_mode(),
            false => WindowMode::Windowed,
        }
    }
    pub fn render_mode_is_gl(&self) -> bool {
        return self.render_mode == "gl";
    }

    pub fn save(&self) {
        if let Some(path) = get_prefs_path() {
            match toml_edit::ser::to_document::<Preferences>(self) {
                Ok(doc) => match fs::write(path.clone(), doc.to_string()) {
                    Ok(_) => {
                        info!("Saved preferences to {path}.");
                    }
                    Err(error) => {
                        error!("Error while writing preferences: {:?}", error);
                    }
                },
                Err(error) => {
                    error!("Error while writing preferences: {:?}", error);
                }
            }
        }
    }
}

fn file_is_readable(file_path: &str) -> bool {
    fs::metadata(file_path)
        .map(|metadata| metadata.is_file())
        .unwrap_or(false)
}

fn path_is_directory(file_path: &str) -> bool {
    fs::metadata(file_path)
        .map(|metadata| metadata.is_dir())
        .unwrap_or(false)
}

fn get_prefs_path() -> Option<String> {
    let test = CONF_FILE;
    if file_is_readable(test) {
        return Some(test.to_string());
    }
    if let Some(mut conf) = dirs::config_dir() {
        conf.push("OutFly");
        if !conf.exists() {
            match fs::create_dir_all(&conf) {
                Ok(_) => {}
                Err(error) => {
                    eprintln!("Failed creating configuration directory: {error}");
                }
            }
        }
        if let Some(test) = conf.to_str() {
            if !path_is_directory(test) {
                eprintln!("Failed creating configuration directory");
                return None;
            }
        }

        conf.push(CONF_FILE);
        if !conf.exists() {
            match fs::write(&conf, DEFAULT_CONFIG_TOML.to_string()) {
                Ok(_) => {}
                Err(error) => {
                    eprintln!("Failed creating configuration file: {error}");
                }
            }
        }
        if let Some(test) = conf.to_str() {
            if file_is_readable(test) {
                return Some(test.to_string());
            }
        }
    }
    return None;
}

pub fn load_prefs() -> Preferences {
    let (toml, path) = match get_prefs_path() {
        Some(path) => {
            let toml = fs::read_to_string(&path);
            match toml {
                Ok(toml) => (toml, Some(path)),
                Err(error) => {
                    eprintln!("Error: Failed to open preferences file '{path}': {error}");
                    return Preferences::default();
                }
            }
        }
        None => {
            println!("Found no preference file, using default preferences.");
            (DEFAULT_CONFIG_TOML.to_string(), None)
        }
    };
    match toml.parse::<DocumentMut>() {
        Ok(doc) => match toml_edit::de::from_document::<Preferences>(doc) {
            Ok(mut pref) => {
                if let Some(path) = &path {
                    println!("Loaded preference file from {path}");
                } else {
                    println!("Loaded preferences from internal defaults");
                }
                pref.source_file = path;
                return pref;
            }
            Err(error) => {
                eprintln!("Error: Failed to read preference line: {error}");
                return Preferences::default();
            }
        },
        Err(error) => {
            eprintln!("Error: Failed to open preferences: {error}");
            return Preferences::default();
        }
    }
}

#[derive(Resource, Debug)]
pub struct GameVars {
    pub db: HashMap<String, String>,
}

impl Default for GameVars {
    fn default() -> Self {
        Self { db: HashMap::new() }
    }
}

impl GameVars {
    pub fn reset(&mut self) {
        self.db.clear();
    }

    #[allow(dead_code)]
    pub fn get(&self, key: &str) -> Option<String> {
        if let Some(value) = self.db.get(key) {
            return Some(value.clone());
        }
        return None;
    }

    #[allow(dead_code)]
    pub fn getf(&self, key: &str) -> Option<f64> {
        if let Some(value) = self.db.get(key) {
            if let Ok(float) = value.parse::<f64>() {
                return Some(float);
            }
        }
        return None;
    }

    pub fn getb(&self, key: &str) -> bool {
        if let Some(value) = self.db.get(key) {
            return Self::evaluate_str_as_bool(value);
        }
        return false;
    }

    pub fn evaluate_str_as_bool(string: &str) -> bool {
        return string != "0";
    }

    // This method ensures that the variable name contains a scope separator,
    // and if a scope is missing, it prefixes the fallback scope.
    // Should NOT be used on non-variable values, like plain strings.
    //
    // See test_normalize_varname() for examples.
    pub fn normalize_varname(fallback_scope: &str, key: &str) -> String {
        let parts: Vec<&str> = key.split(SCOPE_SEPARATOR).collect();
        let key: String = if parts.len() == 1 {
            // we got a key like "foo", turn it into "<scope>$foo"
            fallback_scope.to_string() + SCOPE_SEPARATOR + key
        } else if parts.len() > 1 {
            // we got a key with at least one "$"
            // extract anything before the last "$":
            let scope_part: String = parts[0..parts.len() - 1].join(SCOPE_SEPARATOR);

            if scope_part.is_empty() {
                // we got a key like "$foo", just prefix the fallback scope
                fallback_scope.to_string() + key
            } else {
                // we got a key like "Ke$ha$foo" or "$$foo" (which is the convention for
                // global variables), leave the scope intact
                key.to_string()
            }
        } else {
            // we got an empty string. this is bad, but handle gracefully
            fallback_scope.to_string() + SCOPE_SEPARATOR
        };
        return key.to_lowercase();
    }

    pub fn set_in_scope(&mut self, fallback_scope: &str, key: &str, value: String) {
        let key = Self::normalize_varname(fallback_scope, key);
        self.db.insert(key, value);
    }

    pub fn evaluate_condition(&self, condition: &str, scope: &str) -> bool {
        let parts: Vec<&str> = condition.split(" ").collect();
        if parts.len() == 0 {
            // Got an empty string, this is always false.
            return false;
        } else if parts.len() == 1 {
            // Got something like "if $somevar:".
            // Check whether the variable evaluates to true.
            let part = parts[0];
            let (part, negate) = if part.starts_with(TOKEN_NEGATE) {
                (&part[1..], true)
            } else {
                (part, false)
            };
            if part.contains(SCOPE_SEPARATOR) {
                let part = Self::normalize_varname(scope, part);
                let value_bool = self.getb(part.as_str());
                return value_bool ^ negate;
            } else {
                return Self::evaluate_str_as_bool(part) ^ negate;
            }
        } else if parts.len() == 2 {
            // Got something like "if $something somethingelse"
            // Check whether the two are identical.
            let mut left: String = parts[0].to_string();
            if left.contains(SCOPE_SEPARATOR) {
                let key = Self::normalize_varname(scope, left.as_str());
                let value = self.get(key.as_str());
                left = if let Some(value) = value {
                    value
                } else {
                    warn!("Couldn't find variable `{key}` on left hand side of a condition");
                    "".to_string()
                };
            }
            let mut right: String = parts[1].to_string();
            if right.contains(SCOPE_SEPARATOR) {
                let key = Self::normalize_varname(scope, right.as_str());
                let value = self.get(key.as_str());
                right = if let Some(value) = value {
                    value
                } else {
                    warn!("Couldn't find variable `{key}` on right hand side of a condition");
                    "".to_string()
                };
            }
            return left == right;
        } else {
            // Got something like "if $something != somethingelse bla bla"
            let mut left: String = parts[0].to_string();
            if left.contains(SCOPE_SEPARATOR) {
                let key = Self::normalize_varname(scope, left.as_str());
                let value = self.get(key.as_str());
                left = if let Some(value) = value {
                    value
                } else {
                    warn!("Couldn't find variable `{key}` on left hand side of a condition");
                    "".to_string()
                };
            }

            let mut right: String = parts[2..parts.len()].join(" ").to_string();
            if right.contains(SCOPE_SEPARATOR) {
                let key = Self::normalize_varname(scope, right.as_str());
                let value = self.get(key.as_str());
                right = if let Some(value) = value {
                    value
                } else {
                    warn!("Couldn't find variable `{key}` on right hand side of a condition");
                    "".to_string()
                };
            }
            let floats = (left.parse::<f64>(), right.parse::<f64>());
            let operator: &str = parts[1];

            match operator {
                TOKEN_EQUALS => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left == right;
                    }
                    return left == right;
                }
                TOKEN_EQUALS_NOT => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left != right;
                    }
                    return left != right;
                }
                TOKEN_GREATER_THAN => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left > right;
                    }
                    return false;
                }
                TOKEN_GREATER_EQUALS => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left >= right;
                    }
                    return false;
                }
                TOKEN_LESS_THAN => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left < right;
                    }
                    return false;
                }
                TOKEN_LESS_EQUALS => {
                    if let (Ok(left), Ok(right)) = floats {
                        return left <= right;
                    }
                    return false;
                }
                _ => {
                    error!("Unknown operator '{operator}' in if-condition!");
                    return false;
                }
            }
        }
    }
}

#[test]
fn test_normalize_varname() {
    assert_eq!(GameVars::normalize_varname("Clippy", ""), "clippy$");
    assert_eq!(GameVars::normalize_varname("Clippy", "foo"), "clippy$foo");
    assert_eq!(GameVars::normalize_varname("Clippy", "FOO"), "clippy$foo");
    assert_eq!(GameVars::normalize_varname("Clippy", "$foo"), "clippy$foo");
    assert_eq!(GameVars::normalize_varname("Clippy", "$$foo"), "$$foo");
    assert_eq!(
        GameVars::normalize_varname("Clippy", "PizzaClippy$foo"),
        "pizzaclippy$foo"
    );
    assert_eq!(
        GameVars::normalize_varname("Clippy", "$foo$foo$foo$foo"),
        "$foo$foo$foo$foo"
    );
}

#[derive(Resource, Default)]
pub struct CommandLineOptions {
    pub window_mode_fullscreen: WindowMode,
    pub window_mode_initial: WindowMode,
    pub use_gl: bool,
}