mod game;
mod ldtk;
mod screen;
mod text;

use std::{sync::Arc, time::Duration};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;

use async_mutex::Mutex as AsyncMutex;
use game::Game;
use winit::{
    dpi,
    event::*,
    event_loop::{ControlFlow, EventLoop, EventLoopBuilder},
    window::WindowBuilder,
};

const WINDOW_WIDTH: u32 = 900;
const WINDOW_HEIGHT: u32 = 600;
const LOGIC_DURATION: Duration = Duration::from_millis(16);

#[cfg_attr(target_arch = "wasm32", wasm_bindgen(start))]
#[cfg(feature = "web")]
pub async fn main_web() {
    // TODO move to tracing eventually?
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));
    console_log::init_with_level(log::Level::Warn).expect("Couldn't initialize logger");
    run().await.unwrap()
}

pub async fn run() -> color_eyre::Result<()> {
    #[cfg(not(any(feature = "web", feature = "desktop")))]
    compile_error!("Must compile with platform feature");

    let event_loop: EventLoop<()> = EventLoopBuilder::new().build()?;
    #[cfg_attr(not(target_arch = "wasm32"), allow(unused_mut))] // the mut is used on web
    let mut builder = WindowBuilder::new()
        .with_inner_size(dpi::LogicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT))
        .with_min_inner_size(dpi::PhysicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT));
    #[cfg(target_arch = "wasm32")]
    {
        use winit::platform::web::WindowBuilderExtWebSys;
        builder = builder.with_append(true);
        // .with_inner_size(dpi::PhysicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT));
    }
    let window = builder.build(&event_loop)?;
    #[cfg(target_arch = "wasm32")]
    {
        use winit::platform::web::WindowExtWebSys;
        let on_document = |doc: web_sys::Document| {
            if let Some(canvas) = window.canvas() {
                let dst = doc.get_element_by_id("game")?;
                dst.append_child(&canvas).ok()?;
            }
            Some(())
        };
        if web_sys::window()
            .and_then(|win| win.document())
            .and_then(on_document)
            .is_none()
        {
            log::warn!("Failed to find an element with 'game' id, leaving window appended");
        }
    }

    event_loop.set_control_flow(ControlFlow::Poll);

    let main_window_id = window.id();

    // Pick the assets source
    #[cfg(not(target_arch = "wasm32"))]
    let source = assets_manager::source::FileSystem::new("assets")?;
    #[cfg(target_arch = "wasm32")]
    let source = assets_manager::source::Embedded::from(assets_manager::source::embed!("assets"));
    let assets = Arc::new(assets_manager::AssetCache::with_source(source));

    let game = match Game::new(window, &assets).await {
        Ok(game) => game,
        Err(e) => {
            log::error!("{}", e);
            return Err(e);
        }
    };
    let game = Arc::new(AsyncMutex::new(game));
    let mut outer_cosync: cosync::Cosync<()> = cosync::Cosync::new();

    let mut tick = instant::Instant::now();
    let mut accum = Duration::ZERO;

    event_loop.run(move |mut event, elwt| match event {
        Event::WindowEvent {
            window_id,
            ref mut event,
        } if window_id == main_window_id => {
            if !game.try_lock().unwrap().input(&assets, event) {
                match event {
                    WindowEvent::CloseRequested => elwt.exit(),
                    WindowEvent::RedrawRequested => {
                        // The profiler should get a new frame
                        puffin::GlobalProfiler::lock().new_frame();
                        let mut game = game.try_lock().unwrap(); //winit_executor.get_lock(&game);

                        // Redraw the application.
                        //
                        // It's preferable for applications that do not render continuously to render in
                        // this event rather than in AboutToWait, since rendering in here allows
                        // the program to gracefully handle redraws requested by the OS.
                        match game.render(&assets) {
                            Ok(()) => {}
                            // Reconfigure surface if lost
                            Err(screen::RenderError::Wgpu(wgpu::SurfaceError::Lost)) => {
                                game.screen().reconfigure()
                            }
                            // System OOM, quit
                            Err(screen::RenderError::Wgpu(wgpu::SurfaceError::OutOfMemory)) => {
                                elwt.exit()
                            }
                            // All other errors should resolve themselves next render
                            Err(e) => log::warn!("Encountered render error: {e:?}"),
                        }
                    }
                    WindowEvent::ScaleFactorChanged {
                        scale_factor,
                        ref mut inner_size_writer,
                    } => {
                        inner_size_writer
                            .request_inner_size(dpi::PhysicalSize::new(
                                WINDOW_WIDTH * *scale_factor as u32,
                                WINDOW_HEIGHT * *scale_factor as u32,
                            ))
                            .unwrap();
                    }
                    WindowEvent::Resized(phys_size) => {
                        let mut game = game.try_lock().unwrap();
                        game.screen_mut().resize(*phys_size);
                    }
                    _ => {}
                }
            }
        }
        Event::Resumed => {
            // Actually create the surface (and panic if we fail...)
            let game = game.clone();
            let assets = assets.clone();
            outer_cosync.queue(|_input: cosync::CosyncInput<()>| async move {
                game.try_lock()
                    .unwrap()
                    .create_surface(&assets)
                    .await
                    .unwrap();
            })
        }
        Event::AboutToWait => {
            // poll cosync
            outer_cosync.run_until_stall(&mut ());

            // App update code
            let elapsed = tick.elapsed();
            tick = instant::Instant::now();
            accum += elapsed;

            let mut game = game.try_lock().unwrap();

            while accum >= LOGIC_DURATION {
                accum -= LOGIC_DURATION;
                game.update(LOGIC_DURATION, &assets);
            }

            // Sync the physics system with the real world *now* (using the remainder of the accumuator as a delta)
            game.physics_sync(accum.as_secs_f32() / LOGIC_DURATION.as_secs_f32());

            // Queue a RedrawRequested event.
            //
            // You only need to call this if you've determined that you need to redraw in
            // applications which do not always need to. Applications that redraw continuously
            // can render here instead.
            game.screen().window().request_redraw();
        }
        _ => {}
    })?;

    Ok(())
}