// Copyright 2024 System76 <info@system76.com>
// Copyright 2024 bbb651 <bar.ye651@gmail.com>
// SPDX-License-Identifier: GPL-3.0-only

use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, dropdown, icon, settings};
use cosmic::{Apply, Element, Task, surface};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::SystemActions;
use cosmic_settings_config::shortcuts::action::System;
use cosmic_settings_page::{self as page, Section, section};
use freedesktop_desktop_entry::{DesktopEntry, default_paths, get_languages_from_env};
use mime::Mime;
use slotmap::SlotMap;
use std::borrow::Cow;
use std::collections::BTreeSet;
use std::io::SeekFrom;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::Arc;
use tokio::io::{AsyncSeekExt, AsyncWriteExt};

const DROPDOWN_WEB_BROWSER: usize = 0;
const DROPDOWN_FILE_MANAGER: usize = 1;
const DROPDOWN_MAIL: usize = 2;
const DROPDOWN_MUSIC: usize = 3;
const DROPDOWN_VIDEO: usize = 4;
const DROPDOWN_PHOTO: usize = 5;
const DROPDOWN_CALENDAR: usize = 6;
const DROPDOWN_TERMINAL: usize = 7;
const DROPDOWN_TEXT_EDITOR: usize = 8;

#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
pub enum Category {
    Audio,
    Calendar,
    FileManager,
    Image,
    Mail,
    Mime(&'static str),
    Terminal,
    Video,
    WebBrowser,
    TextEditor,
}

#[derive(Clone, Debug)]
pub enum Message {
    SetDefault(Category, usize),
    Update(Arc<CachedMimeApps>),
    Surface(surface::Action),
}

impl From<Message> for crate::app::Message {
    fn from(message: Message) -> Self {
        crate::pages::Message::DefaultApps(message).into()
    }
}

impl From<Message> for crate::pages::Message {
    fn from(message: Message) -> Self {
        crate::pages::Message::DefaultApps(message)
    }
}

#[derive(Debug)]
pub struct CachedMimeApps {
    local_list: Option<(mime_apps::List, tokio::fs::File)>,
    apps: Vec<AppMeta>,
    known_mimes: BTreeSet<mime::Mime>,
}

#[derive(Clone, Debug)]
pub struct AppMeta {
    selected: Option<usize>,
    app_ids: Vec<Box<str>>,
    apps: Vec<String>,
    icons: Vec<icon::Handle>,
}

#[derive(Debug, Default)]
pub struct Page {
    on_enter_handle: Option<cosmic::iced::task::Handle>,
    mime_apps: Option<CachedMimeApps>,
    shortcuts_config: Option<cosmic_config::Config>,
    update_config: Option<tokio::task::JoinHandle<()>>,
}

impl page::AutoBind<crate::pages::Message> for Page {}

impl page::Page<crate::pages::Message> for Page {
    fn content(
        &self,
        sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
    ) -> Option<cosmic_settings_page::Content> {
        Some(vec![sections.insert(apps())])
    }

    fn info(&self) -> page::Info {
        page::Info::new("default-apps", "preferences-default-applications-symbolic")
            .title(fl!("default-apps"))
            .description(fl!("xdg-entry-default-apps-comment"))
    }

    fn on_enter(&mut self) -> Task<crate::pages::Message> {
        if let Some(handle) = self.on_enter_handle.take() {
            handle.abort();
        }

        let config_update_handle = self.update_config.take();

        if self.shortcuts_config.is_none() {
            self.shortcuts_config = cosmic_settings_config::shortcuts::context().ok();
        }

        let (task, on_enter_handle) = Task::future(async move {
            // Wait for previous copy operation to complete.
            if let Some(handle) = config_update_handle {
                _ = handle.await;
            }

            let local_list = mime_apps::load_user_mimeapps().await.ok();

            let mut list = mime_apps::List::default();
            list.load_from_paths(&mime_apps::list_paths());
            let assocs = mime_apps::associations::load_from_disk(&list);

            let apps = vec![
                load_defaults(&list, &assocs, &["x-scheme-handler/http"]),
                load_defaults(&list, &assocs, &["inode/directory"]),
                load_defaults(&list, &assocs, &["x-scheme-handler/mailto"]),
                load_defaults(
                    &list,
                    &assocs,
                    &["audio/mpeg", "application/ogg", "audio/x-flac", "video/mp4"],
                ),
                load_defaults(&list, &assocs, &["video/mp4"]),
                load_defaults(&list, &assocs, &["image/png"]),
                load_defaults(&list, &assocs, &["text/calendar"]),
                load_terminal_apps(&list, &assocs).await,
                load_defaults(&list, &assocs, &["text/plain"]),
            ];

            Message::Update(Arc::new(CachedMimeApps {
                apps,
                local_list,
                known_mimes: mime_apps::mime_info::mime_types(),
            }))
            .into()
        })
        .abortable();

        self.on_enter_handle = Some(on_enter_handle);

        task
    }

