mod bundles;
mod components;
mod debug;
mod editor;
mod entity;
mod loaders;
mod paths;
mod physics;
mod picking;
mod postprocess;
mod render;
mod states;
mod systems;
mod texture;
mod utility;
mod world;

use crate::bundles::camera::CameraBundle;
use crate::bundles::player::PlayerBundle;
use crate::bundles::spotlight::spawn_spotlights;
use crate::bundles::terrain::{TerrainBundle, TerrainConfig};
use crate::bundles::test_char::TestCharBundle;
use crate::bundles::Bundle;
use crate::debug::{collider_debug, DebugMode};
use crate::editor::{editor_loop, EditorState, FrameStats};
use crate::entity::EntityHandle;
use crate::loaders::scene::Space;
use crate::physics::PhysicsManager;
use crate::render::snow::{SnowConfig, SnowLayer};
use crate::systems::camera::stop_camera_following;
use crate::systems::{
    camera_follow_system, camera_input_system, camera_view_matrix, dialog_bubble_render_system,
    dialog_camera_system, dialog_projectile_system, dialog_system, physics_sync_system,
    player_input_system, render_system, rotate_system, snow_system, spotlight_sync_system,
    start_camera_following, state_machine_physics_system, state_machine_system,
    tree_dissolve_update_system, tree_instance_buffer_update_system, tree_occlusion_system,
    trigger_system,
};
use crate::utility::input::InputState;
use crate::utility::time::Time;

use std::time::{Duration, Instant};

use glam::Vec3;
use render::Renderer;
use sdl3::event::Event;
use sdl3::keyboard::Keycode;
use sdl3::mouse::MouseButton;
use world::World;

struct Game
{
    sdl_context: sdl3::Sdl,
    window: sdl3::video::Window,
    _event_pump: sdl3::EventPump,
    world: World,
    editor: EditorState,
    input_state: InputState,
    camera_entity: EntityHandle,
    last_frame: Instant,
    frame_duration: Duration,
    physics_accumulator: f32,
    stats: FrameStats,
}

fn init() -> Result<Game, Box<dyn std::error::Error>>
{
    let sdl_context = sdl3::init()?;
    let video_subsystem = sdl_context.video()?;

    let window = video_subsystem
        .window("snow_trail", 1200, 900)
        .position_centered()
        .resizable()
        .vulkan()
        .build()?;
    let renderer = pollster::block_on(Renderer::new(&window, 2))?;
    render::init(renderer);

    let mut editor = render::with_device(|device| {
        render::with_queue(|queue| {
            render::with_surface_format(|format| EditorState::new(device, queue, format))
        })
    });
    editor.init_platform(&window);

    let (mut world, camera_entity) = init_world()?;
    start_camera_following(&mut world, camera_entity);

    let _event_pump = sdl_context.event_pump()?;
    let input_state = InputState::new();

    sdl_context.mouse().set_relative_mouse_mode(&window, true);

    Time::init();

    Ok(Game {
        sdl_context,
        window,
        _event_pump,
        world,
        editor,
        input_state,
        camera_entity,
        last_frame: Instant::now(),
        frame_duration: Duration::from_millis(1000 / 60),
        physics_accumulator: 0.0,
        stats: FrameStats {
            fps: 0.0,
            frame_ms: 0.0,
            physics_budget_ms: 0.0,
            draw_call_count: 0,
        },
    })
}

fn init_world() -> Result<(World, EntityHandle), Box<dyn std::error::Error>>
{
    let space = Space::load_space(&crate::paths::meshes::terrain())?;
    let terrain_config = TerrainConfig::default();

    let tree_positions: Vec<Vec3> = space
        .mesh_data
        .iter()
        .flat_map(|(_, instances)| instances.iter().map(|inst| inst.position))
        .collect();

    let player_spawn = space.player_spawn;
    let test_char_spawn = space.test_char_spawn;
    let camera_spawn = space.camera_spawn_position();
    let spotlights = space.spotlights;
    let mesh_data = space.mesh_data;

    let mut world = World::new();

    PlayerBundle {
        position: player_spawn,
    }
    .spawn(&mut world)
    .unwrap();

    TestCharBundle {
        position: test_char_spawn,
    }
    .spawn(&mut world)
    .unwrap();

    TerrainBundle::spawn(&mut world, mesh_data, &terrain_config)?;
    spawn_spotlights(&mut world, spotlights);

    render::set_terrain_data();

    let terrain_half_size = terrain_config.size / 2.0;
    render::init_snow_light_accumulation(
        glam::Vec2::new(-terrain_half_size.x, -terrain_half_size.y),
        glam::Vec2::new(terrain_half_size.x, terrain_half_size.y),
    );

    let snow_config = SnowConfig::default();
    let snow_layer = SnowLayer::load(&snow_config)?;
    for pos in &tree_positions
    {
        snow_layer.deform_at_position(*pos, 5.0, 50.0);
    }
    println!("Snow layer loaded successfully");
    render::set_snow_depth(&snow_layer.depth_texture_view);
    world.snow_layer = Some(snow_layer);

    let camera_entity = CameraBundle {
        position: camera_spawn,
    }
    .spawn(&mut world)
    .unwrap();

    Ok((world, camera_entity))
}

