use crate::{
    hud::{BarNumbers, BuffPosition, CrosshairType, Intro, PressBehavior, ShortcutNumbers, XpBar},
    i18n,
    render::RenderMode,
    ui::ScaleMode,
    window::{FullScreenSettings, GameInput, KeyMouse},
};
use directories_next::UserDirs;
use hashbrown::{HashMap, HashSet};
use serde::{Deserialize, Serialize};
use std::{fmt, fs, path::PathBuf};
use tracing::warn;
use vek::*;
use winit::event::{MouseButton, VirtualKeyCode};
// ControlSetting-like struct used by Serde, to handle not serializing/building
// post-deserializing the inverse_keybindings hashmap
#[derive(Serialize, Deserialize)]
struct ControlSettingsSerde {
    keybindings: HashMap<GameInput, KeyMouse>,
}

impl From<ControlSettings> for ControlSettingsSerde {
    fn from(control_settings: ControlSettings) -> Self {
        let mut user_bindings: HashMap<GameInput, KeyMouse> = HashMap::new();
        // Do a delta between default() ControlSettings and the argument, and let
        // keybindings be only the custom keybindings chosen by the user.
        for (k, v) in control_settings.keybindings {
            if ControlSettings::default_binding(k) != v {
                // Keybinding chosen by the user
                user_bindings.insert(k, v);
            }
        }
        ControlSettingsSerde {
            keybindings: user_bindings,
        }
    }
}

/// `ControlSettings` contains keybindings.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(from = "ControlSettingsSerde", into = "ControlSettingsSerde")]
pub struct ControlSettings {
    pub keybindings: HashMap<GameInput, KeyMouse>,
    pub inverse_keybindings: HashMap<KeyMouse, HashSet<GameInput>>, // used in event loop
}

impl From<ControlSettingsSerde> for ControlSettings {
    fn from(control_serde: ControlSettingsSerde) -> Self {
        let user_keybindings = control_serde.keybindings;
        let mut control_settings = ControlSettings::default();
        for (k, v) in user_keybindings {
            control_settings.modify_binding(k, v);
        }
        control_settings
    }
}

/// Since Macbook trackpads lack middle click, on OS X we default to LShift
/// instead It is an imperfect heuristic, but hopefully it will be a slightly
/// better default, and the two places we default to middle click currently
/// (roll and wall jump) are both situations where you cannot glide (the other
/// default mapping for LShift).
#[cfg(target_os = "macos")]
const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Key(VirtualKeyCode::LShift);
#[cfg(not(target_os = "macos"))]
const MIDDLE_CLICK_KEY: KeyMouse = KeyMouse::Mouse(MouseButton::Middle);

impl ControlSettings {
    pub fn get_binding(&self, game_input: GameInput) -> Option<KeyMouse> {
        self.keybindings.get(&game_input).copied()
    }

    pub fn get_associated_game_inputs(&self, key_mouse: &KeyMouse) -> Option<&HashSet<GameInput>> {
        self.inverse_keybindings.get(key_mouse)
    }

