use crate::ecs::EditorWorld;
use crate::gltf_fetch;
use crate::systems::polyhaven::Category;
use nightshade::ecs::prefab::resources::mesh_cache_clear;
use nightshade::prelude::*;

pub fn preload_browsers(editor_world: &mut EditorWorld) {
    editor_world
        .resources
        .browsers
        .sample_browser
        .ensure_loaded();
    editor_world
        .resources
        .browsers
        .polyhaven_browser
        .ensure_loaded(Category::Models);
    editor_world
        .resources
        .browsers
        .polyhaven_browser
        .ensure_loaded(Category::Hdris);
}

pub struct PendingImport {
    pub name: String,
    pub state: PendingImportState,
}

pub enum PendingImportState {
    #[cfg(not(target_arch = "wasm32"))]
    Threaded(std::sync::mpsc::Receiver<Result<nightshade::ecs::prefab::GltfLoadResult, String>>),
    #[cfg(target_arch = "wasm32")]
    Ready(Result<nightshade::ecs::prefab::GltfLoadResult, String>),
}

pub fn handle_dropped_data(
    editor_world: &mut EditorWorld,
    world: &mut World,
    name: &str,
    data: &[u8],
) {
    let lower = name.to_lowercase();
    if lower.ends_with(".gltf") || lower.ends_with(".glb") {
        load_gltf_bytes(editor_world, name, data);
    } else if lower.ends_with(".json") || lower.ends_with(".nsmap") {
        crate::systems::project_io::load_project_bytes_into_editor(editor_world, world, name, data);
    } else if lower.ends_with(".hdr") {
        load_hdr_skybox(world, data.to_vec());
        world.resources.render_settings.atmosphere = Atmosphere::Hdr;
        editor_world.resources.loading.last_dropped_error = None;
        crate::scene_writeback::sync_hdr_skybox(
            &mut editor_world.resources.project,
            Some(nightshade::ecs::scene::SceneHdrSkybox::Embedded {
                data: data.to_vec(),
            }),
        );
        crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
        tracing::info!("Loaded HDR skybox '{}'", name);
    } else if lower.ends_with(".bin")
        || lower.ends_with(".png")
        || lower.ends_with(".jpg")
        || lower.ends_with(".jpeg")
    {
        tracing::info!(
            "Ignoring '{}' (drop the entire glTF folder, not just the asset)",
            name
        );
        editor_world.resources.loading.last_dropped_error = None;
    } else {
        editor_world.resources.loading.last_dropped_error =
            Some(format!("Unsupported file: {}", name));
        tracing::warn!("dropped file '{}' has unsupported extension", name);
    }
}

