use std::{collections::HashMap, fmt::Debug, hash::Hash, time::Duration};

use bevy::{asset::HandleId, prelude::*, transform::TransformSystem};
use bevy_openal::{Buffer, Context, Sound, SoundState};

use rand::random;

use crate::{
    commands::RunIfExistsExt,
    core::{CoreConfig, Player, PointLike},
    exploration::ExplorationFocused,
    visibility::VisibleEntities,
};

#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component)]
pub struct Footstep {
    pub sound: HandleId,
    pub step_length: f32,
    pub gain: f32,
    pub reference_distance: f32,
    pub max_distance: f32,
    pub rolloff_factor: f32,
    pub pitch: f32,
    pub pitch_variation: Option<f32>,
}

impl Default for Footstep {
    fn default() -> Self {
        Self {
            sound: "".into(),
            step_length: 0.8,
            gain: 1.,
            reference_distance: 1.,
            max_distance: f32::MAX,
            rolloff_factor: 1.,
            pitch: 1.,
            pitch_variation: Some(0.15),
        }
    }
}

#[derive(Component, Clone, Debug)]
pub struct SoundIcon {
    pub sound: HandleId,
    pub gain: f32,
    pub pitch: f32,
    pub reference_distance: f32,
    pub max_distance: f32,
    pub rolloff_factor: f32,
    pub interval: Option<Timer>,
}

impl Default for SoundIcon {
    fn default() -> Self {
        let seconds = random::<f32>() + 4.5;
        let mut icon = Self {
            sound: "".into(),
            gain: 0.3,
            pitch: 1.,
            reference_distance: 1.,
            max_distance: f32::MAX,
            rolloff_factor: 1.,
            interval: Some(Timer::from_seconds(seconds, true)),
        };
        if let Some(ref mut interval) = icon.interval {
            let seconds = Duration::from_secs_f32(seconds - 0.1);
            interval.set_elapsed(seconds);
        }
        icon
    }
}

#[derive(Bundle, Default)]
pub struct FootstepBundle {
    pub footstep: Footstep,
    pub transform: Transform,
    pub global_transform: GlobalTransform,
}

#[derive(Bundle, Clone, Debug, Default)]
pub struct SoundIconBundle {
    pub sound_icon: SoundIcon,
    pub transform: Transform,
    pub global_transform: GlobalTransform,
}

fn add_footstep_sounds(
    mut commands: Commands,
    footsteps: Query<(Entity, &Footstep), Added<Footstep>>,
    assets: Res<Assets<Buffer>>,
) {
    for (entity, footstep) in footsteps.iter() {
        let buffer = assets.get_handle(footstep.sound);
        let gain = footstep.gain;
        commands.run_if_exists(entity, move |mut entity| {
            entity.insert(Sound {
                buffer,
                state: SoundState::Stopped,
                gain,
                ..default()
            });
        });
    }
}

fn footstep(
    mut last_step_distance: Local<HashMap<Entity, (f32, Transform)>>,
    mut footsteps: Query<(Entity, &Footstep, &Parent, &mut Sound), Changed<GlobalTransform>>,
    transforms_storage: Query<&Transform>,
) {
    for (entity, footstep, parent, mut sound) in footsteps.iter_mut() {
        let coordinates = transforms_storage.get(**parent).unwrap();
        if let Some(last) = last_step_distance.get(&entity) {
            let distance = last.0 + (last.1.distance(coordinates));
            if distance >= footstep.step_length {
                last_step_distance.insert(entity, (0., *coordinates));
                sound.gain = footstep.gain;
                sound.reference_distance = footstep.reference_distance;
                sound.max_distance = footstep.max_distance;
                sound.rolloff_factor = footstep.rolloff_factor;
                sound.pitch = footstep.pitch;
                if let Some(pitch_variation) = footstep.pitch_variation {
                    let mut pitch = footstep.pitch - pitch_variation / 2.;
                    pitch += random::<f32>() * pitch_variation;
                    sound.pitch = pitch;
                }
                sound.play();
            } else if last.1 != *coordinates {
                last_step_distance.insert(entity, (distance, *coordinates));
            }
        } else {
            last_step_distance.insert(entity, (0., *coordinates));
        }
    }
}

fn add_sound_icon_sounds(
    mut commands: Commands,
    icons: Query<(Entity, &SoundIcon), Added<SoundIcon>>,
    assets: Res<Assets<Buffer>>,
) {
    for (entity, icon) in icons.iter() {
        let buffer = assets.get_handle(icon.sound);
        let gain = icon.gain;
        let pitch = icon.pitch;
        let looping = icon.interval.is_none();
        let reference_distance = icon.reference_distance;
        let max_distance = icon.max_distance;
        let rolloff_factor = icon.rolloff_factor;
        commands.run_if_exists(entity, move |mut entity| {
            entity.insert(Sound {
                buffer,
                gain,
                pitch,
                looping,
                state: SoundState::Stopped,
                reference_distance,
                max_distance,
                rolloff_factor,
                ..default()
            });
        });
    }
}

