mod physics;
mod scene;

use crate::{
    screen::{Camera, RenderError, RenderInput, RenderOptions, Screen},
    text::{TextSystem, TextSystemData},
};
use assets_manager::AssetCache;
use winit::{
    event::{ElementState, KeyEvent, WindowEvent},
    keyboard::{Key, NamedKey},
    window::Window,
};

use self::{
    physics::{PhysicsState, PhysicsSystem},
    scene::{Scene, SceneLibrary, SceneReference},
};

pub struct Game<S> {
    screen: Screen,
    egui: egui::Context,
    egui_winit: egui_winit::State,

    previous_state: PhysicsState,
    current_state: PhysicsState,

    scene_library: SceneLibrary,
    current_scene: Box<dyn Scene<S> + Send>,

    internal_state: GameState,
    // executor: Cosync<GameState<S>>,

    // systems
    physics: PhysicsSystem,
    text: TextSystem,

    // internal controls
    show_debug: bool,
}

pub struct GameState {
    pub world: hecs::World,
    pub camera: Camera,
    pub render_options: RenderOptions,
}

pub struct GameView<'a, S> {
    pub assets: &'a AssetCache<S>,
    pub world: &'a mut hecs::World,
    pub camera: &'a mut Camera,
    pub render_options: &'a mut RenderOptions,
    // pub executor: &'a mut Cosync<GameState<S>>,
}

macro_rules! extract_view {
    ($game: expr, $assets: expr) => {
        GameView {
            assets: $assets,
            world: &mut $game.internal_state.world,
            camera: &mut $game.internal_state.camera,
            render_options: &mut $game.internal_state.render_options,
            // executor: &mut $game.executor,
        }
    };
}

impl<S> Game<S> {
    pub async fn new(window: Window, assets: &AssetCache<S>) -> color_eyre::Result<Self>
    where
        S: assets_manager::source::Source + 'static,
    {
        let scene_library = SceneLibrary;
        let mut world = hecs::World::new();
        let mut camera = Camera::default();
        let mut render_options = RenderOptions::default();
        // The *only* time we need to manually do this, all others should use extract_view!
        let mut view = GameView {
            assets,
            world: &mut world,
            camera: &mut camera,
            render_options: &mut render_options,
            // executor: &mut executor,
        };
        let mut initial_scene = scene_library.get_scene(&mut view, SceneReference::Init);
        initial_scene.on_enter(&mut view);
        let egui = egui::Context::default();
        let egui_winit =
            egui_winit::State::new(egui.clone(), egui::ViewportId::ROOT, &window, None, None);
        let screen = Screen::new(window).await?;

        Ok(Self {
            egui,
            egui_winit,
            screen,
            scene_library,
            current_scene: initial_scene,

            // initally empty states
            previous_state: PhysicsState::default(),
            current_state: PhysicsState::default(),
            // executor,

            // systems
            physics: PhysicsSystem::new(),
            text: TextSystem::new(assets)?,

            show_debug: false,
            internal_state: GameState {
                world,
                camera,
                render_options,
            },
        })
    }

    pub async fn create_surface(&mut self, assets: &AssetCache<S>) -> color_eyre::Result<()>
    where
        S: assets_manager::source::Source,
    {
        self.screen.create_surface(assets).await
    }

    pub fn screen(&self) -> &Screen {
        &self.screen
    }

    pub fn screen_mut(&mut self) -> &mut Screen {
        &mut self.screen
    }