pub fn handle_dropped_files(
    editor_world: &mut EditorWorld,
    world: &mut World,
    files: &[nightshade::ecs::input::resources::DroppedFile],
) {
    let gltf_index = files
        .iter()
        .position(|file| file.name.to_lowercase().ends_with(".gltf"));
    if let Some(index) = gltf_index {
        let gltf_file = &files[index];
        let parent = gltf_file
            .name
            .rsplit_once('/')
            .map(|(parent, _)| parent.to_string())
            .unwrap_or_default();
        let mut resources = std::collections::HashMap::new();
        for (other_index, file) in files.iter().enumerate() {
            if other_index == index {
                continue;
            }
            let key = if parent.is_empty() {
                file.name.clone()
            } else if let Some(rest) = file.name.strip_prefix(&format!("{}/", parent)) {
                rest.to_string()
            } else {
                file.name.clone()
            };
            resources.insert(key, file.data.clone());
        }
        let name = gltf_file.name.clone();
        #[cfg(not(target_arch = "wasm32"))]
        {
            let (sender, receiver) = std::sync::mpsc::channel();
            let bytes_owned = gltf_file.data.clone();
            let resources_owned = resources;
            std::thread::spawn(move || {
                let result = nightshade::ecs::prefab::import_gltf_with_resources(
                    &bytes_owned,
                    &resources_owned,
                )
                .map_err(|error| error.to_string());
                let _ = sender.send(result);
            });
            editor_world
                .resources
                .loading
                .pending_imports
                .push(PendingImport {
                    name,
                    state: PendingImportState::Threaded(receiver),
                });
        }
        #[cfg(target_arch = "wasm32")]
        {
            let result =
                nightshade::ecs::prefab::import_gltf_with_resources(&gltf_file.data, &resources)
                    .map_err(|error| error.to_string());
            editor_world
                .resources
                .loading
                .pending_imports
                .push(PendingImport {
                    name,
                    state: PendingImportState::Ready(result),
                });
        }
        return;
    }
    for file in files {
        handle_dropped_data(editor_world, world, &file.name, &file.data);
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub fn handle_dropped_path(
    editor_world: &mut EditorWorld,
    world: &mut World,
    path: &std::path::Path,
) {
    let name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("dropped")
        .to_string();
    let lower = name.to_lowercase();
    if lower.ends_with(".gltf") || lower.ends_with(".glb") {
        load_gltf_path(editor_world, path);
    } else if lower.ends_with(".json") || lower.ends_with(".nsmap") {
        match std::fs::read(path) {
            Ok(bytes) => crate::systems::project_io::load_project_bytes_into_editor(
                editor_world,
                world,
                &name,
                &bytes,
            ),
            Err(error) => report_load_error(editor_world, &name, format!("read failed: {error}")),
        }
    } else if lower.ends_with(".hdr") {
        load_hdr_skybox_from_path(world, path.to_path_buf());
        world.resources.render_settings.atmosphere = Atmosphere::Hdr;
        editor_world.resources.loading.last_dropped_error = None;
        if let Some(path_str) = path.to_str() {
            crate::scene_writeback::sync_hdr_skybox(
                &mut editor_world.resources.project,
                Some(nightshade::ecs::scene::SceneHdrSkybox::Reference {
                    path: path_str.to_string(),
                }),
            );
        }
        crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
        tracing::info!("Loaded HDR skybox '{}'", name);
    } else {
        editor_world.resources.loading.last_dropped_error =
            Some(format!("Unsupported file: {}", name));
        tracing::warn!("dropped file '{}' has unsupported extension", name);
    }
}

pub fn load_gltf_with_resources(
    editor_world: &mut EditorWorld,
    pending: gltf_fetch::PendingGltfMulti,
) {
    let gltf_fetch::PendingGltfMulti {
        display_name,
        gltf_bytes,
        resources,
    } = pending;
    #[cfg(not(target_arch = "wasm32"))]
    {
        let (sender, receiver) = std::sync::mpsc::channel();
        let bytes_owned = gltf_bytes;
        let resources_owned = resources;
        std::thread::spawn(move || {
            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                nightshade::ecs::prefab::import_gltf_with_resources(&bytes_owned, &resources_owned)
                    .map_err(|error| error.to_string())
            }));
            let result = match result {
                Ok(value) => value,
                Err(payload) => {
                    let message = if let Some(text) = payload.downcast_ref::<&str>() {
                        (*text).to_string()
                    } else if let Some(text) = payload.downcast_ref::<String>() {
                        text.clone()
                    } else {
                        "import panicked".to_string()
                    };
                    Err(format!("import panicked: {message}"))
                }
            };
            let _ = sender.send(result);
        });
        editor_world
            .resources
            .loading
            .pending_imports
            .push(PendingImport {
                name: display_name,
                state: PendingImportState::Threaded(receiver),
            });
    }
    #[cfg(target_arch = "wasm32")]
    {
        let result = nightshade::ecs::prefab::import_gltf_with_resources(&gltf_bytes, &resources)
            .map_err(|error| error.to_string());
        editor_world
            .resources
            .loading
            .pending_imports
            .push(PendingImport {
                name: display_name,
                state: PendingImportState::Ready(result),
            });
    }
}