    fn on_leave(&mut self) -> Task<crate::pages::Message> {
        if let Some(handle) = self.on_enter_handle.take() {
            handle.abort();
        }

        self.mime_apps = None;

        Task::none()
    }
}

impl Page {
    pub fn update(&mut self, message: Message) -> Task<crate::Message> {
        match message {
            Message::SetDefault(category, id) => {
                let Some(mime_apps) = self.mime_apps.as_mut() else {
                    return Task::none();
                };

                let mime_types: Vec<&str>;
                let (category_id, mime_types): (usize, &[&str]) = match category {
                    Category::Audio => (DROPDOWN_MUSIC, {
                        mime_types = mime_apps
                            .known_mimes
                            .iter()
                            .map(|m| m.essence_str())
                            .filter(|m| m.starts_with("audio"))
                            .chain([
                                "application/ogg",
                                "application/x-cue",
                                "application/x-ogg",
                                "audio/mpeg",
                                "audio/mp3",
                                "x-content/audio-cdda",
                            ])
                            .collect();
                        &mime_types
                    }),
                    Category::Calendar => (DROPDOWN_CALENDAR, &["text/calendar"]),
                    Category::FileManager => (DROPDOWN_FILE_MANAGER, &["inode/directory"]),
                    Category::Image => (DROPDOWN_PHOTO, {
                        mime_types = mime_apps
                            .known_mimes
                            .iter()
                            .map(|m| m.essence_str())
                            .filter(|m| m.starts_with("image"))
                            .collect();
                        &mime_types
                    }),
                    Category::Mail => (DROPDOWN_MAIL, &["x-scheme-handler/mailto"]),
                    Category::Terminal => (
                        DROPDOWN_TERMINAL,
                        &[
                            "x-scheme-handler/terminal",
                            "application/x-terminal-emulator",
                        ],
                    ),
                    Category::Video => (DROPDOWN_VIDEO, {
                        mime_types = mime_apps
                            .known_mimes
                            .iter()
                            .map(|m| m.essence_str())
                            .filter(|m| m.starts_with("video"))
                            .collect();
                        &mime_types
                    }),
                    Category::WebBrowser => (
                        DROPDOWN_WEB_BROWSER,
                        &[
                            "text/html",
                            "application/xhtml+xml",
                            "x-scheme-handler/chrome",
                            "x-scheme-handler/http",
                            "x-scheme-handler/https",
                        ],
                    ),
                    Category::TextEditor => (
                        DROPDOWN_TEXT_EDITOR,
                        &["application/x-zerosize", "text/plain"],
                    ),
                    Category::Mime(_mime_type) => return Task::none(),
                };

                let meta = &mut mime_apps.apps[category_id];

                if meta.selected.is_none_or(|selected| selected != id) {
                    meta.selected = Some(id);
                    let appid = &meta.app_ids[id];

                    if category == Category::Terminal
                        && self.shortcuts_config.is_some()
                        && let Some(config) = self.shortcuts_config.as_ref()
                    {
                        assign_default_terminal(config, appid);
                    }

                    if let Some((local_list, local_file)) = mime_apps.local_list.as_mut() {
                        for mime in mime_types {
                            if let Ok(mime) = mime.parse() {
                                tracing::info!(target: "default-apps", ?mime, appid, "setting default for mime");
                                local_list.set_default_app(mime, [appid, ".desktop"].concat());
                            };
                        }

                        if let Some(config_update_handle) = self.update_config.take() {
                            config_update_handle.abort();
                        }

                        let mut buffer = local_list.to_string();
                        buffer.push('\n');

                        if let Ok(mut local_file) =
                            futures::executor::block_on(local_file.try_clone())
                        {
                            self.update_config = Some(tokio::spawn(async move {
                                tracing::debug!(target: "default-apps", buffer, "writing to mimeapps config");
                                _ = local_file.seek(SeekFrom::Start(0)).await;
                                _ = local_file.set_len(buffer.len() as u64).await;
                                _ = local_file.write_all(buffer.as_bytes()).await;
                                _ = local_file.flush().await;
                                _ = local_file.seek(SeekFrom::Start(0)).await;
                                _ = tokio::process::Command::new("update-desktop-database")
                                    .status()
                                    .await;
                            }));
                        }
                    }
                }
            }
            Message::Update(mime_apps) => self.mime_apps = Arc::into_inner(mime_apps),
            Message::Surface(a) => {
                return cosmic::task::message(crate::app::Message::Surface(a));
            }
        }

        Task::none()
    }
}

fn app_item(meta: &AppMeta, label: String, category: Category) -> widget::FlexRow<'_, Message> {
    // Avoid creating popup_dropdown for empty app lists.
    // Empty categories are rendered as non-interactive text to prevent Wayland crashes.
    settings::flex_item(
        label,
        if meta.apps.is_empty() {
            widget::text(fl!("default-apps", "not-installed")).apply(Element::from)
        } else {
            dropdown::popup_dropdown(
                &meta.apps,
                Some(meta.selected.unwrap_or(0)),
                move |id| Message::SetDefault(category, id),
                cosmic::iced::window::Id::RESERVED,
                Message::Surface,
                |a| crate::app::Message::PageMessage(crate::pages::Message::DefaultApps(a)),
            )
            .icons(Cow::Borrowed(&meta.icons))
            .apply(Element::from)
        },
    )
    .align_items(Alignment::Center)
    .width(Length::Shrink)
}