    pub fn insert_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) {
        self.keybindings.insert(game_input, key_mouse);
        self.inverse_keybindings
            .entry(key_mouse)
            .or_default()
            .insert(game_input);
    }

    pub fn modify_binding(&mut self, game_input: GameInput, key_mouse: KeyMouse) {
        // For the KeyMouse->GameInput hashmap, we first need to remove the GameInput
        // from the old binding
        if let Some(old_binding) = self.get_binding(game_input) {
            self.inverse_keybindings
                .entry(old_binding)
                .or_default()
                .remove(&game_input);
        }
        // then we add the GameInput to the proper key
        self.inverse_keybindings
            .entry(key_mouse)
            .or_default()
            .insert(game_input);
        // For the GameInput->KeyMouse hashmap, just overwrite the value
        self.keybindings.insert(game_input, key_mouse);
    }

    /// Return true if this key is used for multiple GameInputs that aren't
    /// expected to be safe to have bound to the same key at the same time
    pub fn has_conflicting_bindings(&self, key_mouse: KeyMouse) -> bool {
        if let Some(game_inputs) = self.inverse_keybindings.get(&key_mouse) {
            for a in game_inputs.iter() {
                for b in game_inputs.iter() {
                    if !GameInput::can_share_bindings(*a, *b) {
                        return true;
                    }
                }
            }
        }
        false
    }

    pub fn default_binding(game_input: GameInput) -> KeyMouse {
        // If a new GameInput is added, be sure to update GameInput::iterator() too!
        match game_input {
            GameInput::Primary => KeyMouse::Mouse(MouseButton::Left),
            GameInput::Secondary => KeyMouse::Mouse(MouseButton::Right),
            GameInput::ToggleCursor => KeyMouse::Key(VirtualKeyCode::Comma),
            GameInput::Escape => KeyMouse::Key(VirtualKeyCode::Escape),
            GameInput::Chat => KeyMouse::Key(VirtualKeyCode::Return),
            GameInput::Command => KeyMouse::Key(VirtualKeyCode::Slash),
            GameInput::MoveForward => KeyMouse::Key(VirtualKeyCode::W),
            GameInput::MoveLeft => KeyMouse::Key(VirtualKeyCode::A),
            GameInput::MoveBack => KeyMouse::Key(VirtualKeyCode::S),
            GameInput::MoveRight => KeyMouse::Key(VirtualKeyCode::D),
            GameInput::Jump => KeyMouse::Key(VirtualKeyCode::Space),
            GameInput::Sit => KeyMouse::Key(VirtualKeyCode::K),
            GameInput::Dance => KeyMouse::Key(VirtualKeyCode::J),
            GameInput::Glide => KeyMouse::Key(VirtualKeyCode::LShift),
            GameInput::Climb => KeyMouse::Key(VirtualKeyCode::Space),
            GameInput::ClimbDown => KeyMouse::Key(VirtualKeyCode::LControl),
            GameInput::SwimUp => KeyMouse::Key(VirtualKeyCode::Space),
            GameInput::SwimDown => KeyMouse::Key(VirtualKeyCode::LShift),
            GameInput::Fly => KeyMouse::Key(VirtualKeyCode::H),
            GameInput::Sneak => KeyMouse::Key(VirtualKeyCode::LControl),
            GameInput::ToggleLantern => KeyMouse::Key(VirtualKeyCode::G),
            GameInput::Mount => KeyMouse::Key(VirtualKeyCode::F),
            GameInput::Map => KeyMouse::Key(VirtualKeyCode::M),
            GameInput::Bag => KeyMouse::Key(VirtualKeyCode::B),
            GameInput::Trade => KeyMouse::Key(VirtualKeyCode::R),
            GameInput::Social => KeyMouse::Key(VirtualKeyCode::O),
            GameInput::Crafting => KeyMouse::Key(VirtualKeyCode::C),
            GameInput::Spellbook => KeyMouse::Key(VirtualKeyCode::P),
            GameInput::Settings => KeyMouse::Key(VirtualKeyCode::N),
            GameInput::Help => KeyMouse::Key(VirtualKeyCode::F1),
            GameInput::ToggleInterface => KeyMouse::Key(VirtualKeyCode::F2),
            GameInput::ToggleDebug => KeyMouse::Key(VirtualKeyCode::F3),
            GameInput::Fullscreen => KeyMouse::Key(VirtualKeyCode::F11),
            GameInput::Screenshot => KeyMouse::Key(VirtualKeyCode::F4),
            GameInput::ToggleIngameUi => KeyMouse::Key(VirtualKeyCode::F6),
            GameInput::Roll => MIDDLE_CLICK_KEY,
            GameInput::Respawn => KeyMouse::Key(VirtualKeyCode::Space),
            GameInput::Interact => KeyMouse::Key(VirtualKeyCode::E),
            GameInput::ToggleWield => KeyMouse::Key(VirtualKeyCode::T),
            GameInput::FreeLook => KeyMouse::Key(VirtualKeyCode::L),
            GameInput::AutoWalk => KeyMouse::Key(VirtualKeyCode::Period),
            GameInput::CameraClamp => KeyMouse::Key(VirtualKeyCode::Apostrophe),
            GameInput::CycleCamera => KeyMouse::Key(VirtualKeyCode::Key0),
            GameInput::Slot1 => KeyMouse::Key(VirtualKeyCode::Key1),
            GameInput::Slot2 => KeyMouse::Key(VirtualKeyCode::Key2),
            GameInput::Slot3 => KeyMouse::Key(VirtualKeyCode::Key3),
            GameInput::Slot4 => KeyMouse::Key(VirtualKeyCode::Key4),
            GameInput::Slot5 => KeyMouse::Key(VirtualKeyCode::Key5),
            GameInput::Slot6 => KeyMouse::Key(VirtualKeyCode::Key6),
            GameInput::Slot7 => KeyMouse::Key(VirtualKeyCode::Key7),
            GameInput::Slot8 => KeyMouse::Key(VirtualKeyCode::Key8),
            GameInput::Slot9 => KeyMouse::Key(VirtualKeyCode::Key9),
            GameInput::Slot10 => KeyMouse::Key(VirtualKeyCode::Q),
            GameInput::SwapLoadout => KeyMouse::Key(VirtualKeyCode::Tab),
            GameInput::Select => KeyMouse::Key(VirtualKeyCode::Y),
            GameInput::AcceptGroupInvite => KeyMouse::Key(VirtualKeyCode::U),
            GameInput::DeclineGroupInvite => KeyMouse::Key(VirtualKeyCode::I),
        }
    }
}

