// SPDX-FileCopyrightText: 2025 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Uwe Scholz <u.scholz83@gmx.de>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::{
    config::PIXMAPS_DIR, debug::debug, file::File, options::GeneralOptions,
    types::GraphicalLayoutMode,
};
use gtk::{gio, glib, prelude::*};
use std::{cell::RefCell, collections::HashMap, path::Path, rc::Rc, sync::LazyLock};

pub struct IconCache {
    file_type_icons: HashMap<gio::FileType, gio::Icon>,
    symlink: gio::ThemedIcon,
    options: GeneralOptions,
    cache: RefCell<HashMap<IconCacheKey, Option<gio::Icon>>>,
}

#[derive(PartialEq, Eq, Hash)]
struct IconCacheKey {
    mime_type: Option<String>,
    file_type: gio::FileType,
}

impl IconCache {
    fn new() -> Rc<Self> {
        let mut file_type_icons = HashMap::new();
        let file_icons_dir = Path::new(PIXMAPS_DIR).join("file-type-icons");
        if let Some(icon) = load_icon(&file_icons_dir.join("file_type_regular.png")) {
            file_type_icons.insert(gio::FileType::Unknown, icon.clone());
            file_type_icons.insert(gio::FileType::Regular, icon);
        }
        if let Some(icon) = load_icon(&file_icons_dir.join("file_type_dir.png")) {
            file_type_icons.insert(gio::FileType::Directory, icon.clone());
            file_type_icons.insert(gio::FileType::Shortcut, icon.clone());
            file_type_icons.insert(gio::FileType::Mountable, icon);
        }
        if let Some(icon) = load_icon(&file_icons_dir.join("file_type_symlink.png")) {
            file_type_icons.insert(gio::FileType::SymbolicLink, icon);
        }
        if let Some(icon) = load_icon(&file_icons_dir.join("file_type_socket.png")) {
            file_type_icons.insert(gio::FileType::Special, icon);
        }

        let options = GeneralOptions::new();

        let this = Rc::new(Self {
            file_type_icons,
            symlink: gio::ThemedIcon::new("overlay_symlink"),
            options,
            cache: Default::default(),
        });

        this.options.mime_icon_dir.connect_changed(glib::clone!(
            #[weak]
            this,
            move |_| {
                this.cache.borrow_mut().clear();
            }
        ));

        this
    }

    pub fn mime_type_icon(&self, file_type: gio::FileType, mime_type: &str) -> Option<gio::Icon> {
        let cache_key = IconCacheKey {
            mime_type: Some(mime_type.to_owned()),
            file_type,
        };
        self.cache
            .borrow_mut()
            .entry(cache_key)
            .or_insert_with(|| {
                self.options
                    .mime_icon_dir
                    .get()
                    .and_then(|theme_icon_dir| {
                        self.mime_icon_in_dir(&theme_icon_dir, file_type, mime_type)
                    })
                    .or_else(|| {
                        let default_dir = Path::new(PIXMAPS_DIR).join("mime-icons");
                        self.mime_icon_in_dir(&default_dir, file_type, mime_type)
                    })
            })
            .clone()
    }

    pub fn file_type_icon(&self, file_type: gio::FileType) -> Option<gio::Icon> {
        let cache_key = IconCacheKey {
            mime_type: None,
            file_type,
        };
        self.cache
            .borrow_mut()
            .entry(cache_key)
            .or_insert_with(|| self.file_type_icons.get(&file_type).cloned())
            .clone()
    }

    /// Tries to load an image for the specified mime-type in the specified directory.
    fn mime_icon_in_dir(
        &self,
        icon_dir: &Path,
        file_type: gio::FileType,
        mime_type: &str,
    ) -> Option<gio::Icon> {
        if file_type == gio::FileType::SymbolicLink {
            return None;
        }

        debug!('y', "Looking up pixmap for: {mime_type}");
        let icon = find_file_icon(icon_dir, file_type, mime_type);
        debug!('z', "Icon for {} found: {}", mime_type, icon.is_some());

        icon
    }

