use std::sync::{Arc, Condvar, Mutex};

use gtk::prelude::*;
use gtk::subclass::prelude::*;
use gtk::{gio, glib};

use crate::config;
use crate::scale_request::ScaleRequest;
use crate::window::Window;

#[derive(Default)]
pub enum VaDisplayState {
    #[default]
    Empty,
    /// An element is attempting to create a VA display.
    Creating { creator: gst::Object },
    /// The VA display creation had been attempted.
    Ready { display: Option<gst::Object> },
}

mod imp {
    use std::ops::ControlFlow;
    use std::str::FromStr as _;

    use adw::prelude::AdwApplicationExt;
    use adw::subclass::prelude::*;
    use gettextrs::gettext;
    use glib::{g_printerr, VariantDict};

    use super::*;
    use crate::utils::gettext_f;
    use crate::window::DisplayMode;

    #[derive(Default)]
    pub struct Application {
        // GstVaDisplay shared among all playbins.
        va_state: Arc<(Mutex<VaDisplayState>, Condvar)>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Application {
        const NAME: &'static str = "IdApplication";
        type Type = super::Application;
        type ParentType = adw::Application;
    }

    impl ObjectImpl for Application {
        fn constructed(&self) {
            let obj = self.obj();
            self.parent_constructed();

            // Translators: shown in --help usage line as: identity [OPTION…] [FILES…]
            obj.set_option_context_parameter_string(Some(&gettext("[FILES…]")));

            obj.add_main_option(
                "display",
                glib::Char::from(b'd'),
                glib::OptionFlags::NONE,
                glib::OptionArg::String,
                // Translators: CLI option description for --display. Example values in parentheses
                // must not be localized.
                &gettext("Display mode (tabbed, row, column)"),
                // Translators: CLI option argument placeholder for --display, shown as
                // --display=MODE.
                Some(&gettext("MODE")),
            );
            obj.add_main_option(
                "zoom",
                glib::Char::from(b'z'),
                glib::OptionFlags::NONE,
                glib::OptionArg::Double,
                // Translators: CLI option description for --zoom.
                &gettext("Zoom percentage"),
                // Translators: CLI option argument placeholder for --zoom, shown as
                // --zoom=PERCENT.
                Some(&gettext("PERCENT")),
            );
        }
    }

    impl ApplicationImpl for Application {
        fn handle_local_options(&self, options: &VariantDict) -> ControlFlow<glib::ExitCode> {
            if let Some(mode) = options.lookup::<String>("display").unwrap_or(None) {
                if DisplayMode::from_str(&mode).is_err() {
                    g_printerr!(
                        "{}\n",
                        // Translators: CLI warning when an invalid display mode is provided. {} is
                        // replaced with the invalid mode string.
                        gettext_f(
                            "Unknown display mode: {}, defaulting to tabbed. \
                             Valid modes: tabbed, row, column.",
                            [&mode],
                        )
                    );
                    options.remove("display");
                }
            }

            self.parent_handle_local_options(options)
        }

        fn command_line(&self, command_line: &gio::ApplicationCommandLine) -> glib::ExitCode {
            let options = command_line.options_dict();

            let display_mode: Option<String> = options.lookup("display").unwrap_or(None);
            let zoom: Option<f64> = options.lookup("zoom").unwrap_or(None);

            let args = command_line.arguments();
            // Skip argv[0].
            let files: Vec<gio::File> = args
                .iter()
                .skip(1)
                .map(|arg| command_line.create_file_for_arg(arg))
                .collect();

            debug!(
                "command_line: display_mode={display_mode:?}, zoom={zoom:?}, files={:?}",
                files
                    .iter()
                    .map(|x| x.uri().into())
                    .collect::<Vec<String>>()
            );

            let window = self.obj().create_new_window();

            if let Some(mode) = display_mode {
                window.set_display_mode(mode);
            }

            if let Some(z) = zoom {
                window.set_scale_request(ScaleRequest::from(z / 100.0));
            }

            if files.is_empty() {
                window.present();
            } else {
                for file in &files {
                    window.open_file(file);
                }
            }

            glib::ExitCode::SUCCESS
        }

        fn startup(&self) {
            self.parent_startup();

            gtk::Window::set_default_icon_name(config::APP_ID);

            let obj = self.obj();
            obj.style_manager()
                .set_color_scheme(adw::ColorScheme::PreferDark);

            obj.add_action_entries([
                gio::ActionEntry::builder("quit")
                    .activate(|obj: &Self::Type, _, _| obj.quit())
                    .build(),
                gio::ActionEntry::builder("new-window")
                    .activate(|obj: &Self::Type, _, _| drop(obj.open_new_window()))
                    .build(),
            ]);

            obj.set_accels_for_action("app.quit", &["<primary>q"]);
            obj.set_accels_for_action("app.new-window", &["<primary>n"]);
            obj.set_accels_for_action("win.play-pause", &["<primary>space"]);
            obj.set_accels_for_action("win.open", &["<primary>o"]);
            obj.set_accels_for_action("win.close-tab", &["<primary>w"]);
            obj.set_accels_for_action("win.zoom-in", &["<primary>plus"]);
            obj.set_accels_for_action("win.zoom-out", &["<primary>minus"]);
            obj.set_accels_for_action("win.media-properties", &["<alt>Return"]);

            // Zoom shortcuts roughly follow Loupe.
            //
            // In Loupe, Ctrl-0 is "Best Fit" which fits to window when that's smaller than 100% and
            // remains at 100% when the window is bigger. We don't have this behavior in Identity,
            // so for now, leave Ctrl-0 as 100% as it always used to be in Identity.
            //
            // Ctrl-1 goes first in the list so it's the one shown in the zoom dropdown.
            obj.set_accels_for_action("win.set-scale-request(1.)", &["<primary>1", "<primary>0"]);
            for i in 2..=9 {
                obj.set_accels_for_action(
                    &format!("win.set-scale-request({i}.)"),
                    &[&format!("<primary>{i}")],
                );
            }
        }
    }

    impl GtkApplicationImpl for Application {}
    impl AdwApplicationImpl for Application {}

    impl Application {
        pub fn va_state(&self) -> Arc<(Mutex<VaDisplayState>, Condvar)> {
            self.va_state.clone()
        }
    }
}

glib::wrapper! {
    pub struct Application(ObjectSubclass<imp::Application>)
        @extends adw::Application, gtk::Application, gio::Application,
        @implements gio::ActionGroup, gio::ActionMap;
}

impl Application {
    #[allow(clippy::new_without_default)]
    pub fn new() -> Self {
        glib::Object::builder()
            .property("application-id", config::APP_ID)
            .property("flags", gio::ApplicationFlags::HANDLES_COMMAND_LINE)
            .property("resource-base-path", "/org/gnome/gitlab/YaLTeR/Identity")
            .build()
    }

    pub fn create_new_window(&self) -> Window {
        let window = Window::new(self);

        // Put it in a new window group so modal dialogs don't block other windows.
        let group = gtk::WindowGroup::new();
        group.add_window(&window);

        window
    }

    pub fn open_new_window(&self) -> Window {
        let window = self.create_new_window();

        window.present();

        window
    }

    pub fn va_state(&self) -> Arc<(Mutex<VaDisplayState>, Condvar)> {
        self.imp().va_state()
    }
}