pub fn load_gltf_bytes(editor_world: &mut EditorWorld, name: &str, bytes: &[u8]) {
    #[cfg(not(target_arch = "wasm32"))]
    {
        let (sender, receiver) = std::sync::mpsc::channel();
        let bytes_owned = bytes.to_vec();
        std::thread::spawn(move || {
            let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
                nightshade::ecs::prefab::import_gltf_from_bytes(&bytes_owned)
                    .map_err(|error| error.to_string())
            }));
            let result = match result {
                Ok(value) => value,
                Err(payload) => {
                    let message = if let Some(text) = payload.downcast_ref::<&str>() {
                        (*text).to_string()
                    } else if let Some(text) = payload.downcast_ref::<String>() {
                        text.clone()
                    } else {
                        "import panicked".to_string()
                    };
                    Err(format!("import panicked: {message}"))
                }
            };
            let _ = sender.send(result);
        });
        editor_world
            .resources
            .loading
            .pending_imports
            .push(PendingImport {
                name: name.to_string(),
                state: PendingImportState::Threaded(receiver),
            });
    }
    #[cfg(target_arch = "wasm32")]
    {
        let result = nightshade::ecs::prefab::import_gltf_from_bytes(bytes)
            .map_err(|error| error.to_string());
        editor_world
            .resources
            .loading
            .pending_imports
            .push(PendingImport {
                name: name.to_string(),
                state: PendingImportState::Ready(result),
            });
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub fn load_gltf_path(editor_world: &mut EditorWorld, path: &std::path::Path) {
    let name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or("dropped")
        .to_string();
    load_gltf_path_named(editor_world, &name, path);
}

#[cfg(not(target_arch = "wasm32"))]
pub fn load_gltf_path_named(editor_world: &mut EditorWorld, name: &str, path: &std::path::Path) {
    let (sender, receiver) = std::sync::mpsc::channel();
    let path_owned = path.to_path_buf();
    std::thread::spawn(move || {
        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
            nightshade::ecs::prefab::import_gltf_from_path(&path_owned)
                .map_err(|error| error.to_string())
        }));
        let result = match result {
            Ok(value) => value,
            Err(payload) => {
                let message = if let Some(text) = payload.downcast_ref::<&str>() {
                    (*text).to_string()
                } else if let Some(text) = payload.downcast_ref::<String>() {
                    text.clone()
                } else {
                    "import panicked".to_string()
                };
                Err(format!("import panicked: {message}"))
            }
        };
        let _ = sender.send(result);
    });
    editor_world
        .resources
        .loading
        .pending_imports
        .push(PendingImport {
            name: name.to_string(),
            state: PendingImportState::Threaded(receiver),
        });
}

pub fn poll_pending_imports(editor_world: &mut EditorWorld, world: &mut World) {
    let pending = std::mem::take(&mut editor_world.resources.loading.pending_imports);
    for pending in pending {
        match pending.state {
            #[cfg(not(target_arch = "wasm32"))]
            PendingImportState::Threaded(receiver) => match receiver.try_recv() {
                Ok(Ok(result)) => apply_gltf_result(editor_world, world, &pending.name, result),
                Ok(Err(error)) => report_load_error(editor_world, &pending.name, error),
                Err(std::sync::mpsc::TryRecvError::Empty) => {
                    editor_world
                        .resources
                        .loading
                        .pending_imports
                        .push(PendingImport {
                            name: pending.name,
                            state: PendingImportState::Threaded(receiver),
                        });
                }
                Err(std::sync::mpsc::TryRecvError::Disconnected) => {
                    report_load_error(
                        editor_world,
                        &pending.name,
                        "import thread terminated".to_string(),
                    );
                }
            },
            #[cfg(target_arch = "wasm32")]
            PendingImportState::Ready(Ok(result)) => {
                apply_gltf_result(editor_world, world, &pending.name, result);
            }
            #[cfg(target_arch = "wasm32")]
            PendingImportState::Ready(Err(error)) => {
                report_load_error(editor_world, &pending.name, error);
            }
        }
    }
}

