// Monologue Trees Editor
//
// Editor for creating Monologue Trees levels
//
// TODO:
// * Tree Organization: GLTF contains Animations and Scenes
// * Camera can only select one at a time.
// * (hard) Better Colorscheme
// * (medium) Visual errors for bad GLTFs
// * (medium) Collapsable containers (Gltfs, Animations, Scenes, Audio Clips, Cameras)
// * (medium) Show/hide entire UI
// * (medium) Spawn clicked scene
// * (medium) Play clicked animation
// * (idea) Use enum instead of markers for exclusive UI
// * (medium) Add fonts similar to Audios based on inspect-fonts
// * (hard) Add Dialogs (requires text box UI, saving, loading)

use bevy::{
    asset::{AssetPath, Assets},
    gltf::Gltf,
    input::{keyboard::KeyboardInput, ButtonState},
    prelude::*,
    utils::HashSet,
};
use monologue_trees::{debug::*, ui::*};

fn main() {
    App::new()
        .add_plugins((
            DefaultPlugins.set(WindowPlugin {
                primary_window: Some(Window {
                    title: "Monologue Trees Editor".into(),
                    resolution: (640., 480.).into(),
                    ..default()
                }),
                ..default()
            }),
            DebugInfoPlugin,
            GameUiPlugin,
        ))
        .init_resource::<AssetRegistry>()
        .add_systems(Startup, (initialize_ui,))
        .add_systems(
            Update,
            (
                // GLTF Systems
                load_gltf,
                unload_gltf,
                manage_gltf_ui,
                // Scene Systems
                manage_scene_ui,
                spawn_scene,
                // Animation systems
                manage_animation_ui,
                // Camera systems
                manage_camera_ui,
                // Audio Systems
                load_audio,
                unload_audio,
                manage_audio_ui,
                play_audio,
                // Level Import/Export systems
                export_level,
                import_level,
                // Misc/Debug Systems
                load_bogus,
            ),
        )
        .run();
}

/// A generic referenece used for UI elements to point to assets or entities
#[derive(Debug, Component)]
enum UiRef<T> {
    Handle(T),
    // Entity(T),
    // Event(T),
}

/// UI:
/// * GLTFs
///   * Scenes
///   * Cameras
///   * Animations
/// * Audios
fn initialize_ui(mut commands: Commands) {
    commands.spawn((
        Camera2dBundle { ..default() },
        UiCameraConfig { show_ui: true },
    ));

    commands
        .spawn(NodeBundle {
            style: Style {
                width: Val::Percent(100.0),
                height: Val::Percent(100.0),
                ..default()
            },
            ..default()
        })
        .with_children(|parent| {
            parent
                .spawn((
                    GameUiList,
                    Name::new("GLTFs"),
                    NodeBundle {
                        style: Style {
                            flex_direction: FlexDirection::Column,
                            ..default()
                        },
                        ..default()
                    },
                    GltfsUi,
                ))
                .with_children(|parent| {
                    parent.spawn((
                        GameUiList,
                        Name::new("Scenes"),
                        NodeBundle { ..default() },
                        ScenesUi,
                    ));
                    parent.spawn((
                        GameUiList,
                        Name::new("Cameras"),
                        NodeBundle { ..default() },
                        CamerasUi,
                    ));
                    parent.spawn((
                        GameUiList,
                        Name::new("Animations"),
                        NodeBundle { ..default() },
                        AnimationsUi,
                    ));
                });
            parent.spawn((
                GameUiSet,
                Name::new("Audio Clips"),
                NodeBundle {
                    style: Style {
                        flex_direction: FlexDirection::Column,
                        ..default()
                    },
                    ..default()
                },
                AudioClipsUi,
            ));
        });
}

fn load_bogus(
    mut events: EventReader<KeyboardInput>,
    root: Query<Entity, (With<AnimationsUi>, Without<UiRef<Handle<AnimationClip>>>)>,
    mut commands: Commands,
) {
    events
        .iter()
        .filter(
            |&KeyboardInput {
                 key_code, state, ..
             }| *key_code == Some(KeyCode::Space) && *state == ButtonState::Pressed,
        )
        .for_each(|_| {
            commands
                .spawn((GameUiButton, Name::new("bogus"), NodeBundle { ..default() }))
                .set_parent(root.single());
        })
}

#[derive(Resource, Default, Debug)]
struct AssetRegistry(HashSet<HandleUntyped>);

/// Component marking UI for loaded Gltf assets
#[derive(Component)]
struct GltfsUi;

/// Drag+Drop import GLTF to editor
fn load_gltf(
    mut events: EventReader<FileDragAndDrop>,
    server: Res<AssetServer>,
    mut assets: ResMut<AssetRegistry>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf),
            _ => None,
        })
        .for_each(|path_buf| {
            let path = path_buf.as_path();
            let handle = server.load_untyped(path);
            assets.0.insert(handle);
        });
}

/// Helper method to extract the stripped filename given a full asset path
fn get_fname(asset_path: AssetPath, suffixes: &[&str]) -> String {
    let path = asset_path.path().file_name().expect("Filename");
    let path_str = path.to_str().expect("Asset Path to Str");
    let name_str = suffixes
        .iter()
        .rfold(path_str, |acc, &suffix| acc.trim_end_matches(suffix));
    String::from(name_str)
}

