#[cfg(not(target_arch = "wasm32"))]
use std::path::{Path, PathBuf};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::atomic::{AtomicU64, Ordering};
#[cfg(not(target_arch = "wasm32"))]
use std::sync::{Arc, Mutex};

#[cfg(not(target_arch = "wasm32"))]
#[derive(Debug, Clone)]
pub struct KenneyEntry {
    pub category: String,
    pub pack: String,
    pub display_name: String,
    pub asset_name: String,
    pub glb_path: PathBuf,
    pub preview_path: Option<PathBuf>,
}

#[cfg(not(target_arch = "wasm32"))]
pub struct PendingGlb {
    pub display_name: String,
    pub path: PathBuf,
}

#[cfg(not(target_arch = "wasm32"))]
pub enum ScanState {
    Idle,
    Scanning(String),
    Loaded(Arc<Vec<KenneyEntry>>),
    Failed(String),
}

#[cfg(not(target_arch = "wasm32"))]
pub struct KenneyBrowser {
    state: Arc<Mutex<ScanState>>,
    root: Arc<Mutex<Option<PathBuf>>>,
    pending_glbs: Arc<Mutex<Vec<PendingGlb>>>,
    queued_thumbs: Arc<Mutex<bool>>,
    pub generation: Arc<AtomicU64>,
    dialog_in_flight: Arc<Mutex<bool>>,
}

#[cfg(not(target_arch = "wasm32"))]
impl Default for KenneyBrowser {
    fn default() -> Self {
        Self {
            state: Arc::new(Mutex::new(ScanState::Idle)),
            root: Arc::new(Mutex::new(None)),
            pending_glbs: Arc::new(Mutex::new(Vec::new())),
            queued_thumbs: Arc::new(Mutex::new(false)),
            generation: Arc::new(AtomicU64::new(0)),
            dialog_in_flight: Arc::new(Mutex::new(false)),
        }
    }
}

#[cfg(not(target_arch = "wasm32"))]
impl KenneyBrowser {
    pub fn root(&self) -> Option<PathBuf> {
        self.root.lock().ok().and_then(|guard| guard.clone())
    }

    pub fn entries(&self) -> Option<Arc<Vec<KenneyEntry>>> {
        let guard = self.state.lock().ok()?;
        if let ScanState::Loaded(entries) = &*guard {
            Some(Arc::clone(entries))
        } else {
            None
        }
    }

    pub fn status_text(&self) -> String {
        let guard = match self.state.lock() {
            Ok(guard) => guard,
            Err(_) => return "scan state unavailable".to_string(),
        };
        match &*guard {
            ScanState::Idle => "Pick a Kenney directory or .zip to begin".to_string(),
            ScanState::Scanning(message) => format!("Scanning: {message}"),
            ScanState::Loaded(entries) => {
                let pack_count = entries
                    .iter()
                    .map(|entry| (&entry.category, &entry.pack))
                    .collect::<std::collections::BTreeSet<_>>()
                    .len();
                format!("{} models across {pack_count} packs", entries.len())
            }
            ScanState::Failed(message) => format!("failed: {message}"),
        }
    }

    pub fn is_scanning(&self) -> bool {
        matches!(&*self.state.lock().unwrap(), ScanState::Scanning(_))
    }

    pub fn is_busy(&self) -> bool {
        self.is_scanning() || *self.dialog_in_flight.lock().unwrap()
    }

    pub fn drain_pending_glbs(&self) -> Vec<PendingGlb> {
        self.pending_glbs
            .lock()
            .map(|mut queue| std::mem::take(&mut *queue))
            .unwrap_or_default()
    }

    pub fn pick_directory(&self) {
        if self.is_scanning() {
            return;
        }
        if let Ok(mut flag) = self.dialog_in_flight.lock() {
            if *flag {
                return;
            }
            *flag = true;
        }
        let dialog_flag = Arc::clone(&self.dialog_in_flight);
        let root_slot = Arc::clone(&self.root);
        let queued_flag = Arc::clone(&self.queued_thumbs);
        let state_slot = Arc::clone(&self.state);
        let generation = Arc::clone(&self.generation);
        std::thread::spawn(move || {
            let picked = nightshade::prelude::rfd::FileDialog::new()
                .set_title("Select Kenney assets root")
                .pick_folder();
            if let Ok(mut flag) = dialog_flag.lock() {
                *flag = false;
            }
            if let Some(path) = picked {
                begin_root(path, &root_slot, &queued_flag, &state_slot, &generation);
            }
        });
    }

