// SPDX-FileCopyrightText: 2024 Andrey Kutejko <andy128k@gmail.com>
// SPDX-FileCopyrightText: 2026 Wladimir Palant https://palant.info/
//
// SPDX-License-Identifier: GPL-3.0-or-later

use crate::{
    debug::debug,
    file::FileOps,
    options::{GeneralOptions, ProgramsOptions, types::WriteResult},
    spawn::{parse_command_template, spawn_async_command},
    utils::{ErrorMessage, make_run_in_terminal_command, u32_enum},
};
use gettextrs::gettext;
use gtk::{gdk, gio, glib, prelude::*};
use std::{borrow::Cow, ffi::OsString, path::PathBuf};

u32_enum! {
    pub enum AppTarget {
        #[default]
        AllFiles,
        AllDirs,
        AllDirsAndFiles,
        SomeFiles,
    }
}

pub trait AppExt {
    fn name(&self) -> String;
    fn icon(&self) -> Option<gio::Icon>;
}

#[derive(Clone, Debug)]
pub struct RegularApp {
    pub app_info: gio::AppInfo,
}

impl AppExt for RegularApp {
    #[inline]
    fn name(&self) -> String {
        self.app_info.name().into()
    }

    #[inline]
    fn icon(&self) -> Option<gio::Icon> {
        self.app_info.icon()
    }
}

impl RegularApp {
    pub fn launch(&self, files: &[impl FileOps]) -> Result<(), ErrorMessage> {
        let files: Vec<_> = files.iter().map(|f| f.file().clone()).collect();
        debug!('g', "Launching {:?}", self.app_info.commandline());
        self.app_info
            .launch(
                &files,
                gdk::Display::default()
                    .map(|display| display.app_launch_context())
                    .as_ref(),
            )
            .map_err(|error| {
                ErrorMessage::with_error(
                    gettext("Launch of {} failed.").replace("{}", &self.app_info.name()),
                    &error,
                )
            })
    }
}

impl glib::variant::StaticVariantType for RegularApp {
    fn static_variant_type() -> Cow<'static, glib::VariantTy> {
        Cow::Borrowed(glib::VariantTy::STRING)
    }
}

impl glib::variant::ToVariant for RegularApp {
    fn to_variant(&self) -> glib::Variant {
        self.app_info
            .id()
            .map(|app_id| app_id.to_variant())
            .unwrap_or_else(|| "".to_variant())
    }
}

impl From<RegularApp> for glib::Variant {
    fn from(value: RegularApp) -> glib::Variant {
        glib::variant::ToVariant::to_variant(&value)
    }
}

impl glib::variant::FromVariant for RegularApp {
    fn from_variant(variant: &glib::Variant) -> Option<Self> {
        let app_id = variant.str()?;
        let app_info = gio::AppInfo::all()
            .into_iter()
            .find(|a| a.id().as_deref() == Some(app_id))?;
        Some(RegularApp { app_info })
    }
}

#[derive(Clone, Debug, glib::Variant)]
pub struct UserDefinedApp {
    pub name: String,
    pub command_template: String,
    pub icon_path: Option<PathBuf>,
    pub target: AppTarget,
    pub pattern_string: String,
    pub handles_uris: bool,
    pub handles_multiple: bool,
    pub requires_terminal: bool,
}

impl AppExt for UserDefinedApp {
    fn name(&self) -> String {
        self.name.clone()
    }

    fn icon(&self) -> Option<gio::Icon> {
        Some(gio::FileIcon::new(&gio::File::for_path(self.icon_path.as_ref()?)).upcast())
    }
}

impl UserDefinedApp {
    pub fn pattern_list(&self) -> Vec<&str> {
        self.pattern_string.split(';').collect()
    }

    pub fn build_command_line(
        &self,
        files: &[impl FileOps],
        options: &ProgramsOptions,
    ) -> Option<OsString> {
        let mut commandline = parse_command_template(files, &self.command_template)?;
        if self.requires_terminal {
            commandline = make_run_in_terminal_command(&commandline, options);
        }
        Some(commandline)
    }