fn process_events(game: &mut Game) -> bool
{
    game.editor.begin_frame();

    while let Some(raw_event) = dear_imgui_sdl3::sdl3_poll_event_ll()
    {
        game.editor.process_event(&raw_event);
        let event = Event::from_ll(raw_event);

        match &event
        {
            Event::Quit { .. } =>
            {
                return true;
            }

            Event::KeyDown {
                keycode: Some(Keycode::Tab),
                repeat: false,
                ..
            } =>
            {
                toggle_editor(game);
                continue;
            }

            Event::MouseButtonDown {
                mouse_btn: MouseButton::Right,
                ..
            } if game.editor.active =>
            {
                game.editor.right_mouse_held = true;
                game.input_state.mouse_captured = true;
                stop_camera_following(&mut game.world, game.camera_entity);
                game.sdl_context
                    .mouse()
                    .set_relative_mouse_mode(&game.window, true);
                continue;
            }

            Event::MouseButtonUp {
                mouse_btn: MouseButton::Right,
                ..
            } if game.editor.active =>
            {
                game.editor.right_mouse_held = false;
                game.input_state.mouse_captured = false;
                game.sdl_context
                    .mouse()
                    .set_relative_mouse_mode(&game.window, false);
                continue;
            }

            Event::MouseButtonDown {
                mouse_btn: MouseButton::Left,
                x,
                y,
                ..
            } if game.editor.active && !game.editor.wants_mouse() =>
            {
                handle_editor_pick(game, *x, *y);
                continue;
            }

            _ =>
            {}
        }

        if game.editor.active && (game.editor.wants_keyboard() || game.editor.wants_mouse())
        {
            continue;
        }

        let capture_changed = game.input_state.handle_event(&event);
        if capture_changed && !game.editor.active
        {
            game.sdl_context
                .mouse()
                .set_relative_mouse_mode(&game.window, game.input_state.mouse_captured);
        }
    }

    false
}

fn toggle_editor(game: &mut Game)
{
    game.editor.active = !game.editor.active;
    if game.editor.active
    {
        stop_camera_following(&mut game.world, game.camera_entity);
        game.sdl_context
            .mouse()
            .set_relative_mouse_mode(&game.window, false);
        game.editor.right_mouse_held = false;
        game.input_state.mouse_captured = false;
    }
    else
    {
        start_camera_following(&mut game.world, game.camera_entity);
        game.input_state.mouse_captured = true;
        game.sdl_context
            .mouse()
            .set_relative_mouse_mode(&game.window, true);
    }
}

fn handle_editor_pick(game: &mut Game, x: f32, y: f32)
{
    let view = match camera_view_matrix(&game.world)
    {
        Some(v) => v,
        None => return,
    };
    let (_, cam) = match game.world.active_camera()
    {
        Some(c) => c,
        None => return,
    };
    let projection = cam.projection_matrix();
    let (win_w, win_h) = game.window.size();
    let ray = picking::Ray::from_screen_position(x, y, win_w, win_h, &view, &projection);
    game.editor.selected_entity = picking::pick_entity(&ray, &game.world);
    render::set_selected_entity(game.editor.selected_entity);
}