    /// Runs egui code
    pub fn gui_egui(
        &mut self,
        assets: &AssetCache<S>,
    ) -> (egui::FullOutput, Vec<egui::ClippedPrimitive>) {
        egui_winit::update_viewport_info(
            self.egui_winit
                .egui_input_mut()
                .viewports
                .get_mut(&egui::ViewportId::ROOT)
                .unwrap(),
            &self.egui,
            self.screen.window(),
        );
        let input = self.egui_winit.take_egui_input(self.screen.window());

        let full_output = self.egui.run(input, |ctx| {
            egui::Window::new("Debug Tools")
                .open(&mut self.show_debug)
                .show(ctx, |ui| {
                    ui.horizontal(|ui| {
                        let mut vsync = self.screen.is_vsync();
                        let text = if vsync { "yes" } else { "no" };
                        ui.label("VSync");
                        ui.toggle_value(&mut vsync, text);
                        self.screen.vsync(vsync);
                    });
                    ui.add(
                        egui::Slider::new(&mut self.internal_state.render_options.gamma, 0.0..=2.0)
                            .text("Gamma"),
                    );
                });

            let mut view = extract_view!(self, assets);
            self.current_scene.gui_egui(ctx, &mut view)
        });

        self.egui_winit
            .handle_platform_output(self.screen.window(), full_output.platform_output.clone());

        let prims = self
            .egui
            .tessellate(full_output.shapes.clone(), full_output.pixels_per_point);

        (full_output, prims)
    }

    /// Returns true if the event was handled
    pub fn input(&mut self, assets: &AssetCache<S>, event: &WindowEvent) -> bool {
        #[cfg(debug_assertions)]
        if matches!(
            event,
            WindowEvent::KeyboardInput {
                event: KeyEvent {
                    logical_key: Key::Named(NamedKey::F1),
                    state: ElementState::Pressed,
                    repeat: false,
                    ..
                },
                ..
            }
        ) {
            self.show_debug = !self.show_debug;
            return true;
        }

        if self
            .egui_winit
            .on_window_event(self.screen.window(), event)
            .consumed
        {
            return true;
        }

        let mut view = extract_view!(self, assets);
        self.current_scene.input(event, &mut view)
    }

    /// Syncs the physics system's results back into the world
    ///
    /// alpha is a % of the fixed timestep used by update that is unused in the frame,
    /// so this must be [0.0, 1.0)
    pub fn physics_sync(&mut self, delta: f32) {
        // if this becomes a problem, then we might look at this
        // sync the state that is lerped between the previous state and the current state by alpha
        let actual_state = self.current_state.lerp(&self.previous_state, delta);
        _ = actual_state;
    }

    pub fn update(&mut self, dt: std::time::Duration, assets: &AssetCache<S>)
    where
        S: assets_manager::source::Source + Sync + 'static,
    {
        puffin::profile_function!();

        // Reload all assets (if they have changed)
        assets.hot_reload();

        // Run any in-progress tasks from the async executor
        // self.executor.run_until_stall(&mut self.internal_state);

        let mut view = extract_view!(self, assets);

        if let Some(reference) = self.current_scene.update(dt, &mut view) {
            let new_scene = self.scene_library.get_scene(&mut view, reference);
            self.current_scene.on_exit(&mut view);
            self.current_scene = new_scene;
            self.current_scene.on_enter(&mut view);
        }
    }

    pub fn render(&mut self, assets: &AssetCache<S>) -> Result<(), RenderError>
    where
        S: assets_manager::source::Source,
    {
        puffin::profile_function!();

        // Any resources only used for the rendering process go here
        let (egui_output, paint_jobs) = self.gui_egui(assets);
        let text_data = TextSystemData::extract(&mut self.internal_state.world);

        self.text.prepare(&text_data, assets);

        let camera_text_data_info: Vec<_> = self
            .text
            .produce_camera(&text_data, assets)
            .into_iter()
            .collect();
        let camera_text_data = camera_text_data_info
            .iter()
            .map(|(buf, mapper)| mapper.produce(buf))
            .collect();

        let world_text_data_info: Vec<_> = self
            .text
            .produce_world(&text_data, assets)
            .into_iter()
            .collect();
        let world_text_data = world_text_data_info
            .iter()
            .map(|(buf, mapper)| mapper.produce(buf))
            .collect();

        // Build the renderer input
        let input = RenderInput {
            paint_jobs: &paint_jobs,
            textures_delta: &egui_output.textures_delta,
            font_system: &mut self.text.fonts,
            swash_cache: &mut self.text.swash,
            camera: &mut self.internal_state.camera,
            render_options: &self.internal_state.render_options,
            camera_text_data,
            world_text_data,
        };

        self.screen.render(input)
    }

    // methods here might query the world to produce
    // rendering data ready for Screen to use
}