mod imp {
    use crate::{
        app_window::PatchpalAppWindow, commit_list_window::CommitListWindow,
    };
    use gtk::glib;
    use gtk::prelude::*;
    use gtk::subclass::prelude::*;
    use patchpal::ui::{config::Config, workspace::Workspace};
    use std::cell::{Cell, OnceCell, RefCell};

    #[derive(Default)]
    pub(crate) struct PatchpalApplication {
        pub(crate) exit_code: Cell<u8>,
        pub(crate) config: OnceCell<Config>,
        pub(crate) window: OnceCell<PatchpalAppWindow>,
        pub(crate) commit_list_window: OnceCell<CommitListWindow>,
        pub(crate) workspace: OnceCell<Workspace<super::PatchpalApplication>>,
        pub(crate) auto_accept_clean: Cell<bool>,
        pub(crate) fast_forward: Cell<super::FastForward>,
        pub(crate) keyboard_help_window: RefCell<Option<gtk::Window>>,
        pub(crate) processing_dialog: RefCell<Option<gtk::Window>>,
    }

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

    impl ObjectImpl for PatchpalApplication {}

    impl ApplicationImpl for PatchpalApplication {
        fn command_line(&self, command_line: &gtk::gio::ApplicationCommandLine)
                -> glib::ExitCode {
            // The returned exit code will be ignored. On quit, self.exit_code will be used
            // instead.
            match self.obj().start(command_line) {
                Ok(_) => 0,
                Err(err) => {
                    eprintln!("{}", err);
                    self.obj().quit();
                    1
                }
            }.into()
        }
    }

    impl GtkApplicationImpl for PatchpalApplication {}
}

use crate::{
    app_window::PatchpalAppWindow, commit_list_window::CommitListWindow,
    config::GtkFrontendConfig, settings_dialog,
};
use gtk::glib;
use gtk::prelude::*;
use gtk::subclass::prelude::*;
use patchpal::ui::{config, utils};
use patchpal::ui::frontend::{Frontend, ListChange};
use patchpal::ui::workspace::{Overrides, Workspace};
use std::ffi::OsString;
use std::future::Future;

const APP_ID: &str = "com.redhat.patchpal-gui";

#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub(crate) enum FastForward {
    #[default] No,
    FastForward,
    FastForwardCommit,
}

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

impl PatchpalApplication {
    pub(crate) fn new() -> Self {
        gtk::gio::resources_register_include!("patchpal.gresource").unwrap();
        let app: Self = glib::Object::builder()
            .build();
        app.set_application_id(Some(APP_ID));
        app.set_flags(gtk::gio::ApplicationFlags::HANDLES_COMMAND_LINE);
        // If we exit, do so with exit code 1 by default. If we exit in any other way than via
        // terminate(), it's because of an error. If we exit via terminate(), it will tell us
        // the exit code.
        app.set_exit_code(1);

        app.add_main_option("commit-clean", glib::Char::from(b'c'),
            glib::OptionFlags::empty(), glib::OptionArg::None,
            "Automatically commit clean cherry-picks without starting the GUI", None);
        app.add_main_option("git", glib::Char::from(b'g'),
            glib::OptionFlags::empty(), glib::OptionArg::Filename,
            "Path to the repository to cherry-pick from. Defaults to the current directory", Some("PATH"));
        app.add_main_option("jira", glib::Char::from(b'j'),
            glib::OptionFlags::empty(), glib::OptionArg::String,
            "Jira ticket to use for commits. Defaults to no Jira",
            Some("JIRA"));
        app.add_main_option("mergetool", glib::Char::from(b'M'),
            glib::OptionFlags::empty(), glib::OptionArg::None,
            "Run as git mergetool. Instead of COMMIT, specify BASE LOCAL REMOTE MERGED as parameters",
            None);
        app.add_main_option("no-ai", glib::Char::from(0),
            glib::OptionFlags::HIDDEN, glib::OptionArg::None,
            "Debug mode: do not run AI", None);
        app.add_main_option("test-case", glib::Char::from(0),
            glib::OptionFlags::HIDDEN, glib::OptionArg::None,
            "Run as a test case. Do not use except in test cases", None);
        app.set_option_context_parameter_string(Some("COMMIT"));
        app.set_option_context_summary(Some("COMMIT: the commit id to apply (cherry-pick)"));
        app.set_option_context_description(Some("You must be in the target directory where you want to apply the commit."));

        app
    }

    fn set_exit_code(&self, exit_code: u8) {
        self.imp().exit_code.set(exit_code);
    }

    pub(crate) fn run_with_exit_code(&self) -> u8 {
        self.clone().upcast::<gtk::Application>().run();
        self.imp().exit_code.get()
    }

