//! Manages individual sfx event system, listens for sfx events, and requests
//! playback at the requested position and volume
//!
//! Veloren's sfx are managed through a configuration which lives in the
//! codebase under `/assets/voxygen/audio/sfx.ron`.
//!
//! If there are errors while reading or deserialising the configuration file, a
//! warning is logged and sfx will be disabled.
//!
//! Each entry in the configuration consists of an
//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) item, with some
//! additional information to allow playback:
//! - `files` - the paths to the `.wav` files to be played for the sfx. minus
//!   the file extension. This can be a single item if the same sound can be
//!   played each time, or a list of files from which one is chosen at random to
//!   be played.
//! - `threshold` - the time that the system should wait between successive
//!   plays. This avoids playing the sound with very fast successive repetition
//!   when the character can maintain a state over a long period, such as
//!   running or climbing.
//!
//! The following snippet details some entries in the configuration and how they
//! map to the sound files:
//! ```ignore
//! Run(Grass): ( // depends on underfoot block
//!    files: [
//!        "voxygen.audio.sfx.footsteps.stepgrass_1",
//!        "voxygen.audio.sfx.footsteps.stepgrass_2",
//!        "voxygen.audio.sfx.footsteps.stepgrass_3",
//!        "voxygen.audio.sfx.footsteps.stepgrass_4",
//!        "voxygen.audio.sfx.footsteps.stepgrass_5",
//!        "voxygen.audio.sfx.footsteps.stepgrass_6",
//!    ],
//!    threshold: 1.6, // travelled distance before next play
//! ),
//! Wield(Sword): ( // depends on the player's weapon
//!    files: [
//!        "voxygen.audio.sfx.weapon.sword_out",
//!    ],
//!    threshold: 0.5, // wait 0.5s between plays
//! ),
//! ...
//! ```
//!
//! These items (for example, the `Wield(Sword)` occasionally depend on some
//! property which varies in game. The
//! [SfxEvent](../../../veloren_common/event/enum.SfxEvent.html) documentation
//! provides links to those variables, some examples are provided her for longer
//! items:
//!
//! ```ignore
//! // An inventory action
//! Inventory(Dropped): (
//!     files: [
//!        "voxygen.audio.sfx.footsteps.stepgrass_4",
//!    ],
//!    threshold: 0.5,
//! ),
//! // An inventory action which depends upon the item
//! Inventory(Consumed(Apple)): (
//!    files: [
//!        "voxygen.audio.sfx.inventory.consumable.apple",
//!    ],
//!    threshold: 0.5
//! ),
//! // An attack ability which depends on the weapon
//! Attack(DashMelee, Sword): (
//!     files: [
//!         "voxygen.audio.sfx.weapon.sword_dash_01",
//!         "voxygen.audio.sfx.weapon.sword_dash_02",
//!     ],
//!     threshold: 1.2,
//! ),
//! // A multi-stage attack ability which depends on the weapon
//! Attack(ComboMelee(Swing, 1), Sword): (
//!     files: [
//!         "voxygen.audio.sfx.abilities.swing_sword",
//!     ],
//!     threshold: 0.5,
//! ),
//! ```

mod event_mapper;

use crate::{
    audio::AudioFrontend,
    scene::{Camera, Terrain},
};

use client::Client;
use common::{
    assets::{self, AssetExt, AssetHandle},
    comp::{
        beam, biped_large, biped_small, humanoid,
        item::{ItemKind, ToolKind},
        object,
        poise::PoiseState,
        quadruped_low, quadruped_medium, quadruped_small, Body, CharacterAbilityType,
        InventoryUpdateEvent, UtteranceKind,
    },
    outcome::Outcome,
    terrain::{BlockKind, TerrainChunk},
};
use common_state::State;
use event_mapper::SfxEventMapper;
use hashbrown::HashMap;
use rand::prelude::*;
use serde::Deserialize;
use tracing::{debug, warn};
use vek::*;