fn apps() -> Section<crate::pages::Message> {
    Section::default().view::<Page>(move |_binder, page, section| {
        let Some(mime_apps) = page.mime_apps.as_ref() else {
            return widget::space().into();
        };

        settings::section::with_capacity(9)
            .title(&section.title)
            .add(app_item(
                &mime_apps.apps[DROPDOWN_WEB_BROWSER],
                fl!("default-apps", "web-browser"),
                Category::WebBrowser,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_FILE_MANAGER],
                fl!("default-apps", "file-manager"),
                Category::FileManager,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_MAIL],
                fl!("default-apps", "mail-client"),
                Category::Mail,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_MUSIC],
                fl!("default-apps", "music"),
                Category::Audio,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_VIDEO],
                fl!("default-apps", "video"),
                Category::Video,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_PHOTO],
                fl!("default-apps", "photos"),
                Category::Image,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_CALENDAR],
                fl!("default-apps", "calendar"),
                Category::Calendar,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_TERMINAL],
                fl!("default-apps", "terminal"),
                Category::Terminal,
            ))
            .add(app_item(
                &mime_apps.apps[DROPDOWN_TEXT_EDITOR],
                fl!("default-apps", "text-editor"),
                Category::TextEditor,
            ))
            .apply(Element::from)
            .map(crate::pages::Message::DefaultApps)
    })
}

