use std::cell::Cell;

use adw::subclass::prelude::AdwApplicationWindowImpl;
use gettextrs::gettext;
use gtk::{self, gdk, gio, glib, glib::clone, prelude::*, subclass::prelude::*, CompositeTemplate};
use ruma::{RoomId, UserId};
use tracing::{error, warn};

use crate::{
    account_switcher::{AccountSwitcherButton, AccountSwitcherPopover},
    components::Spinner,
    error_page::ErrorPage,
    greeter::Greeter,
    login::Login,
    prelude::*,
    session::{
        model::{Session, SessionState},
        view::{AccountSettings, SessionView},
    },
    session_list::{FailedSession, SessionInfo},
    toast,
    utils::LoadingState,
    Application, APP_ID, PROFILE,
};

mod imp {
    use glib::subclass::InitializingObject;

    use super::*;

    #[derive(Debug, CompositeTemplate, Default, glib::Properties)]
    #[template(resource = "/org/gnome/Fractal/ui/window.ui")]
    #[properties(wrapper_type = super::Window)]
    pub struct Window {
        #[template_child]
        pub main_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub loading: TemplateChild<gtk::WindowHandle>,
        #[template_child]
        pub greeter: TemplateChild<Greeter>,
        #[template_child]
        pub login: TemplateChild<Login>,
        #[template_child]
        pub error_page: TemplateChild<ErrorPage>,
        #[template_child]
        pub session: TemplateChild<SessionView>,
        #[template_child]
        pub toast_overlay: TemplateChild<adw::ToastOverlay>,
        #[template_child]
        pub offline_banner: TemplateChild<adw::Banner>,
        #[template_child]
        pub spinner: TemplateChild<Spinner>,
        /// Whether the window should be in compact view.
        ///
        /// It means that the horizontal size is not large enough to hold all
        /// the content.
        #[property(get, set = Self::set_compact, explicit_notify)]
        pub compact: Cell<bool>,
        /// The selection of the logged-in sessions.
        ///
        /// The one that is selected being the one that is visible.
        #[property(get)]
        pub session_selection: gtk::SingleSelection,
        pub account_switcher: AccountSwitcherPopover,
    }

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

        fn class_init(klass: &mut Self::Class) {
            AccountSwitcherButton::static_type();

            Self::bind_template(klass);

            klass.add_binding_action(
                gdk::Key::v,
                gdk::ModifierType::CONTROL_MASK,
                "win.paste",
                None,
            );
            klass.add_binding_action(
                gdk::Key::Insert,
                gdk::ModifierType::SHIFT_MASK,
                "win.paste",
                None,
            );
            klass.install_action("win.paste", None, move |obj, _, _| {
                obj.imp().session.handle_paste_action();
            });

            klass.install_action(
                "win.open-account-settings",
                Some("s"),
                move |obj, _, variant| {
                    if let Some(session_id) = variant.and_then(|v| v.get::<String>()) {
                        obj.open_account_settings(&session_id);
                    }
                },
            );

            klass.install_action("win.new-session", None, |obj, _, _| {
                obj.switch_to_greeter_page();
            });
            klass.install_action("win.show-login", None, |obj, _, _| {
                obj.switch_to_login_page();
            });
            klass.install_action("win.show-session", None, |obj, _, _| {
                obj.show_selected_session();
            });

            klass.install_action("win.toggle-fullscreen", None, |obj, _, _| {
                if obj.is_fullscreened() {
                    obj.unfullscreen();
                } else {
                    obj.fullscreen();
                }
            });
        }

