use crate::plugin::utils::Plugin;
use anyhow::Context;
use std::sync::OnceLock;

fn current_desktop() -> &'static str {
    static DESKTOP: OnceLock<String> = OnceLock::new();
    DESKTOP.get_or_init(|| std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_else(|_| "sway".into()))
}

fn locale() -> &'static str {
    static LOCALE: OnceLock<String> = OnceLock::new();
    LOCALE.get_or_init(|| std::env::var("LANG").unwrap_or_else(|_| "en_US".into()))
}

pub struct ApplicationsPlugin {
    entries: Vec<crate::model::Entry>,
}

fn to_entry(
    desktop_entry: &freedesktop_desktop_entry::DesktopEntry,
    terminal_command: Option<String>,
) -> Option<crate::model::Entry> {
    let title = name(desktop_entry);

    if !is_visible(desktop_entry) {
        log::debug!(target: "applications", "Desktop entry with name '{}' will be hidden because of its properties.", title);
        return None;
    }

    let mut cmd: Vec<String> = desktop_entry
        .exec()?
        .split_ascii_whitespace()
        .filter_map(|s| {
            if s.starts_with('%') {
                None
            } else {
                Some(String::from(s))
            }
        })
        .collect();

    if desktop_entry.terminal() {
        if terminal_command.is_none() {
            log::warn!(target: "applications", "Desktop entry with name '{}' will be hidden because no terminal emulator was found to launch it with.", title);
            return None;
        }

        cmd.insert(0, terminal_command.unwrap());
        cmd.insert(1, "-e".into());
    }

    let mut meta = desktop_entry
        .keywords(&[locale()])
        .map(|kws| kws.join(" "))
        .unwrap_or_default();
    meta.push_str(" Applications Apps");

    Some(crate::model::Entry {
        id: desktop_entry.appid.to_string(),
        title,
        action: String::from("open"),
        meta,
        command: Some(cmd),
    })
}

fn is_visible(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> bool {
    if desktop_entry.type_() != Some("Application") {
        return false;
    }

    if desktop_entry.desktop_entry("Hidden") == Some("true") {
        return false;
    }

    if desktop_entry.no_display() {
        return false;
    }

    // filter entries where Exec == false
    if let Some(exec) = desktop_entry.exec()
        && exec.eq_ignore_ascii_case("false")
    {
        return false;
    }

    let desktop = current_desktop();
    // filter entries where NotShowIn == current desktop
    if let Some(not_show_in) = desktop_entry.desktop_entry("NotShowIn")
        && not_show_in
            .split(';')
            .any(|d| d.eq_ignore_ascii_case(desktop))
    {
        return false;
    }

    // filter entries where OnlyShowIn != current desktop
    if let Some(only_show_in) = desktop_entry.only_show_in()
        && !only_show_in.iter().any(|d| d.eq_ignore_ascii_case(desktop))
    {
        return false;
    }

    true
}

fn terminal_command(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> Option<String> {
    if !desktop_entry.categories()?.contains(&"TerminalEmulator") {
        return None;
    }
    desktop_entry
        .exec()?
        .split_ascii_whitespace()
        .next()
        .map(String::from)
}

fn name(desktop_entry: &freedesktop_desktop_entry::DesktopEntry) -> String {
    desktop_entry
        .name(&[locale()])
        .unwrap_or_default()
        .to_string()
}

impl Plugin for ApplicationsPlugin {
    fn new() -> Self {
        Self { entries: vec![] }
    }

    fn id() -> &'static str {
        "applications"
    }

    fn priority() -> u32 {
        29
    }

    fn title() -> &'static str {
        "󰀻 Apps"
    }

    fn entries(&self) -> Vec<crate::model::Entry> {
        self.entries.clone()
    }

    fn set_entries(&mut self, entries: Vec<crate::model::Entry>) {
        self.entries = entries;
    }

    fn update_entries(&mut self) -> anyhow::Result<()> {
        self.entries.clear();

        let paths: Vec<std::path::PathBuf> =
            freedesktop_desktop_entry::Iter::new(freedesktop_desktop_entry::default_paths())
                .collect();

        let bytes_collection: Vec<(&std::path::PathBuf, String)> = paths
            .iter()
            .filter_map(|path| Some((path, std::fs::read_to_string(path).ok()?)))
            .collect();

        let mut desktop_entries: Vec<freedesktop_desktop_entry::DesktopEntry> = bytes_collection
            .iter()
            .filter_map(|(path, content)| {
                freedesktop_desktop_entry::DesktopEntry::from_str(path, content, None::<&[&str]>)
                    .ok()
            })
            .collect();

        desktop_entries.sort_by_key(name);
        desktop_entries.dedup_by_key(|desktop_entry| name(desktop_entry));

        let terminal_command = desktop_entries.iter().find_map(terminal_command);

        self.entries = desktop_entries
            .iter()
            .filter_map(|path| to_entry(path, terminal_command.clone()))
            .collect();

        self.sort();

        Ok(())
    }

    fn activate(
        &mut self,
        entry: crate::model::Entry,
        plugin_channel_out: &mut async_channel::Sender<crate::Message>,
    ) -> anyhow::Result<()> {
        let command = entry.command.context(format!(
            "Failed to unpack command while activating entry with id '{}'.",
            entry.id
        ))?;
        std::process::Command::new(&command[0])
            .args(&command[1..])
            .spawn()?;

        plugin_channel_out
            .try_send(crate::Message::Exit)
            .context(format!(
                "Failed to send message to exit application while activating entry with id '{}'.",
                entry.id
            ))?;

        Ok(())
    }
}