impl Default for ControlSettings {
    fn default() -> Self {
        let mut new_settings = Self {
            keybindings: HashMap::new(),
            inverse_keybindings: HashMap::new(),
        };
        // Sets the initial keybindings for those GameInputs.
        for game_input in GameInput::iterator() {
            new_settings.insert_binding(game_input, ControlSettings::default_binding(game_input));
        }
        new_settings
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GamepadSettings {
    pub game_buttons: con_settings::GameButtons,
    pub menu_buttons: con_settings::MenuButtons,
    pub game_axis: con_settings::GameAxis,
    pub menu_axis: con_settings::MenuAxis,
    pub game_analog_buttons: con_settings::GameAnalogButton,
    pub menu_analog_buttons: con_settings::MenuAnalogButton,
    pub pan_sensitivity: u32,
    pub pan_invert_y: bool,
    pub axis_deadzones: HashMap<crate::controller::Axis, f32>,
    pub button_deadzones: HashMap<crate::controller::AnalogButton, f32>,
    pub mouse_emulation_sensitivity: u32,
    pub inverted_axes: Vec<crate::controller::Axis>,
}

impl Default for GamepadSettings {
    fn default() -> Self {
        Self {
            game_buttons: con_settings::GameButtons::default(),
            menu_buttons: con_settings::MenuButtons::default(),
            game_axis: con_settings::GameAxis::default(),
            menu_axis: con_settings::MenuAxis::default(),
            game_analog_buttons: con_settings::GameAnalogButton::default(),
            menu_analog_buttons: con_settings::MenuAnalogButton::default(),
            pan_sensitivity: 10,
            pan_invert_y: false,
            axis_deadzones: HashMap::new(),
            button_deadzones: HashMap::new(),
            mouse_emulation_sensitivity: 12,
            inverted_axes: Vec::new(),
        }
    }
}

pub mod con_settings {
    use crate::controller::*;
    use gilrs::{Axis as GilAxis, Button as GilButton};
    use serde::{Deserialize, Serialize};

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct GameButtons {
        pub primary: Button,
        pub secondary: Button,
        pub toggle_cursor: Button,
        pub escape: Button,
        pub enter: Button,
        pub command: Button,
        pub move_forward: Button,
        pub move_left: Button,
        pub move_back: Button,
        pub move_right: Button,
        pub jump: Button,
        pub sit: Button,
        pub dance: Button,
        pub glide: Button,
        pub climb: Button,
        pub climb_down: Button,
        pub swimup: Button,
        pub swimdown: Button,
        pub sneak: Button,
        pub toggle_lantern: Button,
        pub mount: Button,
        pub map: Button,
        pub bag: Button,
        pub quest_log: Button,
        pub character_window: Button,
        pub social: Button,
        pub crafting: Button,
        pub spellbook: Button,
        pub settings: Button,
        pub help: Button,
        pub toggle_interface: Button,
        pub toggle_debug: Button,
        pub fullscreen: Button,
        pub screenshot: Button,
        pub toggle_ingame_ui: Button,
        pub roll: Button,
        pub respawn: Button,
        pub interact: Button,
        pub toggle_wield: Button,
        pub swap_loadout: Button,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct MenuButtons {
        pub up: Button,
        pub down: Button,
        pub left: Button,
        pub right: Button,
        pub scroll_up: Button,
        pub scroll_down: Button,
        pub scroll_left: Button,
        pub scroll_right: Button,
        pub home: Button,
        pub end: Button,
        pub apply: Button,
        pub back: Button,
        pub exit: Button,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct GameAxis {
        pub movement_x: Axis,
        pub movement_y: Axis,
        pub camera_x: Axis,
        pub camera_y: Axis,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct MenuAxis {
        pub move_x: Axis,
        pub move_y: Axis,
        pub scroll_x: Axis,
        pub scroll_y: Axis,
    }

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct GameAnalogButton {}

    #[derive(Clone, Debug, Serialize, Deserialize)]
    #[serde(default)]
    pub struct MenuAnalogButton {}

    impl Default for GameButtons {
        fn default() -> Self {
            // binding to unknown = getting skipped from processing
            Self {
                primary: Button::Simple(GilButton::RightTrigger2),
                secondary: Button::Simple(GilButton::LeftTrigger2),
                toggle_cursor: Button::Simple(GilButton::Unknown),
                escape: Button::Simple(GilButton::Start),
                enter: Button::Simple(GilButton::Unknown),
                command: Button::Simple(GilButton::Unknown),
                move_forward: Button::Simple(GilButton::Unknown),
                move_left: Button::Simple(GilButton::Unknown),
                move_back: Button::Simple(GilButton::Unknown),
                move_right: Button::Simple(GilButton::Unknown),
                jump: Button::Simple(GilButton::South),
                sit: Button::Simple(GilButton::Unknown),
                dance: Button::Simple(GilButton::Unknown),
                glide: Button::Simple(GilButton::LeftTrigger),
                climb: Button::Simple(GilButton::South),
                climb_down: Button::Simple(GilButton::East),
                swimup: Button::Simple(GilButton::South),
                swimdown: Button::Simple(GilButton::East),
                sneak: Button::Simple(GilButton::East),
                toggle_lantern: Button::Simple(GilButton::DPadLeft),
                mount: Button::Simple(GilButton::North),
                map: Button::Simple(GilButton::Select),
                bag: Button::Simple(GilButton::DPadRight),
                quest_log: Button::Simple(GilButton::Unknown),
                character_window: Button::Simple(GilButton::Unknown),
                social: Button::Simple(GilButton::Unknown),
                crafting: Button::Simple(GilButton::DPadDown),
                spellbook: Button::Simple(GilButton::Unknown),
                settings: Button::Simple(GilButton::Unknown),
                help: Button::Simple(GilButton::Unknown),
                toggle_interface: Button::Simple(GilButton::Unknown),
                toggle_debug: Button::Simple(GilButton::Unknown),
                fullscreen: Button::Simple(GilButton::Unknown),
                screenshot: Button::Simple(GilButton::Unknown),
                toggle_ingame_ui: Button::Simple(GilButton::Unknown),
                roll: Button::Simple(GilButton::RightTrigger),
                respawn: Button::Simple(GilButton::South),
                interact: Button::Simple(GilButton::North),
                toggle_wield: Button::Simple(GilButton::West),
                swap_loadout: Button::Simple(GilButton::LeftThumb),
            }
        }
    }

    impl Default for MenuButtons {
        fn default() -> Self {
            Self {
                up: Button::Simple(GilButton::Unknown),
                down: Button::Simple(GilButton::Unknown),
                left: Button::Simple(GilButton::Unknown),
                right: Button::Simple(GilButton::Unknown),
                scroll_up: Button::Simple(GilButton::Unknown),
                scroll_down: Button::Simple(GilButton::Unknown),
                scroll_left: Button::Simple(GilButton::Unknown),
                scroll_right: Button::Simple(GilButton::Unknown),
                home: Button::Simple(GilButton::DPadUp),
                end: Button::Simple(GilButton::DPadDown),
                apply: Button::Simple(GilButton::South),
                back: Button::Simple(GilButton::East),
                exit: Button::Simple(GilButton::Mode),
            }
        }
    }

    impl Default for GameAxis {
        fn default() -> Self {
            Self {
                movement_x: Axis::Simple(GilAxis::LeftStickX),
                movement_y: Axis::Simple(GilAxis::LeftStickY),
                camera_x: Axis::Simple(GilAxis::RightStickX),
                camera_y: Axis::Simple(GilAxis::RightStickY),
            }
        }
    }

    impl Default for MenuAxis {
        fn default() -> Self {
            Self {
                move_x: Axis::Simple(GilAxis::RightStickX),
                move_y: Axis::Simple(GilAxis::RightStickY),
                scroll_x: Axis::Simple(GilAxis::LeftStickX),
                scroll_y: Axis::Simple(GilAxis::LeftStickY),
            }
        }
    }

    impl Default for GameAnalogButton {
        fn default() -> Self { Self {} }
    }

    impl Default for MenuAnalogButton {
        fn default() -> Self { Self {} }
    }
}

/// `InterfaceSettings` contains UI, HUD and Map options.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct InterfaceSettings {
    pub toggle_debug: bool,
    pub sct: bool,
    pub sct_player_batch: bool,
    pub sct_damage_batch: bool,
    pub speech_bubble_dark_mode: bool,
    pub speech_bubble_icon: bool,
    pub crosshair_transp: f32,
    pub chat_transp: f32,
    pub chat_character_name: bool,
    pub crosshair_type: CrosshairType,
    pub intro_show: Intro,
    pub xp_bar: XpBar,
    pub shortcut_numbers: ShortcutNumbers,
    pub buff_position: BuffPosition,
    pub bar_numbers: BarNumbers,
    pub ui_scale: ScaleMode,
    pub map_zoom: f64,
    pub map_drag: Vec2<f64>,
    pub map_show_topo_map: bool,
    pub map_show_difficulty: bool,
    pub map_show_towns: bool,
    pub map_show_dungeons: bool,
    pub map_show_castles: bool,
    pub loading_tips: bool,
    pub map_show_caves: bool,
    pub map_show_trees: bool,
    pub minimap_show: bool,
    pub minimap_face_north: bool,
}

impl Default for InterfaceSettings {
    fn default() -> Self {
        Self {
            toggle_debug: false,
            sct: true,
            sct_player_batch: false,
            sct_damage_batch: false,
            speech_bubble_dark_mode: false,
            speech_bubble_icon: true,
            crosshair_transp: 0.6,
            chat_transp: 0.4,
            chat_character_name: true,
            crosshair_type: CrosshairType::Round,
            intro_show: Intro::Show,
            xp_bar: XpBar::Always,
            shortcut_numbers: ShortcutNumbers::On,
            buff_position: BuffPosition::Bar,
            bar_numbers: BarNumbers::Values,
            ui_scale: ScaleMode::RelativeToWindow([1920.0, 1080.0].into()),
            map_zoom: 10.0,
            map_drag: Vec2 { x: 0.0, y: 0.0 },
            map_show_topo_map: false,
            map_show_difficulty: true,
            map_show_towns: true,
            map_show_dungeons: true,
            map_show_castles: true,
            loading_tips: true,
            map_show_caves: true,
            map_show_trees: true,
            minimap_show: true,
            minimap_face_north: false,
        }
    }
}

/// `GameplaySettings` contains sensitivity and gameplay options.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GameplaySettings {
    pub pan_sensitivity: u32,
    pub zoom_sensitivity: u32,
    pub camera_clamp_angle: u32,
    pub zoom_inversion: bool,
    pub mouse_y_inversion: bool,
    pub smooth_pan_enable: bool,
    pub free_look_behavior: PressBehavior,
    pub auto_walk_behavior: PressBehavior,
    pub camera_clamp_behavior: PressBehavior,
    pub stop_auto_walk_on_input: bool,
    pub auto_camera: bool,
}

impl Default for GameplaySettings {
    fn default() -> Self {
        Self {
            pan_sensitivity: 100,
            zoom_sensitivity: 100,
            camera_clamp_angle: 45,
            zoom_inversion: false,
            mouse_y_inversion: false,
            smooth_pan_enable: false,
            free_look_behavior: PressBehavior::Toggle,
            auto_walk_behavior: PressBehavior::Toggle,
            camera_clamp_behavior: PressBehavior::Toggle,
            stop_auto_walk_on_input: true,
            auto_camera: false,
        }
    }
}

/// `NetworkingSettings` stores server and networking settings.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct NetworkingSettings {
    pub username: String,
    pub servers: Vec<String>,
    pub default_server: String,
    pub trusted_auth_servers: HashSet<String>,
}

impl Default for NetworkingSettings {
    fn default() -> Self {
        Self {
            username: "".to_string(),
            servers: vec!["server.veloren.net".to_string()],
            default_server: "server.veloren.net".to_string(),
            trusted_auth_servers: ["https://auth.veloren.net"]
                .iter()
                .map(|s| s.to_string())
                .collect(),
        }
    }
}

/// `Log` stores whether we should create a log file
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Log {
    // Whether to create a log file or not.
    // Default is to create one.
    pub log_to_file: bool,
    // The path on which the logs will be stored
    pub logs_path: PathBuf,
}

impl Default for Log {
    fn default() -> Self {
        // Chooses a path to store the logs by the following order:
        //  - The VOXYGEN_LOGS environment variable
        //  - The ProjectsDirs data local directory
        // This function is only called if there isn't already an entry in the settings
        // file. However, the VOXYGEN_LOGS environment variable always overrides
        // the log file path if set.
        let logs_path = std::env::var_os("VOXYGEN_LOGS")
            .map(PathBuf::from)
            .unwrap_or_else(|| {
                let mut path = voxygen_data_dir();
                path.push("logs");
                path
            });

        Self {
            log_to_file: true,
            logs_path,
        }
    }
}

#[derive(Copy, Clone, Serialize, Deserialize, Debug, PartialEq)]
pub enum Fps {
    Max(u32),
    Unlimited,
}

pub fn get_fps(max_fps: Fps) -> u32 {
    match max_fps {
        Fps::Max(x) => x,
        Fps::Unlimited => u32::MAX,
    }
}

impl fmt::Display for Fps {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match *self {
            Fps::Max(x) => write!(f, "{}", x),
            Fps::Unlimited => write!(f, "Unlimited"),
        }
    }
}

/// `GraphicsSettings` contains settings related to framerate and in-game
/// visuals.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct GraphicsSettings {
    pub view_distance: u32,
    pub sprite_render_distance: u32,
    pub particles_enabled: bool,
    pub figure_lod_render_distance: u32,
    pub max_fps: Fps,
    pub fov: u16,
    pub gamma: f32,
    pub exposure: f32,
    pub ambiance: f32,
    pub render_mode: RenderMode,
    pub window_size: [u16; 2],
    pub fullscreen: FullScreenSettings,
    pub lod_detail: u32,
}