fn sound_icon<S>(
    config: Res<SoundConfig<S>>,
    state: Res<State<S>>,
    time: Res<Time>,
    viewers: Query<&VisibleEntities, With<Player>>,
    mut icons: Query<(
        Entity,
        &mut SoundIcon,
        Option<&Transform>,
        Option<&Parent>,
        &mut Sound,
    )>,
    buffers: Res<Assets<Buffer>>,
) where
    S: 'static + Clone + Debug + Eq + Hash + Send + Sync,
{
    if !(*config).sound_icon_states.is_empty()
        && !config.sound_icon_states.contains(state.current())
    {
        return;
    }
    for visible in viewers.iter() {
        for (icon_entity, mut icon, coordinates, parent, mut sound) in icons.iter_mut() {
            let entity = if coordinates.is_some() {
                icon_entity
            } else {
                **parent.unwrap()
            };
            if visible.contains(&entity) {
                let looping = sound.looping;
                if looping {
                    sound.state = SoundState::Playing;
                } else if let Some(interval) = icon.interval.as_mut() {
                    interval.tick(time.delta());
                    if interval.finished() {
                        sound.state = SoundState::Playing;
                        interval.reset();
                    }
                }
                let buffer = buffers.get_handle(icon.sound);
                sound.looping = icon.interval.is_none();
                if sound.buffer != buffer {
                    sound.buffer = buffer;
                }
                sound.gain = icon.gain;
                sound.pitch = icon.pitch;
                sound.reference_distance = icon.reference_distance;
                sound.max_distance = icon.max_distance;
                sound.rolloff_factor = icon.rolloff_factor;
            } else {
                sound.state = SoundState::Stopped;
            }
        }
    }
}

fn sound_icon_exploration_focus_changed(
    mut focused: Query<(Entity, Option<&Children>), Changed<ExplorationFocused>>,
    mut icons: Query<&mut SoundIcon>,
) {
    const ICON_GAIN: f32 = 3.;
    for (entity, children) in focused.iter_mut() {
        if let Ok(mut icon) = icons.get_mut(entity) {
            icon.gain *= ICON_GAIN;
        }
        if let Some(children) = children {
            for child in children.iter() {
                if let Ok(mut icon) = icons.get_mut(*child) {
                    icon.gain *= ICON_GAIN;
                }
            }
        }
    }
}

fn sound_icon_exploration_focus_removed(
    removed: RemovedComponents<ExplorationFocused>,
    mut query: Query<&mut SoundIcon>,
    children: Query<&Children>,
) {
    const ICON_GAIN: f32 = 3.;
    for entity in removed.iter() {
        if let Ok(mut icon) = query.get_mut(entity) {
            icon.gain /= ICON_GAIN;
        }
        if let Ok(children) = children.get(entity) {
            for child in children.iter() {
                if let Ok(mut icon) = query.get_mut(*child) {
                    icon.gain *= ICON_GAIN;
                }
            }
        }
    }
}

fn scale_sounds(config: Res<CoreConfig>, mut sounds: Query<&mut Sound>) {
    let pixels_per_unit = config.pixels_per_unit as f32;
    for mut sound in sounds.iter_mut() {
        sound.reference_distance *= pixels_per_unit;
        if sound.max_distance != f32::MAX {
            sound.max_distance *= pixels_per_unit;
        }
    }
}

#[derive(Clone, Debug)]
pub struct SoundConfig<S> {
    pub sound_icon_states: Vec<S>,
}

impl<S> Default for SoundConfig<S> {
    fn default() -> Self {
        Self {
            sound_icon_states: vec![],
        }
    }
}

pub struct SoundPlugin<'a, S>(std::marker::PhantomData<&'a S>);

impl<'a, S> Default for SoundPlugin<'a, S> {
    fn default() -> Self {
        Self(std::marker::PhantomData)
    }
}

impl<'a, S> Plugin for SoundPlugin<'a, S>
where
    S: 'static + Clone + Debug + Eq + Hash + Send + Sync,
    'a: 'static,
{
    fn build(&self, app: &mut App) {
        if !app.world.contains_resource::<SoundConfig<S>>() {
            app.insert_resource(SoundConfig::<S>::default());
        }
        const SOUND_ICON_AND_EXPLORATION_STAGE: &str = "sound_icon_and_exploration";
        let core_config = *app.world.get_resource::<CoreConfig>().unwrap();
        if let Some(context) = app.world.get_resource::<Context>() {
            context
                .set_meters_per_unit(1. / core_config.pixels_per_unit as f32)
                .unwrap();
        }
        app.register_type::<Footstep>()
            .add_system(add_footstep_sounds)
            .add_system_to_stage(
                CoreStage::PostUpdate,
                footstep.after(TransformSystem::TransformPropagate),
            )
            .add_system(add_sound_icon_sounds)
            .add_system_to_stage(
                CoreStage::PostUpdate,
                sound_icon::<S>.after(TransformSystem::TransformPropagate),
            )
            .add_stage_after(
                CoreStage::PostUpdate,
                SOUND_ICON_AND_EXPLORATION_STAGE,
                SystemStage::parallel(),
            )
            .add_system_to_stage(
                SOUND_ICON_AND_EXPLORATION_STAGE,
                sound_icon_exploration_focus_changed,
            )
            .add_system_to_stage(
                SOUND_ICON_AND_EXPLORATION_STAGE,
                sound_icon_exploration_focus_removed,
            )
            .add_system(scale_sounds);
    }
}