/// We watch the states of nearby entities in order to emit SFX at their
/// position based on their state. This constant limits the radius that we
/// observe to prevent tracking distant entities. It approximates the distance
/// at which the volume of the sfx emitted is too quiet to be meaningful for the
/// player.
const SFX_DIST_LIMIT_SQR: f32 = 20000.0;

pub struct SfxEventItem {
    /// The SFX event that triggers this sound
    pub sfx: SfxEvent,
    /// The position at which the sound should play
    pub pos: Option<Vec3<f32>>,
    /// The volume to play the sound at
    pub vol: Option<f32>,
}

impl SfxEventItem {
    pub fn new(sfx: SfxEvent, pos: Option<Vec3<f32>>, vol: Option<f32>) -> Self {
        Self { sfx, pos, vol }
    }

    pub fn at_player_position(sfx: SfxEvent) -> Self {
        Self {
            sfx,
            pos: None,
            vol: None,
        }
    }
}

#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
pub enum SfxEvent {
    Campfire,
    Embers,
    Birdcall,
    Owl,
    Cricket1,
    Cricket2,
    Cricket3,
    Frog,
    Bees,
    RunningWaterSlow,
    RunningWaterFast,
    Idle,
    Swim,
    Run(BlockKind),
    QuadRun(BlockKind),
    Roll,
    Sneak,
    Climb,
    GliderOpen,
    Glide,
    GliderClose,
    CatchAir,
    Jump,
    Fall,
    ExperienceGained,
    LevelUp,
    Attack(CharacterAbilityType, ToolKind),
    Wield(ToolKind),
    Unwield(ToolKind),
    Inventory(SfxInventoryEvent),
    Explosion,
    Damage,
    Death,
    Parry,
    Block,
    BreakBlock,
    SceptreBeam,
    SkillPointGain,
    ArrowHit,
    ArrowMiss,
    ArrowShot,
    FireShot,
    FlameThrower,
    PoiseChange(PoiseState),
    GroundSlam,
    Utterance(UtteranceKind, VoiceKind),
}

#[derive(Copy, Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
pub enum VoiceKind {
    HumanFemale,
    HumanMale,
    BipedLarge,
    Wendigo,
    Reptile,
    Bird,
    Critter,
    Sheep,
    Pig,
    Cow,
    Canine,
    Lion,
    Mindflayer,
    Marlin,
    Maneater,
    Adlet,
    Antelope,
    Alligator,
    SeaCrocodile,
    Saurok,
    Cat,
    Goat,
    Mandragora,
    Asp,
    Fungome,
    Truffler,
}

