// yeah this lint is required to be allowed because winit doesn't use async
// logically, it wouldn't be possible to deadlock
#![allow(clippy::await_holding_lock)]

use winit::event::WindowEvent;
use winit::event_loop::ActiveEventLoop;
use winit::window::WindowId;

use crate::multisampling::AntiAliasingMode;
use crate::{WindowData, graphics::SharedGraphicsContext, input};
use parking_lot::RwLock;
use std::{collections::HashMap, rc::Rc, sync::Arc};
use egui::Ui;

pub trait Scene {
    fn load(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
    fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
    fn update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
    fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui);
    fn exit(&mut self, event_loop: &ActiveEventLoop);
    fn handle_event(&mut self, _event: &WindowEvent) {}
    /// By far a mess of a trait however it works.
    ///
    /// This struct allows you to add in a SceneCommand enum and send it to the scene management for them
    /// to parse through.
    fn run_command(&mut self) -> SceneCommand {
        SceneCommand::None
    }
}

#[derive(Clone)]
pub enum SceneCommand {
    None,
    Quit(Option<fn()>),
    SwitchScene(String),
    DebugMessage(String),
    RequestWindow(WindowData),
    CloseWindow(WindowId),
    SetFPS(u32),
    SetAntialiasing(AntiAliasingMode),
    ResizeViewport((u32, u32)),
}

impl Default for SceneCommand {
    fn default() -> Self {
        Self::None
    }
}

pub type SceneImpl = Rc<RwLock<dyn Scene>>;

#[derive(Clone)]
pub struct Manager {
    current_scene: Option<String>,
    next_scene: Option<String>,
    scenes: HashMap<String, SceneImpl>,
    scene_input_map: HashMap<String, String>,
}

impl Default for Manager {
    fn default() -> Self {
        Self::new()
    }
}

impl Manager {
    pub fn new() -> Self {
        Self {
            scenes: HashMap::new(),
            current_scene: None,
            next_scene: None,
            scene_input_map: HashMap::new(),
        }
    }

    pub fn switch(&mut self, name: &str) {
        if self.scenes.contains_key(name) {
            self.next_scene = Some(name.to_string());
            log::debug!("Switching to scene: {}", name)
        } else {
            log::warn!("No such scene as {}, not switching", name);
        }
    }

    pub fn add(&mut self, name: &str, scene: SceneImpl) {
        self.scenes.insert(name.to_string(), scene);
    }

    pub fn attach_input(&mut self, scene_name: &str, input_name: &str) {
        self.scene_input_map
            .insert(scene_name.to_string(), input_name.to_string());
    }