    pub(crate) fn config(&self) -> &config::Config {
        self.imp().config.get().unwrap()
    }

    pub(crate) fn workspace(&self) -> &Workspace<PatchpalApplication> {
        self.imp().workspace.get().unwrap()
    }

    pub(crate) fn app_window(&self) -> &PatchpalAppWindow {
        self.imp().window.get().unwrap()
    }

    fn app_window_started(&self) -> Option<&PatchpalAppWindow> {
        if let Some(window) = self.imp().window.get() {
            if window.get_visible() {
                return Some(window);
            }
        }
        None
    }

    pub(crate) fn commit_list_window(&self) -> &CommitListWindow {
        self.imp().commit_list_window.get_or_init(|| {
            CommitListWindow::new(self)
        })
    }

    fn set_style(&self) {
        let theme = gtk::IconTheme::for_display(&gtk::gdk::Display::default().unwrap());
        theme.add_resource_path("/com/redhat/patchpal-gui/icons");
        gtk::Window::set_default_icon_name("patchpal");

        let provider = gtk::CssProvider::new();
        provider.load_from_resource("/com/redhat/patchpal-gui/interface/app.css");
        gtk::style_context_add_provider_for_display(
            &gtk::gdk::Display::default().unwrap(),
            &provider,
            gtk::STYLE_PROVIDER_PRIORITY_APPLICATION
        );
    }

   fn init_app_window(&self) {
        let config = config::Config::new(None);
        config.init_local_gtk_frontend();
        self.imp().config.set(config.clone()).unwrap();
        let window = PatchpalAppWindow::new(self);
        self.imp().window.set(window).unwrap();
    }

    fn start(&self, cmdline: &gtk::gio::ApplicationCommandLine) -> Result<(), String>
    {
        let options = cmdline.options_dict();
        let mut commit_clean = options.contains("commit-clean");
        let upstream_repo_path = options.lookup_value("git", None)
            .map_or(None, |val| val.get::<OsString>());
        let jira = options.lookup_value("jira", None)
            .map_or(None, |val| val.get::<String>());
        let mergetool = options.contains("mergetool");
        let overrides = if options.contains("test-case") {
            commit_clean = true;
            Overrides::AllClean
        } else if options.contains("no-ai") {
            Overrides::NoAI
        } else {
            Overrides::None
        };
        let args = cmdline.arguments();

        self.set_style();
        // Create the main window every time, even when we're not going to use it. Without
        // a window, the gtk main loop just quits. But we want to submit telemetry at the
        // end of the session, even in the commit_clean case.
        self.init_app_window();

        let commit_headers = utils::build_commit_headers(jira.as_deref())?;
        let (upstream_repo, downstream_repo) =
            utils::open_repos(upstream_repo_path.as_deref())?;
        let rpc = utils::create_rpc(self.config().get("rpc"));

        let workspace = Workspace::new(
            self.clone(),
            upstream_repo,
            downstream_repo,
            overrides,
            rpc,
            commit_headers,
        );
        workspace.telemetry().tele_set("frontend", "gtk");
        self.imp().workspace.set(workspace.clone())
            .unwrap_or_else(|_| panic!("workspace already set"));
        self.imp().fast_forward.set(
            if commit_clean { FastForward::FastForwardCommit } else { FastForward::No }
        );
        self.imp().auto_accept_clean.set(commit_clean);
        if commit_clean {
            workspace.telemetry().tele_set("arg_commit_clean", true);
        }
        if !mergetool && args.len() >= 2 {
            for commit_id in args.iter().skip(1) {
                workspace.push_commit(commit_id)?;
            }
        } else if !mergetool {
            if !workspace.push_merge_conflicts()? {
                return Err(
                    "Run Patchpal with a commit you want to apply as a parameter.\n\
                     Or run it without parameters to resolve a merge conflict.\n\
                     Run patchpal --help for more information."
                    .to_string()
                );
            }
        } else {
            if args.len() != 5 {
                return Err("Expected parameters: BASE LOCAL REMOTE MERGED".to_string());
            }
            workspace.push_mergetool(&args[1], &args[2], &args[3], &args[4])?;
        }
        workspace.start()?;
        Ok(())
    }

    pub(crate) fn keyboard_help(&self) {
        let mut win = self.imp().keyboard_help_window.borrow_mut();
        if let Some(w) = win.as_ref() {
            w.present();
            return;
        }
        let w = crate::keyboard_help::create(self);
        w.present();
        *win = Some(w);
    }

    pub(crate) fn show_settings(&self) {
        let settings_dialog = settings_dialog::SettingsDialog::new(self);
        settings_dialog.present();
    }

