use std::collections::HashMap;

use bevy::prelude::*;

use crate::{
    bevy_synthizer::{Listener, Sound},
    commands::RunIfExistsExt,
    sound::SoundIcon,
};

#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component)]
struct Behind;

fn tag_behind(
    mut commands: Commands,
    config: Res<PitchShiftPlugin>,
    sounds: Query<(Entity, &GlobalTransform, Option<&Sound>, Option<&SoundIcon>)>,
    listener: Query<&GlobalTransform, With<Listener>>,
    behind: Query<Entity, With<Behind>>,
) {
    if config.downshift_behind {
        if let Ok(listener_transform) = listener.get_single() {
            let listener_forward = listener_transform.forward();
            for (entity, transform, sound, icon) in sounds.iter() {
                if icon.is_none() && sound.is_none() {
                    continue;
                }
                let v = transform.translation() - listener_transform.translation();
                let dot = v.dot(listener_forward);
                let is_behind = dot <= 0.;
                if is_behind {
                    commands.run_if_exists(entity, |mut entity| {
                        entity.insert(Behind);
                    });
                } else if !is_behind {
                    commands.run_if_exists(entity, |mut entity| {
                        entity.remove::<Behind>();
                    });
                }
            }
        }
    } else {
        for entity in behind.iter() {
            commands.run_if_exists(entity, |mut entity| {
                entity.remove::<Behind>();
            });
        }
    }
}

#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
struct LastIconPitch(HashMap<Entity, f64>);

#[derive(Resource, Clone, Debug, Default, Deref, DerefMut)]
struct LastSoundPitch(HashMap<Entity, f64>);

fn behind_added(
    config: Res<PitchShiftPlugin>,
    mut last_icon_pitch: ResMut<LastIconPitch>,
    mut last_sound_pitch: ResMut<LastSoundPitch>,
    mut query: Query<(Entity, Option<&mut SoundIcon>, Option<&mut Sound>), Added<Behind>>,
) {
    for (entity, icon, sound) in query.iter_mut() {
        if let Some(mut icon) = icon {
            icon.pitch *= config.downshift;
            last_icon_pitch.insert(entity, icon.pitch);
        } else if let Some(mut sound) = sound {
            sound.pitch *= config.downshift;
            last_sound_pitch.insert(entity, sound.pitch);
        }
    }
}

fn behind_removed(
    config: Res<PitchShiftPlugin>,
    mut last_icon_pitch: ResMut<LastIconPitch>,
    mut last_sound_pitch: ResMut<LastSoundPitch>,
    removed: RemovedComponents<Behind>,
    mut icons: Query<&mut SoundIcon>,
    mut sounds: Query<&mut Sound>,
) {
    let downshift = 1. / config.downshift;
    for entity in removed.iter() {
        if let Ok(mut icon) = icons.get_mut(entity) {
            icon.pitch *= downshift;
            last_icon_pitch.remove(&entity);
        } else if let Ok(mut sound) = sounds.get_mut(entity) {
            sound.pitch *= downshift;
            last_sound_pitch.remove(&entity);
        }
    }
}

fn sound_icon_changed(
    config: Res<PitchShiftPlugin>,
    mut last_icon_pitch: ResMut<LastIconPitch>,
    mut icons: Query<(Entity, &mut SoundIcon), (With<Behind>, Changed<SoundIcon>)>,
) {
    for (entity, mut icon) in icons.iter_mut() {
        let should_change = if let Some(pitch) = last_icon_pitch.get(&entity) {
            *pitch != icon.pitch
        } else {
            false
        };
        if should_change {
            icon.pitch *= config.downshift;
            last_icon_pitch.insert(entity, icon.pitch);
        }
    }
}

fn sound_changed(
    config: Res<PitchShiftPlugin>,
    mut last_sound_pitch: ResMut<LastSoundPitch>,
    mut sounds: Query<(Entity, &mut Sound), (With<Behind>, Without<SoundIcon>, Changed<Sound>)>,
) {
    for (entity, mut sound) in sounds.iter_mut() {
        let should_change = if let Some(pitch) = last_sound_pitch.get(&entity) {
            *pitch != sound.pitch
        } else {
            false
        };
        if should_change {
            sound.pitch *= config.downshift;
            last_sound_pitch.insert(entity, sound.pitch);
        }
    }
}

fn sync_config(
    mut commands: Commands,
    config: Res<PitchShiftPlugin>,
    behind: Query<Entity, With<Behind>>,
) {
    if config.is_changed() {
        for entity in behind.iter() {
            commands.entity(entity).remove::<Behind>();
        }
    }
}

#[derive(Resource, Clone, Copy, Debug)]
pub struct PitchShiftPlugin {
    pub downshift_behind: bool,
    pub downshift: f64,
}

impl Default for PitchShiftPlugin {
    fn default() -> Self {
        Self {
            downshift_behind: false,
            downshift: 0.95,
        }
    }
}

impl Plugin for PitchShiftPlugin {
    fn build(&self, app: &mut App) {
        app.insert_resource(*self)
            .register_type::<Behind>()
            .init_resource::<LastIconPitch>()
            .init_resource::<LastSoundPitch>()
            .add_system_to_stage(CoreStage::PreUpdate, tag_behind)
            .add_system(behind_added)
            .add_system_to_stage(CoreStage::PostUpdate, behind_removed)
            .add_system(sound_icon_changed)
            .add_system(sound_changed)
            .add_system(sync_config);
    }
}