/// Sync GLTF assets with UI
///
/// TODO: Handle failed load events
///       Options:
///       * Show Error message, do not add to UI
///       * Add to UI with visual indicator
///       This should be a separate async system
fn manage_gltf_ui(
    mut events: EventReader<AssetEvent<Gltf>>,
    root: Query<Entity, (With<GltfsUi>, Without<UiRef<Handle<Gltf>>>)>,
    mut commands: Commands,
    server: Res<AssetServer>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            AssetEvent::Created { handle } => {
                let asset_path = server
                    .get_handle_path(handle.clone())
                    .expect("Fetch Asset Path");
                let name = get_fname(asset_path, &[".gltf", ".glb"]);
                Some((handle.clone(), String::from(name)))
            }
            _ => None,
        })
        .for_each(|(handle, name)| {
            root.iter().for_each(|entity| {
                commands.entity(entity).log_components();
            });
            commands
                .spawn((
                    GameUiButton,
                    Name::new(name),
                    NodeBundle { ..default() },
                    GltfsUi,
                    UiRef::Handle(handle.clone()),
                ))
                .set_parent(root.single());
        });
}

/// Remove gltf from editor
fn unload_gltf() {}

/// Component marking UI for Scene assets
#[derive(Component)]
struct ScenesUi;

/// Sync scene assets with UI
fn manage_scene_ui(
    mut events: EventReader<AssetEvent<Scene>>,
    root: Query<Entity, (With<ScenesUi>, Without<UiRef<Handle<Scene>>>)>,
    mut commands: Commands,
    gltfs: Res<Assets<Gltf>>,
    registry: Res<AssetRegistry>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            AssetEvent::Created { handle } => {
                let name = registry
                    .0
                    .iter()
                    .find_map(
                        |gltf_handle| match gltfs.get(&gltf_handle.clone().typed::<Gltf>()) {
                            Some(gltf) => {
                                gltf.named_scenes.iter().find_map(|(name, scene_handle)| {
                                    (scene_handle == handle).then_some(name)
                                })
                            }
                            None => None,
                        },
                    )
                    .expect("Find scene name");
                Some((handle.clone(), String::from(name)))
            }
            _ => None,
        })
        .for_each(|(handle, name)| {
            commands
                .spawn((
                    GameUiButton,
                    Name::new(name),
                    NodeBundle { ..default() },
                    ScenesUi,
                    UiRef::Handle(handle.clone()),
                ))
                .set_parent(root.single());
        });
}

/// Component marking UI for Camera assets
#[derive(Component)]
struct CamerasUi;

fn manage_camera_ui() {}

/// Component marking UI for Animation assets
#[derive(Component)]
struct AnimationsUi;

fn manage_animation_ui(
    mut events: EventReader<AssetEvent<AnimationClip>>,
    root: Query<Entity, (With<AnimationsUi>, Without<UiRef<Handle<AnimationClip>>>)>,
    mut commands: Commands,
    gltfs: Res<Assets<Gltf>>,
    registry: Res<AssetRegistry>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            AssetEvent::Created { handle } => {
                let name =
                    registry
                        .0
                        .iter()
                        .find_map(|gltf_handle| {
                            match gltfs.get(&gltf_handle.clone().typed::<Gltf>()) {
                                Some(gltf) => gltf.named_animations.iter().find_map(
                                    |(name, animation_handle)| {
                                        (animation_handle == handle).then_some(name)
                                    },
                                ),
                                None => None,
                            }
                        })
                        .expect("Find animation name");
                Some((handle.clone(), String::from(name)))
            }
            _ => None,
        })
        .for_each(|(handle, name)| {
            commands
                .spawn((
                    GameUiButton,
                    Name::new(name),
                    NodeBundle { ..default() },
                    AnimationsUi,
                    UiRef::Handle(handle.clone()),
                ))
                .set_parent(root.single());
        });
}

/// Component marking UI for Audio Clip assets
#[derive(Component)]
struct AudioClipsUi;

/// Drag+Drop import Audio to editor
fn load_audio(
    mut events: EventReader<FileDragAndDrop>,
    server: Res<AssetServer>,
    mut assets: ResMut<AssetRegistry>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            FileDragAndDrop::DroppedFile { path_buf, .. } => Some(path_buf),
            _ => None,
        })
        .for_each(|path_buf| {
            let path = path_buf.as_path();
            let handle = server.load_untyped(path);
            assets.0.insert(handle);
        });
}

fn manage_audio_ui(
    mut events: EventReader<AssetEvent<AudioSource>>,
    root: Query<Entity, (With<AudioClipsUi>, Without<AudioSink>)>,
    mut commands: Commands,
    server: Res<AssetServer>,
) {
    events
        .iter()
        .filter_map(|event| match event {
            AssetEvent::Created { handle } => {
                let asset_path = server
                    .get_handle_path(handle.clone())
                    .expect("Fetch Asset Path");
                let name = get_fname(asset_path, &[".ogg"]);
                Some((handle.clone(), String::from(name)))
            }
            _ => None,
        })
        .for_each(|(handle, name)| {
            commands
                .spawn((
                    GameUiButton,
                    Name::new(name),
                    NodeBundle { ..default() },
                    AudioClipsUi,
                    AudioBundle {
                        source: handle.clone(),
                        settings: PlaybackSettings {
                            mode: bevy::audio::PlaybackMode::Loop,
                            paused: true,
                            ..default()
                        },
                    },
                ))
                .set_parent(root.single());
        });
}

/// Play/Loop Audio
fn play_audio(
    mut events: Query<
        (&Interaction, &AudioSink, &mut UiElementState),
        (Changed<Interaction>, With<AudioClipsUi>),
    >,
) {
    events
        .iter_mut()
        .for_each(|(interaction, sink, mut state)| match interaction {
            Interaction::Pressed => {
                sink.toggle();

                *state = match *state {
                    UiElementState::Enabled => UiElementState::Active,
                    _ => UiElementState::Enabled,
                }
            }
            _ => (),
        });
}

/// Remove audio from editor
fn unload_audio() {}

/// Spawn Scene
fn spawn_scene() {}

/// Export level
fn export_level() {}

/// Import Level
fn import_level() {}