pub fn poll_thumbnails(editor_world: &mut EditorWorld, world: &mut World) {
    let entries_signature = editor_world
        .resources
        .browsers
        .sample_browser
        .entries()
        .map(|e| e.len() as u64)
        .unwrap_or(0);
    if entries_signature > 0
        && entries_signature != editor_world.resources.browsers.thumbnail_signature
    {
        editor_world.resources.browsers.thumbnail_signature = entries_signature;
        editor_world
            .resources
            .browsers
            .sample_browser
            .queue_thumbnails(&editor_world.resources.browsers.thumbnail_queue);
    }

    #[cfg(not(target_arch = "wasm32"))]
    editor_world
        .resources
        .browsers
        .kenney_browser
        .queue_thumbnails(&editor_world.resources.browsers.thumbnail_queue);

    process_texture_queue(&editor_world.resources.browsers.thumbnail_queue, 4);
    let completed = editor_world
        .resources
        .browsers
        .thumbnail_queue
        .lock()
        .map(|mut q| q.take_completed())
        .unwrap_or_default();
    for loaded in completed {
        if let Some(upload) = ui_upload_image(world, &loaded.rgba_data, loaded.width, loaded.height)
        {
            editor_world.resources.browsers.thumbnail_slots.insert(
                loaded.name,
                crate::ecs::ThumbnailSlot {
                    layer: upload.layer,
                    uv_min: upload.uv_min,
                    uv_max: upload.uv_max,
                    aspect: vec2(upload.content_width as f32, upload.content_height as f32),
                },
            );
        }
    }
}

pub fn poll_browsers(editor_world: &mut EditorWorld, world: &mut World) {
    if let Some(pending) = editor_world
        .resources
        .browsers
        .sample_browser
        .take_pending_glb()
    {
        load_gltf_bytes(editor_world, &pending.display_name, &pending.bytes);
    }

    if let Some(pending) = editor_world
        .resources
        .browsers
        .polyhaven_browser
        .take_pending_hdr()
    {
        let hdr_bytes = pending.bytes.clone();
        load_hdr_skybox(world, pending.bytes);
        world.resources.render_settings.atmosphere = Atmosphere::Hdr;
        crate::scene_writeback::sync_hdr_skybox(
            &mut editor_world.resources.project,
            Some(nightshade::ecs::scene::SceneHdrSkybox::Embedded { data: hdr_bytes }),
        );
        crate::scene_writeback::sync_settings(&mut editor_world.resources.project, world);
        tracing::info!("Loaded Polyhaven HDRI '{}'", pending.display_name);
    }

    if let Some(pending) = editor_world
        .resources
        .browsers
        .polyhaven_browser
        .take_pending_gltf()
    {
        load_gltf_with_resources(editor_world, pending);
    }

    if let Some(pending) = editor_world
        .resources
        .browsers
        .sample_browser
        .take_pending_gltf()
    {
        load_gltf_with_resources(editor_world, pending);
    }

    #[cfg(not(target_arch = "wasm32"))]
    if let Some(pending) = editor_world
        .resources
        .browsers
        .sketchfab_browser
        .take_pending_gltf()
    {
        load_gltf_with_resources(editor_world, pending);
    }

    #[cfg(not(target_arch = "wasm32"))]
    {
        let pending_kenney = editor_world
            .resources
            .browsers
            .kenney_browser
            .drain_pending_glbs();
        for pending in pending_kenney {
            load_gltf_path_named(editor_world, &pending.display_name, &pending.path);
        }
    }
}