fn body_to_voice(body: &Body) -> Option<VoiceKind> {
    Some(match body {
        Body::Humanoid(body) => match &body.body_type {
            humanoid::BodyType::Female => VoiceKind::HumanFemale,
            humanoid::BodyType::Male => VoiceKind::HumanMale,
        },
        Body::QuadrupedLow(body) => match body.species {
            quadruped_low::Species::Maneater => VoiceKind::Maneater,
            quadruped_low::Species::Alligator => VoiceKind::Alligator,
            quadruped_low::Species::SeaCrocodile => VoiceKind::SeaCrocodile,
            quadruped_low::Species::Asp => VoiceKind::Asp,
            _ => return None,
        },
        Body::QuadrupedSmall(body) => match body.species {
            quadruped_small::Species::Truffler => VoiceKind::Truffler,
            quadruped_small::Species::Fungome => VoiceKind::Fungome,
            quadruped_small::Species::Sheep => VoiceKind::Sheep,
            quadruped_small::Species::Pig | quadruped_small::Species::Boar => VoiceKind::Pig,
            quadruped_small::Species::Cat => VoiceKind::Cat,
            quadruped_small::Species::Goat => VoiceKind::Goat,
            _ => VoiceKind::Critter,
        },
        Body::QuadrupedMedium(body) => match body.species {
            quadruped_medium::Species::Saber
            | quadruped_medium::Species::Tiger
            | quadruped_medium::Species::Lion
            | quadruped_medium::Species::Frostfang
            | quadruped_medium::Species::Snowleopard => VoiceKind::Lion,
            quadruped_medium::Species::Wolf
            | quadruped_medium::Species::Roshwalr
            | quadruped_medium::Species::Tarasque
            | quadruped_medium::Species::Darkhound
            | quadruped_medium::Species::Bonerattler
            | quadruped_medium::Species::Grolgar => VoiceKind::Canine,
            quadruped_medium::Species::Cattle
            | quadruped_medium::Species::Catoblepas
            | quadruped_medium::Species::Highland
            | quadruped_medium::Species::Yak
            | quadruped_medium::Species::Moose
            | quadruped_medium::Species::Dreadhorn => VoiceKind::Cow,
            quadruped_medium::Species::Antelope => VoiceKind::Antelope,
            _ => return None,
        },
        Body::BirdMedium(_) | Body::BirdLarge(_) => VoiceKind::Bird,
        Body::BipedSmall(body) => match body.species {
            biped_small::Species::Adlet => VoiceKind::Adlet,
            biped_small::Species::Mandragora => VoiceKind::Mandragora,
            _ => return None,
        },
        Body::BipedLarge(body) => match body.species {
            biped_large::Species::Wendigo => VoiceKind::Wendigo,
            biped_large::Species::Occultsaurok
            | biped_large::Species::Mightysaurok
            | biped_large::Species::Slysaurok => VoiceKind::Saurok,
            biped_large::Species::Mindflayer => VoiceKind::Mindflayer,
            _ => VoiceKind::BipedLarge,
        },
        Body::Theropod(_) | Body::Dragon(_) => VoiceKind::Reptile,
        Body::FishSmall(_) | Body::FishMedium(_) => VoiceKind::Marlin,
        _ => return None,
    })
}

#[derive(Clone, Debug, PartialEq, Deserialize, Hash, Eq)]
pub enum SfxInventoryEvent {
    Collected,
    CollectedTool(ToolKind),
    CollectedItem(String),
    CollectFailed,
    Consumed(String),
    Debug,
    Dropped,
    Given,
    Swapped,
}

// TODO Move to a separate event mapper?
impl From<&InventoryUpdateEvent> for SfxEvent {
    fn from(value: &InventoryUpdateEvent) -> Self {
        match value {
            InventoryUpdateEvent::Collected(item) => {
                // Handle sound effects for types of collected items, falling
                // back to the default Collected event
                match &item.kind() {
                    ItemKind::Tool(tool) => {
                        SfxEvent::Inventory(SfxInventoryEvent::CollectedTool(tool.kind))
                    },
                    ItemKind::Ingredient { kind } => match &kind[..] {
                        "Diamond" | "Ruby" | "Emerald" | "Sapphire" | "Topaz" | "Amethyst" => {
                            SfxEvent::Inventory(SfxInventoryEvent::CollectedItem(kind.clone()))
                        },
                        _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
                    },
                    _ => SfxEvent::Inventory(SfxInventoryEvent::Collected),
                }
            },
            InventoryUpdateEvent::BlockCollectFailed(_)
            | InventoryUpdateEvent::EntityCollectFailed(_) => {
                SfxEvent::Inventory(SfxInventoryEvent::CollectFailed)
            },
            InventoryUpdateEvent::Consumed(consumable) => {
                SfxEvent::Inventory(SfxInventoryEvent::Consumed(consumable.clone()))
            },
            InventoryUpdateEvent::Debug => SfxEvent::Inventory(SfxInventoryEvent::Debug),
            InventoryUpdateEvent::Dropped => SfxEvent::Inventory(SfxInventoryEvent::Dropped),
            InventoryUpdateEvent::Given => SfxEvent::Inventory(SfxInventoryEvent::Given),
            InventoryUpdateEvent::Swapped => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
            _ => SfxEvent::Inventory(SfxInventoryEvent::Swapped),
        }
    }
}