fn submit_frame(game: &mut Game, draw_calls: &[render::DrawCall], time: f32, delta: f32)
{
    let (camera_entity, camera_component) = match game.world.active_camera()
    {
        Some(c) => c,
        None => return,
    };
    let camera_transform = match game.world.transforms.get(camera_entity)
    {
        Some(t) => t,
        None => return,
    };
    let view = match camera_view_matrix(&game.world)
    {
        Some(v) => v,
        None => return,
    };

    let projection = camera_component.projection_matrix();
    let view_proj = projection * view;
    let player_pos = game.world.player_position();

    let billboard_calls =
        dialog_bubble_render_system(&game.world, camera_transform.position, view_proj);

    let frame = render::render(
        &view,
        &projection,
        camera_transform.position,
        player_pos,
        draw_calls,
        &billboard_calls,
        time,
        delta,
        game.world.debug_mode,
    );

    if game.editor.active || game.editor.show_player_state
    {
        let screen_view = frame
            .texture
            .create_view(&wgpu::TextureViewDescriptor::default());
        let mut encoder = render::with_device(|d| {
            d.create_command_encoder(&wgpu::CommandEncoderDescriptor {
                label: Some("ImGui Encoder"),
            })
        });
        game.editor.render(&mut encoder, &screen_view);
        render::with_queue(|q| q.submit(std::iter::once(encoder.finish())));
    }

    frame.present();
}

const FIXED_TIMESTEP: f32 = 1.0 / 60.0;

fn main() -> Result<(), Box<dyn std::error::Error>>
{
    let mut game = init()?;

    loop
    {
        let frame_start = Instant::now();
        let time = Time::get_time_elapsed();
        let delta = (frame_start - game.last_frame).as_secs_f32();
        game.last_frame = frame_start;

        // --- events ---
        if process_events(&mut game)
        {
            break;
        }

        if game.input_state.debug_cycle_just_pressed
        {
            game.world.debug_mode = game.world.debug_mode.cycle();
            println!("Debug mode: {:?}", game.world.debug_mode);
        }
        if game.input_state.f2_just_pressed
        {
            game.editor.show_player_state = !game.editor.show_player_state;
        }

        // --- camera + input ---
        camera_input_system(&mut game.world, &game.input_state);

        if game.editor.active
        {
            editor_loop(
                &mut game.editor,
                &mut game.world,
                &game.input_state,
                &game.stats,
                delta,
            );
        }
        else
        {
            let dialog_active = !game.world.bubble_tags.all().is_empty();
            if dialog_active
            {
                dialog_camera_system(&mut game.world, delta);
            }
            else
            {
                camera_follow_system(&mut game.world);
            }
            player_input_system(&mut game.world, &game.input_state);
            if game.editor.show_player_state
            {
                game.editor.build_hud(&game.world);
            }
        }

        // --- fixed-step physics ---
        let physics_start = Instant::now();
        game.physics_accumulator += delta;

        while game.physics_accumulator >= FIXED_TIMESTEP
        {
            state_machine_physics_system(&mut game.world, FIXED_TIMESTEP);
            PhysicsManager::physics_step();
            physics_sync_system(&mut game.world);
            trigger_system(&mut game.world);
            dialog_system(&mut game.world, FIXED_TIMESTEP);
            dialog_projectile_system(&mut game.world, &game.input_state);
            game.physics_accumulator -= FIXED_TIMESTEP;
        }

        game.stats.physics_budget_ms = physics_start.elapsed().as_secs_f32() * 1000.0;

        // --- per-frame systems ---
        state_machine_system(&mut game.world, delta);
        rotate_system(&mut game.world, delta);

        tree_occlusion_system(&mut game.world);
        tree_dissolve_update_system(&mut game.world, delta);
        tree_instance_buffer_update_system(&mut game.world);

        let spotlights = spotlight_sync_system(&game.world);
        render::update_spotlights(spotlights);

        snow_system(&mut game.world, game.editor.active);

        // --- draw call collection ---
        let mut draw_calls = render_system(&game.world);
        if let Some(ref snow_layer) = game.world.snow_layer
        {
            draw_calls.extend(snow_layer.get_draw_calls());
        }

        if game.world.debug_mode == DebugMode::Colliders
        {
            draw_calls.extend(collider_debug::render_collider_debug());
        }

        game.stats.draw_call_count = draw_calls.len();
        game.stats.fps = 1.0 / delta;
        game.stats.frame_ms = delta * 1000.0;

        // --- render ---
        submit_frame(&mut game, &draw_calls, time, delta);

        // --- end frame ---
        game.input_state.clear_just_pressed();

        let frame_time = frame_start.elapsed();
        if frame_time < game.frame_duration
        {
            std::thread::sleep(game.frame_duration - frame_time);
        }
    }

    Ok(())
}