pub fn update_top_progress_bar(editor_world: &EditorWorld, world: &mut World) {
    let pipeline = &world.resources.loading.pipeline;
    let pipeline_active = nightshade::ecs::loading::loading_pipeline_is_active(pipeline);
    let pipeline_total = nightshade::ecs::loading::loading_pipeline_total(pipeline);
    let pipeline_completed = nightshade::ecs::loading::loading_pipeline_completed(pipeline);

    let imports_active = !editor_world.resources.loading.pending_imports.is_empty();
    let sample_active = editor_world
        .resources
        .browsers
        .sample_browser
        .loading_status()
        .is_some();
    let polyhaven_active = editor_world
        .resources
        .browsers
        .polyhaven_browser
        .loading_status()
        .is_some();
    #[cfg(not(target_arch = "wasm32"))]
    let kenney_active = editor_world.resources.browsers.kenney_browser.is_busy();
    #[cfg(target_arch = "wasm32")]
    let kenney_active = false;
    #[cfg(not(target_arch = "wasm32"))]
    let sketchfab_active = matches!(
        editor_world.resources.browsers.sketchfab_browser.status(),
        crate::systems::sketchfab::FetchStatus::Working(_)
    );
    #[cfg(target_arch = "wasm32")]
    let sketchfab_active = false;

    let any_active = pipeline_active
        || imports_active
        || sample_active
        || polyhaven_active
        || kenney_active
        || sketchfab_active;

    let bar = &mut world.resources.retained_ui.top_progress_bar;
    if !any_active {
        bar.clear();
        return;
    }
    if pipeline_active && pipeline_total > 0 {
        bar.set((pipeline_completed as f32 / pipeline_total as f32).clamp(0.0, 1.0));
    } else {
        bar.set_indeterminate();
    }
}

pub fn poll_fit_frames(editor_world: &mut EditorWorld, world: &mut World) {
    let frames = editor_world.resources.loading.pending_fit_frames;
    if frames == 0 {
        return;
    }
    let next = frames - 1;
    editor_world.resources.loading.pending_fit_frames = next;
    if next == 0 {
        nightshade::ecs::transform::systems::update_global_transforms_system(world);
        let roots = std::mem::take(&mut editor_world.resources.loading.pending_fit_roots);
        if editor_world.resources.camera.reset_camera_on_load {
            if roots.is_empty() {
                crate::systems::camera::frame_scene(editor_world, world);
            } else {
                crate::systems::camera::frame_entities(editor_world, world, &roots);
            }
        }
    }
}

pub fn clear_loaded_model(editor_world: &mut EditorWorld, world: &mut World) {
    let entities = std::mem::take(&mut editor_world.resources.loading.model_entities);
    for entity in entities {
        let descendants = nightshade::ecs::transform::queries::query_descendants(world, entity);
        crate::scene_writeback::remove_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            entity,
        );
        for descendant in descendants {
            crate::scene_writeback::remove_entity(
                &mut editor_world.resources.project,
                &mut editor_world.resources.editor_scene,
                descendant,
            );
        }
        despawn_recursive_immediate(world, entity);
    }
    mesh_cache_clear(&mut world.resources.assets.mesh_cache);
    world.resources.mesh_render_state.request_full_rebuild();
    crate::systems::selection::clear(editor_world);
    crate::systems::picking::reset_cycle(editor_world);
}

/// Clears every user-spawned scene entity: the loaded glTF tree, dev
/// tool spawns, and any extra camera tiles that were referencing
/// content that may no longer be valid. Cameras (including the
/// default), the sun, lights, the skybox, and other infrastructure
/// stay untouched so the editor remains usable.
pub fn clear_scene(editor_world: &mut EditorWorld, world: &mut World) {
    clear_loaded_model(editor_world, world);
    editor_world.resources.loading.current_model_name = None;
    let dev_entities = std::mem::take(&mut editor_world.resources.loading.dev_tool_entities);
    for entity in dev_entities {
        let descendants = nightshade::ecs::transform::queries::query_descendants(world, entity);
        crate::scene_writeback::remove_entity(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            entity,
        );
        for descendant in descendants {
            crate::scene_writeback::remove_entity(
                &mut editor_world.resources.project,
                &mut editor_world.resources.editor_scene,
                descendant,
            );
        }
        despawn_recursive_immediate(world, entity);
    }
}