    pub(crate) fn get_auto_accept_clean(&self) -> bool {
        self.imp().auto_accept_clean.get()
    }

    pub(crate) fn set_auto_accept_clean(&self, value: bool) {
        if self.imp().auto_accept_clean.get() == value {
            return;
        }
        self.imp().auto_accept_clean.set(value);
        if value {
            self.workspace().command_ack_all_clean();
        }
    }

    pub(crate) fn fast_forward(&self, commit: bool) {
        self.imp().fast_forward.set(
            if commit { FastForward::FastForwardCommit } else { FastForward::FastForward }
        );
        if let Err(msg) = self.step_fast_forward() {
            self.commit_failed(msg, None);
        }
    }

    /// Attempt to do one step in fast forward. If `fast_forward` is not set, return
    /// Ok(false). If we can't do the step, unset `fast_forward` and return Ok(false).
    /// In case of error, unset `fast_forward` and return Err. If we managed to do the
    /// step, return Ok(true).
    fn step_fast_forward(&self) -> Result<bool, String> {
        fn do_step(app: &PatchpalApplication) -> Result<bool, String> {
            let ff = app.imp().fast_forward.get();
            if ff == FastForward::No || !app.workspace().is_resolved() {
                return Ok(false);
            }
            if app.workspace().apply_next()? {
                return Ok(true);
            }
            // no more commits to apply
            if ff == FastForward::FastForwardCommit {
                app.workspace().save()?;
                return Ok(true);
            }
            Ok(false)
        }

        match do_step(self) {
            Ok(true) => Ok(true),
            other => {
                self.imp().fast_forward.set(FastForward::No);
                other
            }
        }
    }
}

impl Frontend for PatchpalApplication {
    fn spawn_future(&self, future: impl Future<Output = ()> + 'static) {
        glib::spawn_future_local(future);
    }

    fn terminate(&self, exit_code: u8) {
        self.set_exit_code(exit_code);
        self.quit();
    }

    fn processing_commit(&self) {
        let Some(window) = self.app_window_started() else {
            return;
        };
        window.hide_commit();
        let spinner = gtk::Spinner::builder()
            .spinning(true)
            .width_request(32)
            .height_request(32)
            .build();
        let label = gtk::Label::new(Some("Processing commit\u{2026}"));
        let content = gtk::Box::builder()
            .orientation(gtk::Orientation::Vertical)
            .spacing(10)
            .margin_top(20)
            .margin_bottom(20)
            .margin_start(30)
            .margin_end(30)
            .halign(gtk::Align::Center)
            .build();
        content.append(&spinner);
        content.append(&label);
        if self.imp().processing_dialog.borrow().is_none() {
            let dialog = gtk::Window::builder()
                .child(&content)
                .modal(true)
                .resizable(false)
                .deletable(false)
                .transient_for(window)
                .title("Processing")
                .build();
            dialog.connect_close_request(|_| glib::Propagation::Stop);
            dialog.present();
            self.imp().processing_dialog.replace(Some(dialog));
        }
    }

    fn commit_applied(&self, commit_index: usize) {
        if self.imp().auto_accept_clean.get() {
            self.workspace().command_ack_all_clean();
        }
        match self.step_fast_forward() {
            Ok(true) => return,
            Ok(false) => self.app_window().show_commit(commit_index),
            Err(msg) => self.commit_failed(msg, None),
        }
        if let Some(dialog) = self.imp().processing_dialog.take() {
            dialog.destroy();
        }
    }

    fn commit_failed(&self, err_msg: String, _commit_index: Option<usize>) {
        eprintln!("{}", err_msg);
        self.workspace().abort();
    }

    fn commit_list_changed(&self, change: ListChange, selected: Option<usize>) {
        if let Some(window) = self.app_window_started() {
            window.commit_list_changed(Some(change), selected);
        }
    }

    fn file_changed(&self, file_index: usize) {
        if let Some(window) = self.app_window_started() {
            window.file_changed(file_index);
        }
    }

    fn view_changed(&self, file_index: usize, change: ListChange) {
        if let Some(window) = self.app_window_started() {
            window.view_changed(file_index, change);
        }
    }

    fn run_editor(
        &self,
        path: OsString,
        data: Vec<u8>,
        done: Box<dyn FnOnce(Option<Vec<u8>>)>,
    ) {
        if let Some(window) = self.app_window_started() {
            window.run_editor(path, data, done);
        }
    }

    async fn telemetry_agreement(&self) -> Option<uuid::Uuid> {
        crate::telemetry::agreement(self.clone()).await
    }
}