    pub fn launch(
        &self,
        files: &[impl FileOps],
        options: &ProgramsOptions,
    ) -> Result<(), ErrorMessage> {
        let working_directory: Option<PathBuf> = files
            .first()
            .ok_or_else(|| ErrorMessage {
                message: gettext("Cannot launch an app {}. No files were given.")
                    .replace("{}", &self.name),
                secondary_text: None,
            })?
            .parent_path();

        let command = self
            .build_command_line(files, options)
            .ok_or_else(|| ErrorMessage {
                message: gettext("Cannot build a command line."),
                secondary_text: None,
            })?;

        spawn_async_command(working_directory.as_deref(), &command)
            .map_err(|e| e.into_message())?;
        Ok(())
    }
}

#[derive(Clone, Debug, glib::Variant)]
pub enum App {
    Regular(RegularApp),
    UserDefined(UserDefinedApp),
}

impl App {
    pub fn name(&self) -> String {
        match self {
            App::Regular(app) => app.name(),
            App::UserDefined(app) => app.name(),
        }
    }

    pub fn command(&self) -> Option<String> {
        match self {
            App::Regular(app) => Some(app.app_info.commandline()?.to_str()?.to_owned()),
            App::UserDefined(app) => Some(app.command_template.clone()),
        }
    }

    pub fn icon(&self) -> Option<gio::Icon> {
        match self {
            App::Regular(app) => app.icon(),
            App::UserDefined(app) => app.icon(),
        }
    }

    pub fn target(&self) -> AppTarget {
        match self {
            App::Regular(_) => AppTarget::AllFiles,
            App::UserDefined(app) => app.target,
        }
    }

    pub fn pattern_string(&self) -> &str {
        match self {
            App::Regular(..) => "",
            App::UserDefined(app) => &app.pattern_string,
        }
    }

    pub fn pattern_list(&self) -> Vec<&str> {
        match self {
            App::Regular(..) => Vec::new(),
            App::UserDefined(app) => app.pattern_list(),
        }
    }

    pub fn handles_uris(&self) -> bool {
        match self {
            App::Regular(app) => app.app_info.supports_uris(),
            App::UserDefined(app) => app.handles_uris,
        }
    }

    pub fn handles_multiple(&self) -> bool {
        match self {
            App::Regular(..) => true,
            App::UserDefined(app) => app.handles_multiple,
        }
    }

    pub fn requires_terminal(&self) -> bool {
        match self {
            App::Regular(..) => false,
            App::UserDefined(app) => app.requires_terminal,
        }
    }

    pub fn launch(
        &self,
        files: &[impl FileOps],
        options: &ProgramsOptions,
    ) -> Result<(), ErrorMessage> {
        match self {
            App::Regular(app) => app.launch(files),
            App::UserDefined(app) => app.launch(files, options),
        }
    }
}

#[derive(glib::Variant)]
pub struct FavoriteAppVariant {
    pub name: String,
    pub command_template: String,
    pub icon_path: String,
    pub pattern_string: String,
    pub target: u32,
    pub handles_uris: bool,
    pub handles_multiple: bool,
    pub requires_terminal: bool,
}

pub fn save_favorite_apps(apps: &[UserDefinedApp], options: &GeneralOptions) -> WriteResult {
    let variants = apps
        .iter()
        .map(|app| FavoriteAppVariant {
            name: app.name.to_owned(),
            command_template: app.command_template.to_owned(),
            icon_path: app
                .icon_path
                .as_ref()
                .and_then(|p| p.to_str().map(ToString::to_string))
                .unwrap_or_default(),
            pattern_string: app.pattern_string.to_owned(),
            target: app.target as u32,
            handles_uris: app.handles_uris,
            handles_multiple: app.handles_multiple,
            requires_terminal: app.requires_terminal,
        })
        .collect::<Vec<_>>();
    options.favorite_apps.set(variants)
}

pub fn load_favorite_apps(options: &GeneralOptions) -> Vec<UserDefinedApp> {
    options
        .favorite_apps
        .get()
        .into_iter()
        .map(|app| UserDefinedApp {
            name: app.name,
            command_template: app.command_template,
            icon_path: Some(app.icon_path)
                .filter(|p| !p.is_empty())
                .map(PathBuf::from),
            pattern_string: app.pattern_string,
            target: app.target.into(),
            handles_uris: app.handles_uris,
            handles_multiple: app.handles_multiple,
            requires_terminal: app.requires_terminal,
        })
        .collect()
}

#[cfg(test)]
mod test {
    use super::*;

    #[test]
    fn test_variant_type() {
        assert_eq!(&*FavoriteAppVariant::static_variant_type(), "(ssssubbb)");
    }
}