fn assign_default_terminal(config: &cosmic_config::Config, appid: &str) {
    let mut actions = config
        .get_local::<SystemActions>("system_actions")
        .unwrap_or_default();

    let default_paths = default_paths();
    let mut resolved_path = None;

    // loop through all FDE paths to try and find a valid .desktop file
    for path in default_paths {
        if let Ok(mut full_path) = path.canonicalize() {
            full_path = full_path.join([appid, ".desktop"].concat());
            if full_path.exists() && full_path.is_file() {
                resolved_path = Some(full_path);
                break;
            }
        }
    }

    // if we find a valid .desktop file, we can grab its exec
    if let Some(resolved_path) = resolved_path {
        let desktop_entry = DesktopEntry::from_path(resolved_path, Some(&get_languages_from_env()));

        if let Ok(desktop_entry) = desktop_entry
            && let Some(exec) = desktop_entry.exec()
        {
            actions.insert(System::Terminal, String::from(exec));

            if let Err(why) = config.set("system_actions", actions) {
                tracing::error!(?why, "Unable to set system_actions shortcuts config");
            }
        }
    }
}

fn load_defaults(
    list: &mime_apps::List,
    assocs: &mime_apps::Associations,
    for_mimes: &[&str],
) -> AppMeta {
    let mut unsorted = Vec::new();
    let mut default_app = None;

    for for_mime in for_mimes {
        let Ok(mime) = Mime::from_str(for_mime) else {
            continue;
        };

        for default in list.default_app_for(&mime).into_iter().flatten() {
            let appid = default.strip_suffix(".desktop").unwrap_or(default.as_ref());

            tracing::info!(target: "default-apps", ?mime, appid, "entry for mime");
            if default_app.is_none() {
                default_app = assocs.by_app_id.get(appid);
                if default_app.is_some() {
                    break;
                }
            }
        }

        for (appid, app) in mime_apps::apps_for_mime(&mime, assocs) {
            if app.icon.is_none() {
                continue;
            }

            if unsorted.iter().any(|(id, _)| id == appid) {
                continue;
            }

            unsorted.push((appid.clone(), app.clone()));
        }
    }

    unsorted.sort_unstable_by_key(|(_, app)| app.name.clone());

    let mut selected = None;
    let mut app_ids = Vec::new();
    let mut apps = Vec::new();
    let mut icons = Vec::new();

    let mut id = 0;
    for (appid, app) in unsorted.iter() {
        let Some(icon) = app.icon.as_deref() else {
            continue;
        };
        if let Some(default_app) = default_app
            && app.name.as_ref() == default_app.name.as_ref()
        {
            selected = Some(id);
        }

        app_ids.push(appid.as_ref().into());
        apps.push(app.name.as_ref().into());
        icons.push(if icon.starts_with('/') {
            icon::from_path(PathBuf::from(icon))
        } else {
            icon::from_name(icon).size(20).handle()
        });

        id += 1;
    }

    AppMeta {
        selected,
        app_ids,
        apps,
        icons,
    }
}

async fn load_terminal_apps(list: &mime_apps::List, assocs: &mime_apps::Associations) -> AppMeta {
    let mut selected = None;
    let mut app_ids = Vec::new();
    let mut apps = Vec::new();
    let mut icons = Vec::new();

    let mut terminals: Vec<_> = assocs
        .apps
        .iter()
        .filter(|app| app.is_terminal && app.icon.is_some())
        .collect();

    terminals.sort_unstable_by_key(|app| app.name.clone());

    'outer: for for_mime in [
        "x-scheme-handler/terminal",
        "application/x-terminal-emulator",
    ] {
        let Ok(mime) = Mime::from_str(for_mime) else {
            continue;
        };

        for default in list.default_app_for(&mime).into_iter().flatten() {
            let appid = default.strip_suffix(".desktop").unwrap_or(default.as_ref());

            for (id, app) in terminals.iter().enumerate() {
                if app.appid.as_ref() == appid {
                    selected = Some(id);
                    break 'outer;
                }
            }
        }
    }

    for (id, app) in terminals.into_iter().enumerate() {
        let Some(icon) = app.icon.as_deref() else {
            continue;
        };

        if selected.is_none() && app.appid.as_ref() == "com.system76.CosmicTerm" {
            selected = Some(id);
        }

        app_ids.push(app.appid.clone());
        apps.push(app.name.as_ref().into());
        icons.push(if icon.starts_with('/') {
            icon::from_path(PathBuf::from(icon))
        } else {
            icon::from_name(icon).size(20).handle()
        });
    }

    AppMeta {
        selected,
        app_ids,
        apps,
        icons,
    }
}