#[derive(Deserialize)]
pub struct SfxTriggerItem {
    /// A list of SFX filepaths for this event
    pub files: Vec<String>,
    /// The time to wait before repeating this SfxEvent
    pub threshold: f32,
}

#[derive(Deserialize, Default)]
pub struct SfxTriggers(HashMap<SfxEvent, SfxTriggerItem>);

impl SfxTriggers {
    pub fn get_trigger(&self, trigger: &SfxEvent) -> Option<&SfxTriggerItem> { self.0.get(trigger) }

    pub fn get_key_value(&self, trigger: &SfxEvent) -> Option<(&SfxEvent, &SfxTriggerItem)> {
        self.0.get_key_value(trigger)
    }
}

pub struct SfxMgr {
    /// This is an `AssetHandle` so it is reloaded automatically
    /// when the manifest is edited.
    pub triggers: AssetHandle<SfxTriggers>,
    event_mapper: SfxEventMapper,
}

impl Default for SfxMgr {
    fn default() -> Self {
        Self {
            triggers: Self::load_sfx_items(),
            event_mapper: SfxEventMapper::new(),
        }
    }
}

impl SfxMgr {
    pub fn maintain(
        &mut self,
        audio: &mut AudioFrontend,
        state: &State,
        player_entity: specs::Entity,
        camera: &Camera,
        terrain: &Terrain<TerrainChunk>,
        client: &Client,
    ) {
        // Checks if the SFX volume is set to zero or audio is disabled
        // This prevents us from running all the following code unnecessarily
        if !audio.sfx_enabled() {
            return;
        }

        let focus_off = camera.get_focus_pos().map(f32::trunc);
        let cam_pos = camera.dependents().cam_pos + focus_off;

        // Sets the listener position to the camera position facing the
        // same direction as the camera
        audio.set_listener_pos(cam_pos, camera.dependents().cam_dir);

        let triggers = self.triggers.read();

        self.event_mapper.maintain(
            audio,
            state,
            player_entity,
            camera,
            &triggers,
            terrain,
            client,
        );
    }

