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::components::intent::{FollowPlayerIntent, StopFollowingIntent};
use crate::debug::{collider_debug, gizmo, DebugMode};
use crate::editor::{editor_loop, EditorState, FrameStats};
use crate::entity::EntityHandle;
use crate::loaders::scene::Space;
use crate::physics::PhysicsManager;
use crate::render::particle_types::ParticleInstanceRaw;
use crate::render::snow::{SnowConfig, SnowLayer};
use crate::systems::{
    camera_follow_system, camera_ground_clamp_system, camera_input_system, camera_intent_system,
    camera_noclip_system, camera_transition_system, camera_view_matrix, collect_instances,
    dialog_bubble_render_system, dialog_camera_system, dialog_camera_transition_system,
    dialog_projectile_system, dialog_system, particle_intent_system, particle_update_system,
    physics_sync_system, player_input_system, render_system, rotate_system, snow_system,
    spawn_snow_particles, spotlight_sync_system, 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()
        .high_pixel_density()
        .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()?;
    world
        .follow_player_intents
        .insert(camera_entity, FollowPlayerIntent);

    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;
                game.world
                    .stop_following_intents
                    .insert(game.camera_entity, StopFollowingIntent);
                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
    {
        game.world
            .stop_following_intents
            .insert(game.camera_entity, StopFollowingIntent);
        game.sdl_context
            .mouse()
            .set_relative_mouse_mode(&game.window, false);
        game.editor.right_mouse_held = false;
        game.input_state.mouse_captured = false;
        if game.world.gizmo_mesh.is_none()
        {
            game.world.gizmo_mesh = Some(render::with_device(|device| {
                gizmo::create_gizmo_mesh(device)
            }));
            game.world.gizmo_instance_buffer = Some(render::with_device(|device| {
                gizmo::create_gizmo_instance_buffer(device)
            }));
        }
    }
    else
    {
        game.world
            .follow_player_intents
            .insert(game.camera_entity, FollowPlayerIntent);
        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.cameras,
        &game.world.transforms,
        &game.world.follows,
    )
    {
        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],
    particle_instances: &[ParticleInstanceRaw],
    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.cameras,
        &game.world.transforms,
        &game.world.follows,
    )
    {
        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, text_vertices) = dialog_bubble_render_system(
        &game.world.transforms,
        &game.world.dialog_bubbles,
        &game.world.bubble_tags,
        camera_transform.position,
        view_proj,
    );

    let frame = render::render(
        &view,
        &projection,
        camera_transform.position,
        player_pos,
        draw_calls,
        &billboard_calls,
        &text_vertices,
        particle_instances,
        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;
        }

        // --- intent generation ---
        camera_input_system(
            &mut game.world.cameras,
            &game.world.follows,
            &game.input_state,
        );
        player_input_system(
            &game.world.cameras,
            &game.world.follows,
            &game.world.player_tags,
            &mut game.world.inputs,
            &game.input_state,
        );
        dialog_camera_transition_system(
            &game.world.bubble_tags,
            &mut game.world.camera_transition_intents,
            &mut game.world.was_dialog_active,
            game.camera_entity,
        );

        // --- intent processing + camera ---
        camera_intent_system(
            &mut game.world.follow_player_intents,
            &mut game.world.stop_following_intents,
            &mut game.world.camera_transition_intents,
            &mut game.world.follows,
            &mut game.world.transforms,
            &mut game.world.cameras,
            &game.world.player_tags,
            &mut game.world.camera_transitions,
        );
        camera_noclip_system(
            &game.world.cameras,
            &game.world.follows,
            &mut game.world.transforms,
            &game.input_state,
            delta,
        );
        let player_pos = game.world.player_position();
        dialog_camera_system(
            &mut game.world.cameras,
            &mut game.world.transforms,
            &game.world.bubble_tags,
            player_pos,
            delta,
        );
        camera_follow_system(
            &mut game.world.follows,
            &game.world.cameras,
            &mut game.world.transforms,
        );
        camera_transition_system(
            &mut game.world.camera_transitions,
            &mut game.world.transforms,
            &mut game.world.cameras,
            delta,
        );
        camera_ground_clamp_system(
            &game.world.cameras,
            &game.world.follows,
            &mut game.world.transforms,
        );

        // --- editor overlay ---
        if game.editor.active
        {
            editor_loop(&mut game.editor, &mut game.world, &game.stats);
        }
        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(
                &game.world.entities,
                &game.world.physics,
                &mut game.world.transforms,
            );
            trigger_system(
                &mut game.world.trigger_events,
                &mut game.world.triggers,
                &game.world.transforms,
                &game.world.player_tags,
            );
            dialog_system(
                &mut game.world.entities,
                &game.world.trigger_events,
                &game.world.dialog_sources,
                &mut game.world.bubble_tags,
                &mut game.world.dialog_bubbles,
                &mut game.world.transforms,
                &mut game.world.names,
                &game.world.player_tags,
                &mut game.world.projectile_tags,
                &mut game.world.dialog_projectiles,
                &mut game.world.dialog_outcomes,
                FIXED_TIMESTEP,
            );
            dialog_projectile_system(
                &game.world.player_tags,
                &mut game.world.transforms,
                &mut game.world.projectile_tags,
                &mut game.world.dialog_projectiles,
                &mut game.world.spawn_particle_intents,
                &mut game.world.dialog_outcomes,
                &game.world.leaping_states,
                &game.world.rolling_states,
                &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(&game.world.rotates, &mut game.world.transforms, delta);

        tree_occlusion_system(
            &game.world.player_tags,
            &game.world.transforms,
            &game.world.cameras,
            &mut game.world.tree_instances,
        );
        tree_dissolve_update_system(&mut game.world.tree_instances, delta);
        tree_instance_buffer_update_system(&game.world.tree_instances);

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

        snow_system(
            &game.world.cameras,
            &game.world.transforms,
            &game.world.player_tags,
            &game.world.follows,
            &mut game.world.snow_layer,
        );

        particle_intent_system(
            &mut game.world.particle_buffers,
            &mut game.world.spawn_particle_intents,
        );
        particle_update_system(&mut game.world.particle_buffers, delta);
        let particle_cam_pos = game.world.active_camera_position();
        if let Some(ref mut buffers) = game.world.particle_buffers
        {
            spawn_snow_particles(buffers, particle_cam_pos, delta);
        }

        // --- draw call collection ---
        let mut draw_calls = render_system(
            &game.world.entities,
            &game.world.transforms,
            &game.world.meshes,
            &game.world.dissolves,
        );
        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());
        }

        if game.editor.active
        {
            if let Some(entity) = game.editor.selected_entity
            {
                let cam_pos = game.world.active_camera_position();
                if let (Some(ref mesh), Some(ref buf)) =
                    (&game.world.gizmo_mesh, &game.world.gizmo_instance_buffer)
                {
                    draw_calls.extend(gizmo::render_transform_gizmo(
                        entity,
                        &game.world.transforms,
                        cam_pos,
                        mesh,
                        buf,
                    ));
                }
            }
        }

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

        let particle_instances: Vec<ParticleInstanceRaw> = game
            .world
            .particle_buffers
            .as_mut()
            .map(|b| collect_instances(b).to_vec())
            .unwrap_or_default();

        // --- render ---
        submit_frame(&mut game, &draw_calls, &particle_instances, 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(())
}