        fn instance_init(obj: &InitializingObject<Self>) {
            obj.init_template();
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for Window {
        fn constructed(&self) {
            self.parent_constructed();
            let obj = self.obj();

            let builder = gtk::Builder::from_resource("/org/gnome/Fractal/ui/shortcuts.ui");
            let shortcuts = builder.object("shortcuts").unwrap();
            obj.set_help_overlay(Some(&shortcuts));

            // Development Profile
            if PROFILE.should_use_devel_class() {
                obj.add_css_class("devel");
            }

            obj.load_window_size();

            self.main_stack.connect_visible_child_notify(
                clone!(@weak obj => move |_| obj.set_default_by_child()),
            );
            obj.set_default_by_child();

            self.account_switcher
                .set_session_selection(Some(self.session_selection.clone()));

            self.session_selection
                .connect_selected_item_notify(clone!(@weak obj => move |_| {
                    obj.show_selected_session();
                }));
            self.session_selection.connect_items_changed(
                clone!(@weak obj => move |session_selection, pos, removed, added| {
                    let n_items = session_selection.n_items();
                    obj.action_set_enabled("win.show-session", n_items > 0);

                    if removed > 0 && n_items == 0 {
                        obj.switch_to_greeter_page();
                        return;
                    }

                    if added == 0 {
                        return;
                    }

                    for i in pos..pos + added {
                        let Some(session) = session_selection.item(i).and_downcast::<SessionInfo>() else {
                            continue;
                        };

                        if let Some(failed) = session.downcast_ref::<FailedSession>() {
                            toast!(obj, failed.error().to_user_facing());
                        }

                        let settings = Application::default().settings();
                        if session.session_id() == settings.string("current-session")
                        {
                            session_selection.set_selected(i);
                        }
                    }
                }),
            );

            let app = Application::default();
            let session_list = app.session_list();

            self.session_selection.set_model(Some(session_list));

            if session_list.state() == LoadingState::Ready {
                if session_list.is_empty() {
                    obj.switch_to_greeter_page();
                }
            } else {
                session_list.connect_state_notify(clone!(@weak obj => move |session_list| {
                    if session_list.state() == LoadingState::Ready && session_list.is_empty() {
                        obj.switch_to_greeter_page();
                    }
                }));
            }

            let monitor = gio::NetworkMonitor::default();
            monitor.connect_network_changed(clone!(@weak obj => move |_, _| {
                obj.update_network_state();
            }));

            obj.update_network_state();
        }
    }

    impl WindowImpl for Window {
        // save window state on delete event
        fn close_request(&self) -> glib::Propagation {
            let obj = self.obj();

            if let Err(error) = obj.save_window_size() {
                warn!("Failed to save window state: {error}");
            }
            if let Err(error) = obj.save_current_visible_session() {
                warn!("Failed to save current session: {error}");
            }

            glib::Propagation::Proceed
        }
    }

    impl WidgetImpl for Window {}
    impl ApplicationWindowImpl for Window {}
    impl AdwApplicationWindowImpl for Window {}

    impl Window {
        /// Set whether the window should be in compact view.
        fn set_compact(&self, compact: bool) {
            if compact == self.compact.get() {
                return;
            }

            self.compact.set(compact);
            self.obj().notify_compact();
        }
    }
}

glib::wrapper! {
    /// The main window.
    pub struct Window(ObjectSubclass<imp::Window>)
        @extends gtk::Widget, gtk::Window, gtk::Root, gtk::ApplicationWindow, adw::ApplicationWindow, @implements gtk::Accessible, gio::ActionMap, gio::ActionGroup;
}

impl Window {
    pub fn new(app: &Application) -> Self {
        glib::Object::builder()
            .property("application", Some(app))
            .property("icon-name", Some(APP_ID))
            .build()
    }

    /// Add the given session to the session list and select it.
    pub fn add_session(&self, session: Session) {
        let index = Application::default().session_list().insert(session);
        self.session_selection().set_selected(index as u32);
    }

    /// The ID of the currently visible session, if any.
    pub fn current_session_id(&self) -> Option<String> {
        Some(
            self.imp()
                .session_selection
                .selected_item()
                .and_downcast::<SessionInfo>()?
                .session_id()
                .to_owned(),
        )
    }

    /// Set the current session by its ID.
    ///
    /// Returns `true` if the session was set as the current session.
    pub fn set_current_session_by_id(&self, session_id: &str) -> bool {
        let imp = self.imp();

        let Some(index) = Application::default().session_list().index(session_id) else {
            return false;
        };

        let index = index as u32;
        let prev_selected = imp.session_selection.selected();

        if index == prev_selected {
            // Make sure the session is displayed;
            self.show_selected_session();
        } else {
            imp.session_selection.set_selected(index);
        }

        true
    }

    /// Show the selected session.
    ///
    /// The displayed view will change according to the current session.
    pub fn show_selected_session(&self) {
        let imp = self.imp();

        let Some(session) = imp
            .session_selection
            .selected_item()
            .and_downcast::<SessionInfo>()
        else {
            return;
        };

        if let Some(session) = session.downcast_ref::<Session>() {
            imp.session.set_session(Some(session));

            if session.state() == SessionState::Ready {
                imp.main_stack.set_visible_child(&*imp.session);
            } else {
                session.connect_ready(clone!(@weak imp => move |_| {
                    imp.main_stack.set_visible_child(&*imp.session);
                }));
                self.switch_to_loading_page();
            }

            // We need to grab the focus so that keyboard shortcuts work.
            imp.session.grab_focus();

            return;
        }

        if let Some(failed) = session.downcast_ref::<FailedSession>() {
            imp.error_page
                .display_session_error(&failed.error().to_user_facing());
            imp.main_stack.set_visible_child(&*imp.error_page);
        } else {
            self.switch_to_loading_page();
        }

        imp.session.set_session(None::<Session>);
    }