    #[allow(clippy::single_match)]
    pub fn handle_outcome(
        &mut self,
        outcome: &Outcome,
        audio: &mut AudioFrontend,
        client: &Client,
    ) {
        if !audio.sfx_enabled() {
            return;
        }
        let triggers = self.triggers.read();

        // TODO handle underwater
        match outcome {
            Outcome::Explosion { pos, power, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Explosion);
                audio.emit_sfx(
                    sfx_trigger_item,
                    *pos,
                    Some((power.abs() / 2.5).min(1.5)),
                    false,
                );
            },
            Outcome::GroundSlam { pos, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GroundSlam);
                audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false);
            },
            Outcome::ProjectileShot { pos, body, .. } => {
                match body {
                    Body::Object(
                        object::Body::Arrow
                        | object::Body::MultiArrow
                        | object::Body::ArrowSnake
                        | object::Body::ArrowTurret,
                    ) => {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowShot);
                        audio.emit_sfx(sfx_trigger_item, *pos, None, false);
                    },
                    Body::Object(
                        object::Body::BoltFire
                        | object::Body::BoltFireBig
                        | object::Body::BoltNature,
                    ) => {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FireShot);
                        audio.emit_sfx(sfx_trigger_item, *pos, None, false);
                    },
                    _ => {
                        // not mapped to sfx file
                    },
                }
            },
            Outcome::ProjectileHit {
                pos,
                body,
                source,
                target,
                ..
            } => match body {
                Body::Object(
                    object::Body::Arrow
                    | object::Body::MultiArrow
                    | object::Body::ArrowSnake
                    | object::Body::ArrowTurret,
                ) => {
                    if target.is_none() {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowMiss);
                        audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false);
                    } else if *source == client.uid() {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
                        audio.emit_sfx(
                            sfx_trigger_item,
                            client.position().unwrap_or(*pos),
                            Some(2.0),
                            false,
                        );
                    } else {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::ArrowHit);
                        audio.emit_sfx(sfx_trigger_item, *pos, Some(2.0), false);
                    }
                },
                _ => {},
            },
            Outcome::SkillPointGain { pos, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SkillPointGain);
                audio.emit_sfx(sfx_trigger_item, *pos, None, false);
            },
            Outcome::Beam { pos, specifier } => match specifier {
                beam::FrontendSpecifier::LifestealBeam => {
                    if thread_rng().gen_bool(0.5) {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::SceptreBeam);
                        audio.emit_sfx(sfx_trigger_item, *pos, None, false);
                    };
                },
                beam::FrontendSpecifier::Flamethrower | beam::FrontendSpecifier::Cultist => {
                    if thread_rng().gen_bool(0.5) {
                        let sfx_trigger_item = triggers.get_key_value(&SfxEvent::FlameThrower);
                        audio.emit_sfx(sfx_trigger_item, *pos, None, false);
                    }
                },
                beam::FrontendSpecifier::ClayGolem
                | beam::FrontendSpecifier::Bubbles
                | beam::FrontendSpecifier::Frost
                | beam::FrontendSpecifier::WebStrand => {},
            },
            Outcome::BreakBlock { pos, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::BreakBlock);
                audio.emit_sfx(
                    sfx_trigger_item,
                    pos.map(|e| e as f32 + 0.5),
                    Some(3.0),
                    false,
                );
            },
            Outcome::Damage { pos, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Damage);
                audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
            },
            Outcome::Death { pos, .. } => {
                let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Death);
                audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
            },
            Outcome::Block { pos, parry, .. } => {
                if *parry {
                    let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Parry);
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                } else {
                    let sfx_trigger_item = triggers.get_key_value(&SfxEvent::Block);
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                }
            },
            Outcome::PoiseChange { pos, state, .. } => match state {
                PoiseState::Normal => {},
                PoiseState::Interrupted => {
                    let sfx_trigger_item =
                        triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Interrupted));
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                },
                PoiseState::Stunned => {
                    let sfx_trigger_item =
                        triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Stunned));
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                },
                PoiseState::Dazed => {
                    let sfx_trigger_item =
                        triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::Dazed));
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                },
                PoiseState::KnockedDown => {
                    let sfx_trigger_item =
                        triggers.get_key_value(&SfxEvent::PoiseChange(PoiseState::KnockedDown));
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.5), false);
                },
            },
            Outcome::Utterance { pos, kind, body } => {
                if let Some(voice) = body_to_voice(body) {
                    let sfx_trigger_item =
                        triggers.get_key_value(&SfxEvent::Utterance(*kind, voice));
                    if let Some(sfx_trigger_item) = sfx_trigger_item {
                        audio.emit_sfx(Some(sfx_trigger_item), *pos, Some(1.5), false);
                    } else {
                        debug!(
                            "No utterance sound effect exists for ({:?}, {:?})",
                            kind, voice
                        );
                    }
                }
            },
            Outcome::Glider { pos, wielded } => {
                if *wielded {
                    let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderOpen);
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), false);
                } else {
                    let sfx_trigger_item = triggers.get_key_value(&SfxEvent::GliderClose);
                    audio.emit_sfx(sfx_trigger_item, *pos, Some(1.0), false);
                }
            },
            Outcome::ExpChange { .. }
            | Outcome::ComboChange { .. }
            | Outcome::SummonedCreature { .. } => {},
        }
    }

    fn load_sfx_items() -> AssetHandle<SfxTriggers> {
        SfxTriggers::load_or_insert_with("voxygen.audio.sfx", |error| {
            warn!(
                "Error reading sfx config file, sfx will not be available: {:#?}",
                error
            );

            SfxTriggers::default()
        })
    }
}

impl assets::Asset for SfxTriggers {
    type Loader = assets::RonLoader;

    const EXTENSION: &'static str = "ron";
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_load_sfx_triggers() { let _ = SfxTriggers::load_expect("voxygen.audio.sfx"); }
}