    pub fn file_icon(&self, file: &File, mode: GraphicalLayoutMode) -> Option<gio::Icon> {
        let file_type = file.file_type();

        match mode {
            GraphicalLayoutMode::MimeIcons => file
                .content_type()
                .and_then(|mime_type| self.mime_type_icon(file_type, &mime_type))
                .or_else(|| self.file_type_icon(file_type)),
            GraphicalLayoutMode::TypeIcons => self.file_type_icon(file_type),
            GraphicalLayoutMode::Text => None,
        }
    }

    pub fn symlink_overlay(&self) -> &gio::Icon {
        self.symlink.upcast_ref()
    }
}

/// Looks in the directory for any icon matching the MIME type. If not found will fall back to
/// generic icons.
fn find_file_icon(icon_dir: &Path, file_type: gio::FileType, mime_type: &str) -> Option<gio::Icon> {
    const EXTENSIONS: &[&str] = &["png", "svg"];
    const PREFIXES: &[&str] = &["", "gnome-"];

    for ext in EXTENSIONS {
        for prefix in PREFIXES {
            let file_name = format!("{prefix}{}.{ext}", mime_type.replace('/', "-"));
            let icon = try_load_icon(&icon_dir.join(file_name));
            if icon.is_some() {
                return icon;
            }
        }
    }

    if let Some(category_name) = category_icon_path(mime_type) {
        for ext in EXTENSIONS {
            let file_name = format!("{category_name}.{ext}");
            let icon = try_load_icon(&icon_dir.join(file_name));
            if icon.is_some() {
                return icon;
            }
        }
    }

    let type_name = type_icon_name(file_type);
    for ext in EXTENSIONS {
        let file_name = format!("{type_name}.{ext}");
        let icon = try_load_icon(&icon_dir.join(file_name));
        if icon.is_some() {
            return icon;
        }
    }

    None
}

fn try_load_icon(path: &Path) -> Option<gio::Icon> {
    debug!('z', "Trying {}", path.display());
    if path.exists() {
        Some(gio::FileIcon::new(&gio::File::for_path(path)).upcast())
    } else {
        None
    }
}

fn load_icon(path: &Path) -> Option<gio::Icon> {
    debug!('i', "imageloader: loading pixmap: {}", path.display());
    try_load_icon(path).or_else(|| {
        eprintln!("Cannot load icon {}. File wasn't found.", path.display());
        None
    })
}

/// Returns the file name that an image representing the given filetype should have.
fn type_icon_name(file_type: gio::FileType) -> &'static str {
    match file_type {
        gio::FileType::Directory | gio::FileType::Shortcut | gio::FileType::Mountable => {
            "i-directory"
        }
        gio::FileType::Regular => "i-regular",
        gio::FileType::SymbolicLink => "i-symlink",
        _ => "i-regular",
    }
}

/// Returns the file name of the image representing the category of the given
/// mime-type. This is a hack to avoid having 20 equal
/// icons for 20 different video formats etc.
fn category_icon_path(mime_type: &str) -> Option<&str> {
    if mime_type.starts_with("text") {
        Some("gnome-text-plain")
    } else if mime_type.starts_with("video") {
        Some("gnome-video-plain")
    } else if mime_type.starts_with("image") {
        Some("gnome-image-plain")
    } else if mime_type.starts_with("audio") {
        Some("gnome-audio-plain")
    } else if mime_type.starts_with("pack") {
        Some("gnome-pack-plain")
    } else if mime_type.starts_with("font") {
        Some("gnome-font-plain")
    } else {
        None
    }
}

pub fn icon_cache() -> Rc<IconCache> {
    static ICON_CACHE: LazyLock<glib::thread_guard::ThreadGuard<Rc<IconCache>>> =
        LazyLock::new(|| glib::thread_guard::ThreadGuard::new(IconCache::new()));
    ICON_CACHE.get_ref().clone()
}
