use bevy::{
    diagnostic::{
        DiagnosticsStore, EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin,
        SystemInformationDiagnosticsPlugin,
    },
    input::{keyboard::KeyboardInput, ButtonState},
    utils::{hashbrown::hash_map::Iter, HashMap},
};

use crate::prelude::*;

pub(crate) struct DebugPlugin;

impl Plugin for DebugPlugin {
    fn build(&self, app: &mut App) {
        app.add_plugins((
            FrameTimeDiagnosticsPlugin,
            EntityCountDiagnosticsPlugin::default(),
            SystemInformationDiagnosticsPlugin::default(),
        ))
        .init_resource::<DebugInfo>()
        .insert_resource(GizmoConfig {
            depth_bias: -0.1,
            ..default()
        })
        .add_systems(Update, (aabb_gizmo,))
        // Systems that run in the editor mode
        .add_systems(
            Update,
            (
                selected_gizmo.run_if(any_with_component::<game::Selected>()),
                selected_position.run_if(any_with_component::<game::Selected>()),
            )
                .run_if(resource_exists::<DebugEnabled>()),
        )
        .add_systems(Startup, init_debug_ui)
        .add_systems(
            Update,
            (
                toggle_debug_mode.run_if(on_event::<KeyboardInput>()),
                display_diagnostics.run_if(resource_exists::<DebugEnabled>()),
                toggle_debug_ui.run_if(resource_changed_or_removed::<DebugEnabled>()),
                camera_info.run_if(resource_exists::<DebugEnabled>()),
            ),
        );
    }
}

#[derive(Debug, Default, Resource)]
pub(crate) struct DebugInfo(HashMap<String, String>);

impl DebugInfo {
    pub fn set(&mut self, key: String, val: String) -> Option<String> {
        self.0.insert(key, val)
    }

    pub fn _get(&self, key: &String) -> Option<&String> {
        self.0.get(key)
    }

    pub fn _clear(&mut self, key: String) {
        self.0.remove(&key);
    }

    pub fn iter(&self) -> Iter<'_, String, String> {
        self.0.iter()
    }
}

/// Marker resource used to enable Debug mode when present
#[derive(Debug, Resource, Default)]
pub(crate) struct DebugEnabled;

#[derive(Debug, Component)]
struct DebugRoot;

fn toggle_debug_mode(
    mut events: EventReader<KeyboardInput>,
    enabled: Option<Res<DebugEnabled>>,
    mut commands: Commands,
) {
    events
        .read()
        .filter(
            |KeyboardInput {
                 state, key_code, ..
             }| *state == ButtonState::Pressed && *key_code == Some(KeyCode::F3),
        )
        .for_each(|_| match enabled {
            Some(_) => commands.remove_resource::<DebugEnabled>(),
            None => commands.insert_resource(DebugEnabled),
        });
}

fn toggle_debug_ui(
    mut visibility: Query<&mut Visibility, With<DebugRoot>>,
    enabled: Option<Res<DebugEnabled>>,
) {
    visibility.iter_mut().for_each(|mut vis| {
        *vis = match enabled {
            Some(_) => Visibility::Visible,
            None => Visibility::Hidden,
        }
    });
}

fn init_debug_ui(mut commands: Commands) {
    commands
        .spawn((
            NodeBundle {
                style: Style {
                    padding: UiRect::all(Val::Px(10.0)),
                    ..default()
                },
                background_color: Color::BLACK.into(),
                visibility: Visibility::Hidden,
                ..default()
            },
            DebugRoot,
        ))
        .with_children(|parent| {
            parent.spawn((
                TextBundle {
                    style: Style { ..default() },
                    ..default()
                },
                DebugRoot,
            ));
        });
}

fn display_diagnostics(
    mut root: Query<&mut Text, With<DebugRoot>>,
    diagnostics: Res<DiagnosticsStore>,
    debug_infos: Res<DebugInfo>,
) {
    root.iter_mut().for_each(|mut text| {
        text.sections = diagnostics
            .iter()
            .map(|d| format!("{}: {:.0}\n", d.name, d.smoothed().unwrap_or(0.0),))
            .chain(debug_infos.iter().map(|(k, v)| format!("{}: {}\n", k, v)))
            .map(|s| TextSection::new(s, TextStyle { ..default() }))
            .collect();
        text.sections.sort_unstable_by(|a, b| a.value.cmp(&b.value));
    });
}

fn camera_info(mut debug_infos: ResMut<DebugInfo>, cameras: Query<(&Camera, &Name)>) {
    let camera_names = cameras
        .iter()
        .filter(|(c, _)| c.is_active)
        .map(|(_, n)| n.as_str())
        .collect::<Vec<&str>>()
        .iter()
        .copied()
        .intersperse(", ")
        .collect::<String>();
    debug_infos.set("Cameras".into(), camera_names);
}

fn aabb_gizmo(
    added: Query<Entity, Added<game::Selected>>,
    mut removed: RemovedComponents<game::Selected>,
    selected: Query<Entity, With<game::Selected>>,
    active: Option<Res<DebugEnabled>>,
    mut commands: Commands,
) {
    added.iter().for_each(|e| {
        commands.entity(e).insert(AabbGizmo {
            color: Some(Color::RED),
        });
    });
    removed.read().for_each(|e| {
        commands.entity(e).remove::<AabbGizmo>();
    });
    match active {
        Some(_) => selected.iter().for_each(|e| {
            commands.entity(e).insert(AabbGizmo {
                color: Some(Color::RED),
            });
        }),
        None => selected.iter().for_each(|e| {
            commands.entity(e).remove::<AabbGizmo>();
        }),
    }
}

/// Draw a gizmo showing cardinal directions for a selected object
fn selected_gizmo(selected: Query<&GlobalTransform, With<game::Selected>>, mut gizmos: Gizmos) {
    selected.iter().for_each(|g| {
        let s = g.translation();
        gizmos.ray(s, Vec3::X, Color::RED);
        gizmos.ray(s, Vec3::Y, Color::GREEN);
        gizmos.ray(s, Vec3::Z, Color::BLUE);
    });
}

fn selected_position(
    selected: Query<(Entity, &GlobalTransform), With<game::Selected>>,
    mut debug_info: ResMut<debug::DebugInfo>,
) {
    let val = selected
        .iter()
        .map(|(e, gt)| format!("\n{:?} {:?}", e, gt.translation()))
        .collect::<Vec<String>>()
        .join("");
    debug_info.set("Position".into(), val);
}