pub fn apply_gltf_result(
    editor_world: &mut EditorWorld,
    world: &mut World,
    name: &str,
    mut result: nightshade::ecs::prefab::GltfLoadResult,
) {
    let suggested_exposure = result.suggested_exposure;
    if editor_world.resources.loading.viewer_mode {
        clear_loaded_model(editor_world, world);
    }

    nightshade::ecs::loading::queue_gltf_load(world, &mut result);

    let animations = result.animations.clone();
    let skins = result.skins.clone();
    let prefabs = result.prefabs.clone();
    let single = prefabs.len() == 1;
    let base_name = name.trim_end_matches(".gltf").trim_end_matches(".glb");
    let mut spawned_roots: Vec<Entity> = Vec::with_capacity(prefabs.len());
    for (index, prefab) in prefabs.into_iter().enumerate() {
        let cache_key = if single {
            base_name.to_string()
        } else {
            format!("{base_name}#{index}")
        };
        let cached = nightshade::ecs::prefab::resources::CachedPrefab {
            prefab: prefab.clone(),
            animations: animations.clone(),
            skins: skins.clone(),
            source_path: None,
        };
        nightshade::ecs::prefab::resources::prefab_cache_insert(
            &mut world.resources.assets.prefab_cache,
            cache_key,
            cached,
        );
        let entity = nightshade::ecs::prefab::spawn_prefab_with_skins(
            world,
            &prefab,
            &animations,
            &skins,
            Vec3::zeros(),
        );
        if let Some(player) = world.core.get_animation_player_mut(entity)
            && !player.clips.is_empty()
        {
            player.play(0);
        }
        spawned_roots.push(entity);
        editor_world.resources.loading.model_entities.push(entity);
    }

    for &root in &spawned_roots {
        crate::scene_writeback::register_subtree(
            &mut editor_world.resources.project,
            &mut editor_world.resources.editor_scene,
            world,
            root,
        );
    }
    if !spawned_roots.is_empty() {
        let mut combined_scene = nightshade::ecs::scene::Scene::new("undo_capture");
        let mut root_uuids: Vec<nightshade::ecs::scene::AssetUuid> = Vec::new();
        let mut root_parents: Vec<Option<nightshade::ecs::scene::AssetUuid>> = Vec::new();
        for &root in &spawned_roots {
            let captured =
                crate::undo::capture_subtree(world, &mut editor_world.resources.editor_scene, root);
            for entity in captured.scene.entities {
                combined_scene.add_entity(entity);
            }
            root_uuids.extend(captured.root_uuids);
            root_parents.extend(captured.root_parents);
        }
        nightshade::ecs::scene::embed_referenced_meshes(world, &mut combined_scene);
        nightshade::ecs::scene::embed_referenced_textures(world, &mut combined_scene);
        combined_scene.compute_spawn_order();
        editor_world.resources.undo.push(
            crate::undo::UndoableOperation::EntityCreated {
                captured: Box::new(crate::undo::CapturedSubtree {
                    scene: combined_scene,
                    root_uuids,
                    root_parents,
                }),
            },
            format!("Load {name}"),
        );
    }

    editor_world.resources.loading.loaded = true;
    editor_world.resources.loading.pending_fit_frames = 2;
    editor_world.resources.loading.pending_fit_roots = spawned_roots;
    editor_world.resources.loading.current_model_name = Some(name.to_string());
    editor_world.resources.loading.last_dropped_error = None;

    if editor_world.resources.loading.viewer_mode {
        world.resources.render_settings.color_grading.exposure = suggested_exposure;
        world.resources.render_settings.color_grading.preset = ColorGradingPreset::Custom;
    }

    tracing::info!("Loaded GLTF '{}'", name);
}

fn report_load_error(editor_world: &mut EditorWorld, name: &str, error: String) {
    tracing::error!("Failed to load '{}': {}", name, error);
    editor_world.resources.loading.last_dropped_error = Some(error);
}