    pub fn pick_zip(&self) {
        if self.is_scanning() {
            return;
        }
        if let Ok(mut flag) = self.dialog_in_flight.lock() {
            if *flag {
                return;
            }
            *flag = true;
        }
        let dialog_flag = Arc::clone(&self.dialog_in_flight);
        let root_slot = Arc::clone(&self.root);
        let queued_flag = Arc::clone(&self.queued_thumbs);
        let state_slot = Arc::clone(&self.state);
        let generation = Arc::clone(&self.generation);
        std::thread::spawn(move || {
            let picked = nightshade::prelude::rfd::FileDialog::new()
                .set_title("Select Kenney .zip archive")
                .add_filter("Zip archive", &["zip"])
                .pick_file();
            if let Ok(mut flag) = dialog_flag.lock() {
                *flag = false;
            }
            if let Some(zip_path) = picked {
                begin_zip(zip_path, &root_slot, &queued_flag, &state_slot, &generation);
            }
        });
    }

    pub fn queue_thumbnails(&self, queue: &nightshade::prelude::SharedTextureQueue) {
        let Some(entries) = self.entries() else {
            return;
        };
        let mut already_queued = match self.queued_thumbs.lock() {
            Ok(guard) => guard,
            Err(_) => return,
        };
        if *already_queued {
            return;
        }
        let Ok(mut q) = queue.lock() else {
            return;
        };
        for entry in entries.iter() {
            let Some(preview) = entry.preview_path.as_ref() else {
                continue;
            };
            let name = format!("kenney_thumb_{}", entry.asset_name);
            q.queue_texture(name, preview.to_string_lossy().to_string());
        }
        *already_queued = true;
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn begin_root(
    path: PathBuf,
    root_slot: &Arc<Mutex<Option<PathBuf>>>,
    queued_flag: &Arc<Mutex<bool>>,
    state_slot: &Arc<Mutex<ScanState>>,
    generation: &Arc<AtomicU64>,
) {
    let token = generation.fetch_add(1, Ordering::Relaxed) + 1;
    if let Ok(mut slot) = root_slot.lock() {
        *slot = Some(path.clone());
    }
    if let Ok(mut flag) = queued_flag.lock() {
        *flag = false;
    }
    start_scan(path, state_slot, generation, token);
}

#[cfg(not(target_arch = "wasm32"))]
fn begin_zip(
    zip_path: PathBuf,
    root_slot: &Arc<Mutex<Option<PathBuf>>>,
    queued_flag: &Arc<Mutex<bool>>,
    state_slot: &Arc<Mutex<ScanState>>,
    generation: &Arc<AtomicU64>,
) {
    let token = generation.fetch_add(1, Ordering::Relaxed) + 1;
    let display = zip_path
        .file_name()
        .map(|name| name.to_string_lossy().to_string())
        .unwrap_or_else(|| zip_path.display().to_string());
    if let Ok(mut slot) = state_slot.lock() {
        *slot = ScanState::Scanning(format!("extracting {display}"));
    }
    let state_clone = Arc::clone(state_slot);
    let root_clone = Arc::clone(root_slot);
    let queued_clone = Arc::clone(queued_flag);
    let generation_clone = Arc::clone(generation);
    std::thread::spawn(move || match extract_zip(&zip_path) {
        Ok(extracted_root) => {
            if generation_clone.load(Ordering::Relaxed) != token {
                return;
            }
            if let Ok(mut slot) = root_clone.lock() {
                *slot = Some(extracted_root.clone());
            }
            if let Ok(mut flag) = queued_clone.lock() {
                *flag = false;
            }
            run_scan(&extracted_root, &state_clone, &generation_clone, token);
        }
        Err(error) => {
            if generation_clone.load(Ordering::Relaxed) != token {
                return;
            }
            if let Ok(mut slot) = state_clone.lock() {
                *slot = ScanState::Failed(error);
            }
        }
    });
}

#[cfg(not(target_arch = "wasm32"))]
fn start_scan(
    root: PathBuf,
    state_slot: &Arc<Mutex<ScanState>>,
    generation: &Arc<AtomicU64>,
    token: u64,
) {
    let display = root
        .file_name()
        .map(|name| name.to_string_lossy().to_string())
        .unwrap_or_else(|| root.display().to_string());
    if let Ok(mut slot) = state_slot.lock() {
        *slot = ScanState::Scanning(format!("scanning {display}"));
    }
    let target = Arc::clone(state_slot);
    let generation_clone = Arc::clone(generation);
    std::thread::spawn(move || {
        run_scan(&root, &target, &generation_clone, token);
    });
}

#[cfg(not(target_arch = "wasm32"))]
fn run_scan(root: &Path, state: &Arc<Mutex<ScanState>>, generation: &Arc<AtomicU64>, token: u64) {
    let entries = match collect_entries(root) {
        Ok(entries) => entries,
        Err(error) => {
            if generation.load(Ordering::Relaxed) != token {
                return;
            }
            if let Ok(mut slot) = state.lock() {
                *slot = ScanState::Failed(error);
            }
            return;
        }
    };
    if generation.load(Ordering::Relaxed) != token {
        return;
    }
    if entries.is_empty() {
        if let Ok(mut slot) = state.lock() {
            *slot = ScanState::Failed("no GLB models found at the selected location".to_string());
        }
        return;
    }
    if let Ok(mut slot) = state.lock() {
        *slot = ScanState::Loaded(Arc::new(entries));
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn collect_entries(root: &Path) -> Result<Vec<KenneyEntry>, String> {
    if !root.is_dir() {
        return Err(format!("not a directory: {}", root.display()));
    }
    let mut entries = Vec::new();
    if root.join("Models").join("GLB format").is_dir() {
        let pack_name = root
            .file_name()
            .and_then(|name| name.to_str())
            .unwrap_or("Pack")
            .to_string();
        collect_pack("", &pack_name, root, &mut entries);
    } else {
        let category_roots = candidate_category_roots(root);
        for (category, category_path) in category_roots {
            let pack_iter = match std::fs::read_dir(&category_path) {
                Ok(iter) => iter,
                Err(_) => continue,
            };
            for pack_entry in pack_iter.flatten() {
                let pack_path = pack_entry.path();
                if !pack_path.is_dir() {
                    continue;
                }
                let pack_name = pack_path
                    .file_name()
                    .and_then(|name| name.to_str())
                    .unwrap_or("")
                    .to_string();
                if pack_name.is_empty() {
                    continue;
                }
                collect_pack(&category, &pack_name, &pack_path, &mut entries);
            }
        }
    }
    entries.sort_by(|left, right| {
        left.category
            .cmp(&right.category)
            .then(left.pack.cmp(&right.pack))
            .then(left.asset_name.cmp(&right.asset_name))
    });
    Ok(entries)
}

#[cfg(not(target_arch = "wasm32"))]
fn candidate_category_roots(root: &Path) -> Vec<(String, PathBuf)> {
    let three_d = root.join("3D assets");
    if three_d.is_dir() {
        return vec![("3D assets".to_string(), three_d)];
    }
    let mut categories = Vec::new();
    let mut treat_root_as_category = false;
    if let Ok(iter) = std::fs::read_dir(root) {
        for entry in iter.flatten() {
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }
            if path.join("Models").join("GLB format").is_dir() {
                treat_root_as_category = true;
                break;
            }
        }
    }
    if treat_root_as_category {
        categories.push(("".to_string(), root.to_path_buf()));
        return categories;
    }
    if let Ok(iter) = std::fs::read_dir(root) {
        for entry in iter.flatten() {
            let path = entry.path();
            if !path.is_dir() {
                continue;
            }
            let name = path
                .file_name()
                .and_then(|name| name.to_str())
                .unwrap_or("")
                .to_string();
            if name.is_empty() {
                continue;
            }
            categories.push((name, path));
        }
    }
    categories
}

#[cfg(not(target_arch = "wasm32"))]
fn collect_pack(category: &str, pack: &str, pack_path: &Path, entries: &mut Vec<KenneyEntry>) {
    let glb_dir = pack_path.join("Models").join("GLB format");
    if !glb_dir.is_dir() {
        return;
    }
    let preview_dir = pack_path.join("Previews");
    let glb_iter = match std::fs::read_dir(&glb_dir) {
        Ok(iter) => iter,
        Err(_) => return,
    };
    for glb_entry in glb_iter.flatten() {
        let glb_path = glb_entry.path();
        if !glb_path.is_file() {
            continue;
        }
        let extension = glb_path
            .extension()
            .and_then(|ext| ext.to_str())
            .unwrap_or("");
        if !extension.eq_ignore_ascii_case("glb") {
            continue;
        }
        let stem = glb_path
            .file_stem()
            .and_then(|name| name.to_str())
            .unwrap_or("")
            .to_string();
        if stem.is_empty() {
            continue;
        }
        let preview_path = if preview_dir.is_dir() {
            let candidate = preview_dir.join(format!("{stem}.png"));
            if candidate.is_file() {
                Some(candidate)
            } else {
                None
            }
        } else {
            None
        };
        let asset_name = format!("{category}::{pack}::{stem}");
        let display_name = format!("{pack} / {stem}");
        entries.push(KenneyEntry {
            category: category.to_string(),
            pack: pack.to_string(),
            display_name,
            asset_name,
            glb_path,
            preview_path,
        });
    }
}

#[cfg(not(target_arch = "wasm32"))]
pub fn load_entry(browser: &KenneyBrowser, entry: &KenneyEntry) {
    if let Ok(mut queue) = browser.pending_glbs.lock() {
        queue.push(PendingGlb {
            display_name: entry.display_name.clone(),
            path: entry.glb_path.clone(),
        });
    }
}

#[cfg(not(target_arch = "wasm32"))]
fn extract_zip(zip_path: &Path) -> Result<PathBuf, String> {
    use std::io::Read;

    let bytes = std::fs::read(zip_path).map_err(|error| error.to_string())?;
    let cursor = std::io::Cursor::new(bytes);
    let mut archive = zip::ZipArchive::new(cursor).map_err(|error| error.to_string())?;

    let stem = zip_path
        .file_stem()
        .and_then(|name| name.to_str())
        .unwrap_or("kenney")
        .to_string();
    let cache_root = std::env::temp_dir()
        .join("nightshade-editor")
        .join("kenney")
        .join(stem);
    std::fs::create_dir_all(&cache_root).map_err(|error| error.to_string())?;

    for index in 0..archive.len() {
        let mut entry = archive.by_index(index).map_err(|error| error.to_string())?;
        let raw_name = entry.name().to_string();
        let relative = raw_name.replace('\\', "/");
        if relative.contains("..") || relative.starts_with('/') {
            continue;
        }
        let out_path = cache_root.join(&relative);
        if entry.is_dir() {
            std::fs::create_dir_all(&out_path).map_err(|error| error.to_string())?;
            continue;
        }
        if let Some(parent) = out_path.parent() {
            std::fs::create_dir_all(parent).map_err(|error| error.to_string())?;
        }
        if out_path.exists()
            && let Ok(metadata) = std::fs::metadata(&out_path)
            && metadata.len() == entry.size()
        {
            continue;
        }
        let mut buffer = Vec::with_capacity(entry.size() as usize);
        entry
            .read_to_end(&mut buffer)
            .map_err(|error| error.to_string())?;
        std::fs::write(&out_path, &buffer).map_err(|error| error.to_string())?;
    }

    let nested_root = detect_root(&cache_root);
    Ok(nested_root.unwrap_or(cache_root))
}

#[cfg(not(target_arch = "wasm32"))]
fn detect_root(start: &Path) -> Option<PathBuf> {
    if start.join("3D assets").is_dir() {
        return Some(start.to_path_buf());
    }
    let iter = std::fs::read_dir(start).ok()?;
    for entry in iter.flatten() {
        let path = entry.path();
        if path.is_dir() && path.join("3D assets").is_dir() {
            return Some(path);
        }
    }
    None
}