    pub fn update<'a>(
        &mut self,
        dt: f32,
        graphics: Arc<SharedGraphicsContext>,
        event_loop: &ActiveEventLoop,
        ui: &mut Ui,
    ) -> Vec<SceneCommand> {
        puffin::profile_function!();
        // transition scene
        if let Some(next_scene_name) = self.next_scene.take() {
            if let Some(current_scene_name) = &self.current_scene
                && let Some(scene) = self.scenes.get_mut(current_scene_name)
            {
                {
                    puffin::profile_scope!("exit old scene", current_scene_name);
                    scene.write().exit(event_loop);
                }
            }
            if let Some(scene) = self.scenes.get_mut(&next_scene_name) {
                {
                    puffin::profile_scope!("load new scene", &next_scene_name);
                    scene.write().load(graphics.clone(), ui);
                }
            }
            self.current_scene = Some(next_scene_name);
        }

        // update scene
        if let Some(scene_name) = &self.current_scene
            && let Some(scene) = self.scenes.get_mut(scene_name)
        {
            {
                puffin::profile_scope!("update new scene", &scene_name);
                scene.write().update(dt, graphics.clone(), ui);
            }
            let command = scene.write().run_command();
            match command {
                SceneCommand::SwitchScene(target) => {
                    if let Some(current) = &self.current_scene {
                        if current == &target {
                            // reload the scene
                            if let Some(scene) = self.scenes.get_mut(current) {
                                puffin::profile_scope!("reload the scene", &current);
                                scene.write().exit(event_loop);
                                scene.write().load(graphics.clone(), ui);

                                log::debug!("Reloaded scene: {}", current);
                            }
                        } else {
                            self.switch(&target);
                        }
                    } else {
                        self.switch(&target);
                    }
                }
                SceneCommand::Quit(hook) => {
                    return vec![SceneCommand::Quit(hook)];
                }
                SceneCommand::None => {}
                SceneCommand::DebugMessage(msg) => log::debug!("{}", msg),
                SceneCommand::RequestWindow(_)
                | SceneCommand::CloseWindow(_)
                | SceneCommand::SetFPS(_)
                | SceneCommand::SetAntialiasing(_)
                | SceneCommand::ResizeViewport(_) => {
                    return vec![command];
                }
            }
        }

        Vec::new()
    }

    pub fn physics_update(&mut self, dt: f32, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui) {
        puffin::profile_function!();
        if let Some(scene_name) = &self.current_scene
            && let Some(scene) = self.scenes.get_mut(scene_name)
        {
            puffin::profile_scope!("physics-update the scene", &scene_name);
            scene.write().physics_update(dt, graphics.clone(), ui)
        }
    }

    pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>, ui: &mut Ui) {
        puffin::profile_function!();
        if let Some(scene_name) = &self.current_scene
            && let Some(scene) = self.scenes.get_mut(scene_name)
        {
            puffin::profile_scope!("render the scene", &scene_name);
            scene.write().render(graphics.clone(), ui)
        }
    }

    pub fn handle_event(&mut self, event: &WindowEvent) {
        puffin::profile_function!();
        if let Some(scene_name) = &self.current_scene
            && let Some(scene) = self.scenes.get_mut(scene_name)
        {
            puffin::profile_scope!("handle winit window event in the scene", &scene_name);
            scene.write().handle_event(event);
        }
    }

    pub fn has_scene(&self) -> bool {
        self.current_scene.is_some()
    }

    pub fn get_current_scene(&self) -> Option<(&String, &SceneImpl)> {
        if let Some(scene_name) = &self.current_scene {
            if let Some(scene) = self.scenes.get(scene_name) {
                return Some((scene_name, scene));
            }
            return None;
        }
        None
    }

    pub fn get_active_input_handlers(&self) -> Vec<String> {
        if let Some(scene_name) = &self.current_scene {
            vec![
                format!("{}_keyboard", scene_name),
                format!("{}_mouse", scene_name),
                format!("{}_controller", scene_name),
            ]
        } else {
            Vec::new()
        }
    }

    pub fn get_current_scene_name(&self) -> Option<&String> {
        self.current_scene.as_ref()
    }

    pub fn cleanup_scene(&mut self, scene_name: &str, event_loop: &ActiveEventLoop) {
        if let Some(scene) = self.scenes.get(scene_name) {
            scene.write().exit(event_loop);
        }
        self.scenes.remove(scene_name);
    }

    pub fn clear_all(&mut self, event_loop: &ActiveEventLoop) {
        for scene in self.scenes.values() {
            scene.write().exit(event_loop);
        }
        self.scenes.clear();
        self.current_scene = None;
        self.next_scene = None;
        self.scene_input_map.clear();
    }
}

/// Helper function that adds a struct that implements [`Scene`], [`input::Keyboard`],
/// [`input::Mouse`] and [`input::Controller`].
///
/// Specifically, it adds the struct as keyboard, mouse and controller, then it attaches
/// that input structs to the scene.
pub fn add_scene_with_input<
    S: 'static + Scene + input::Keyboard + input::Mouse + input::Controller,
>(
    scene_manager: &mut Manager,
    input_manager: &mut input::Manager,
    scene: Rc<RwLock<S>>,
    scene_name: &str,
) {
    scene_manager.add(scene_name, scene.clone());
    input_manager.add_keyboard(&format!("{}_keyboard", scene_name), scene.clone());
    input_manager.add_mouse(&format!("{}_mouse", scene_name), scene.clone());
    input_manager.add_controller(&format!("{}_controller", scene_name), scene.clone());
    scene_manager.attach_input(scene_name, &format!("{}_keyboard", scene_name));
    scene_manager.attach_input(scene_name, &format!("{}_mouse", scene_name));
    scene_manager.attach_input(scene_name, &format!("{}_controller", scene_name));
}