impl Default for GraphicsSettings {
    fn default() -> Self {
        Self {
            view_distance: 10,
            sprite_render_distance: 100,
            particles_enabled: true,
            figure_lod_render_distance: 300,
            max_fps: Fps::Max(60),
            fov: 70,
            gamma: 1.0,
            exposure: 1.0,
            ambiance: 10.0,
            render_mode: RenderMode::default(),
            window_size: [1280, 720],
            fullscreen: FullScreenSettings::default(),
            lod_detail: 250,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
pub enum AudioOutput {
    /// Veloren's audio system wont work on some systems,
    /// so you can use this to disable it, and allow the
    /// game to function
    // If this option is disabled, functions in the rodio
    // library MUST NOT be called.
    Off,
    #[serde(other)]
    Automatic,
}

impl AudioOutput {
    pub fn is_enabled(&self) -> bool { !matches!(self, Self::Off) }
}
/// `AudioSettings` controls the volume of different audio subsystems and which
/// device is used.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct AudioSettings {
    pub master_volume: f32,
    pub music_volume: f32,
    pub sfx_volume: f32,
    pub max_sfx_channels: usize,

    /// Audio Device that Voxygen will use to play audio.
    pub output: AudioOutput,
}

impl Default for AudioSettings {
    fn default() -> Self {
        Self {
            master_volume: 1.0,
            music_volume: 0.4,
            sfx_volume: 0.6,
            max_sfx_channels: 30,
            output: AudioOutput::Automatic,
        }
    }
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct LanguageSettings {
    pub selected_language: String,
}

impl Default for LanguageSettings {
    fn default() -> Self {
        Self {
            selected_language: i18n::REFERENCE_LANG.to_string(),
        }
    }
}

/// `Settings` contains everything that can be configured in the settings.ron
/// file.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Settings {
    pub controls: ControlSettings,
    pub interface: InterfaceSettings,
    pub gameplay: GameplaySettings,
    pub networking: NetworkingSettings,
    pub log: Log,
    pub graphics: GraphicsSettings,
    pub audio: AudioSettings,
    pub show_disclaimer: bool,
    pub send_logon_commands: bool,
    // TODO: Remove at a later date, for dev testing
    pub logon_commands: Vec<String>,
    pub language: LanguageSettings,
    pub screenshots_path: PathBuf,
    pub controller: GamepadSettings,
}

impl Default for Settings {
    fn default() -> Self {
        let user_dirs = UserDirs::new().expect("System's $HOME directory path not found!");

        // Chooses a path to store the screenshots by the following order:
        //  - The VOXYGEN_SCREENSHOT environment variable
        //  - The user's picture directory
        //  - The executable's directory
        // This only selects if there isn't already an entry in the settings file
        let screenshots_path = std::env::var_os("VOXYGEN_SCREENSHOT")
            .map(PathBuf::from)
            .or_else(|| user_dirs.picture_dir().map(|dir| dir.join("veloren")))
            .or_else(|| {
                std::env::current_exe()
                    .ok()
                    .and_then(|dir| dir.parent().map(PathBuf::from))
            })
            .expect("Couldn't choose a place to store the screenshots");

        Settings {
            controls: ControlSettings::default(),
            interface: InterfaceSettings::default(),
            gameplay: GameplaySettings::default(),
            networking: NetworkingSettings::default(),
            log: Log::default(),
            graphics: GraphicsSettings::default(),
            audio: AudioSettings::default(),
            show_disclaimer: true,
            send_logon_commands: false,
            logon_commands: Vec::new(),
            language: LanguageSettings::default(),
            screenshots_path,
            controller: GamepadSettings::default(),
        }
    }
}

impl Settings {
    pub fn load() -> Self {
        let path = Self::get_settings_path();

        if let Ok(file) = fs::File::open(&path) {
            match ron::de::from_reader::<_, Self>(file) {
                Ok(mut s) => {
                    // Override the logs path if it is explicitly set using the VOXYGEN_LOGS
                    // environment variable. This is needed to support package managers that enforce
                    // strict application confinement (e.g. snap). In fact, the veloren snap package
                    // relies on this environment variable to be respected in
                    // order to communicate a path where the snap package is
                    // allowed to write to.
                    if let Some(logs_path_override) =
                        std::env::var_os("VOXYGEN_LOGS").map(PathBuf::from)
                    {
                        s.log.logs_path = logs_path_override;
                    }
                    return s;
                },
                Err(e) => {
                    warn!(?e, "Failed to parse setting file! Fallback to default.");
                    // Rename the corrupted settings file
                    let mut new_path = path.to_owned();
                    new_path.pop();
                    new_path.push("settings.invalid.ron");
                    if let Err(e) = std::fs::rename(&path, &new_path) {
                        warn!(?e, ?path, ?new_path, "Failed to rename settings file.");
                    }
                },
            }
        }
        // This is reached if either:
        // - The file can't be opened (presumably it doesn't exist)
        // - Or there was an error parsing the file
        let default_settings = Self::default();
        default_settings.save_to_file_warn();
        default_settings
    }

    pub fn save_to_file_warn(&self) {
        if let Err(e) = self.save_to_file() {
            warn!(?e, "Failed to save settings");
        }
    }

    pub fn save_to_file(&self) -> std::io::Result<()> {
        let path = Self::get_settings_path();
        if let Some(dir) = path.parent() {
            fs::create_dir_all(dir)?;
        }

        let ron = ron::ser::to_string_pretty(self, ron::ser::PrettyConfig::default()).unwrap();
        fs::write(path, ron.as_bytes())
    }

    pub fn get_settings_path() -> PathBuf {
        if let Some(path) = std::env::var_os("VOXYGEN_CONFIG") {
            let settings = PathBuf::from(&path).join("settings.ron");
            if settings.exists() || settings.parent().map(|x| x.exists()).unwrap_or(false) {
                return settings;
            }
            warn!(?path, "VOXYGEN_CONFIG points to invalid path.");
        }

        let mut path = voxygen_data_dir();
        path.push("settings.ron");
        path
    }
}

pub fn voxygen_data_dir() -> PathBuf {
    // Note: since voxygen is technically a lib we made need to lift this up to
    // run.rs
    let mut path = common_base::userdata_dir_workspace!();
    path.push("voxygen");
    path
}