    pub fn save_window_size(&self) -> Result<(), glib::BoolError> {
        let settings = Application::default().settings();

        let size = self.default_size();

        settings.set_int("window-width", size.0)?;
        settings.set_int("window-height", size.1)?;

        settings.set_boolean("is-maximized", self.is_maximized())?;

        Ok(())
    }

    fn load_window_size(&self) {
        let settings = Application::default().settings();

        let width = settings.int("window-width");
        let height = settings.int("window-height");
        let is_maximized = settings.boolean("is-maximized");

        self.set_default_size(width, height);
        self.set_property("maximized", is_maximized);
    }

    /// Change the default widget of the window based on the visible child.
    ///
    /// These are the default widgets:
    /// - `Greeter` screen => `Login` button.
    fn set_default_by_child(&self) {
        let imp = self.imp();

        if imp.main_stack.visible_child() == Some(imp.greeter.get().upcast()) {
            self.set_default_widget(Some(&imp.greeter.default_widget()));
        } else {
            self.set_default_widget(gtk::Widget::NONE);
        }
    }

    pub fn switch_to_loading_page(&self) {
        let imp = self.imp();
        imp.main_stack.set_visible_child(&*imp.loading);
    }

    pub fn switch_to_login_page(&self) {
        let imp = self.imp();
        imp.main_stack.set_visible_child(&*imp.login);
        imp.login.focus_default();
    }

    pub fn switch_to_greeter_page(&self) {
        let imp = self.imp();
        imp.main_stack.set_visible_child(&*imp.greeter);
    }

    /// This appends a new toast to the list
    pub fn add_toast(&self, toast: adw::Toast) {
        self.imp().toast_overlay.add_toast(toast);
    }

    pub fn account_switcher(&self) -> &AccountSwitcherPopover {
        &self.imp().account_switcher
    }

    /// The `SessionView` of this window.
    pub fn session_view(&self) -> &SessionView {
        &self.imp().session
    }

    fn update_network_state(&self) {
        let imp = self.imp();
        let monitor = gio::NetworkMonitor::default();

        let is_network_available = monitor.is_network_available();
        self.action_set_enabled("win.show-login", is_network_available);

        if !is_network_available {
            imp.offline_banner
                .set_title(&gettext("No network connection"));
            imp.offline_banner.set_revealed(true);
        } else if monitor.connectivity() < gio::NetworkConnectivity::Full {
            imp.offline_banner
                .set_title(&gettext("No Internet connection"));
            imp.offline_banner.set_revealed(true);
        } else {
            imp.offline_banner.set_revealed(false);
        }
    }

    /// Show the given room for the given session.
    pub fn show_room(&self, session_id: &str, room_id: &RoomId) {
        if self.set_current_session_by_id(session_id) {
            self.imp().session.select_room_by_id(room_id);

            self.present();
        }
    }

    pub fn save_current_visible_session(&self) -> Result<(), glib::BoolError> {
        let settings = Application::default().settings();

        settings.set_string(
            "current-session",
            self.current_session_id().unwrap_or_default().as_str(),
        )?;

        Ok(())
    }

    /// Open the account settings for the session with the given ID.
    pub fn open_account_settings(&self, session_id: &str) {
        let Some(session) = Application::default()
            .session_list()
            .get(session_id)
            .and_downcast::<Session>()
        else {
            error!("Tried to open account settings of unknown session with ID '{session_id}'");
            return;
        };

        let window = AccountSettings::new(Some(self), &session);
        window.present();
    }

    /// Open the error page and display the given secret error message.
    pub fn show_secret_error(&self, message: &str) {
        let imp = self.imp();
        imp.error_page.display_secret_error(message);
        imp.main_stack.set_visible_child(&*imp.error_page);
    }

    /// Show the verification with the given flow ID for the user with the given
    /// ID for the given session.
    pub fn show_verification(&self, session_id: &str, user_id: &UserId, flow_id: &str) {
        if self.set_current_session_by_id(session_id) {
            self.imp()
                .session
                .select_verification_by_id(user_id, flow_id);

            self.present();
        }
    }
}
