use adw::subclass::prelude::*;
use gettextrs::gettext;
use gtk::{
    gio,
    glib::{self, clone},
    prelude::*,
};

use super::{
    export::ExportPopover, new_project::NewProjectWindow, recents::RecentPopover,
    sets::IconsSetView, symbolic::SymbolicView,
};
use crate::{
    application::Application,
    config::{APP_ID, PROFILE},
    project::{Project, ProjectType},
    recents::RecentManager,
};

#[derive(Clone, Copy, Eq, PartialEq)]
pub enum View {
    Empty,
    IconsSet,
    Symbolic,
    Loading,
}

mod imp {
    use std::{
        cell::{Cell, RefCell},
        rc::Rc,
    };

    use super::*;

    #[derive(gtk::CompositeTemplate)]
    #[template(resource = "/org/gnome/design/SymbolicPreview/window.ui")]
    pub struct Window {
        pub(super) current_view: Cell<View>,
        #[template_child]
        pub(super) icons_set_view: TemplateChild<IconsSetView>,
        #[template_child]
        pub(super) progress: TemplateChild<gtk::ProgressBar>,
        #[template_child]
        pub(super) symbolic_view: TemplateChild<SymbolicView>,
        pub(super) open_project: Rc<RefCell<Option<Project>>>,
        pub(super) settings: gio::Settings,
        #[template_child]
        pub export_btn: TemplateChild<gtk::MenuButton>,
        #[template_child]
        pub revealer: TemplateChild<gtk::Revealer>,
        #[template_child]
        pub export_popover: TemplateChild<ExportPopover>,
        #[template_child]
        pub main_stack: TemplateChild<gtk::Stack>,
        #[template_child]
        pub recent_popover: TemplateChild<RecentPopover>,
        #[template_child]
        pub window_title: TemplateChild<adw::WindowTitle>,
        #[template_child]
        pub spinner: TemplateChild<gtk::Spinner>,
        pub history: RefCell<Option<RecentManager>>,
    }

    impl Default for Window {
        fn default() -> Self {
            Self {
                current_view: Cell::new(View::Empty),
                icons_set_view: TemplateChild::default(),
                progress: TemplateChild::default(),
                symbolic_view: TemplateChild::default(),
                open_project: Rc::new(RefCell::new(None)),
                settings: gio::Settings::new(APP_ID),
                export_btn: TemplateChild::default(),
                export_popover: TemplateChild::default(),
                main_stack: TemplateChild::default(),
                recent_popover: TemplateChild::default(),
                window_title: TemplateChild::default(),
                spinner: TemplateChild::default(),
                revealer: TemplateChild::default(),
                history: RefCell::new(None),
            }
        }
    }

    #[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) {
            klass.bind_template();

            klass.install_action_async("win.open", None, |win, _, _| async move {
                if let Err(err) = win.open_file().await {
                    tracing::error!("Failed to open a project {err}");
                }
            });

            klass.install_action("win.new-icon-set", None, move |win, _, _| {
                let new_dialog = NewProjectWindow::new(ProjectType::IconsSet);
                new_dialog.set_transient_for(Some(win));
                new_dialog.present();
            });

            klass.install_action("win.new-symbolic-icon", None, move |win, _, _| {
                let new_dialog = NewProjectWindow::new(ProjectType::Symbolic);
                new_dialog.set_transient_for(Some(win));
                new_dialog.present();
            });

            // The shuffle is only available for Symbolic view
            klass.install_action("win.shuffle-icons", None, move |win, _, _| {
                win.imp().symbolic_view.shuffle();
            });

            // The shuffle is only available for Symbolic view
            klass.install_action_async("win.reload", None, move |win, _, _| async move {
                // Just re-open the project which will re-parse the file and re-create all the
                // widgets Not very optimal, but detecting changes per context
                // is HARD.
                if let Some(p) = win.open_project() {
                    // The project cache needs to be cleaned so we can re-generate it again.
                    win.parse_project(p.file()).await;
                }
            });
        }

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

    impl ObjectImpl for Window {
        fn constructed(&self) {
            self.parent_constructed();

            let obj = self.obj();

            // Devel Profile
            if PROFILE == "Devel" {
                obj.add_css_class("devel");
            }

            // Load latest window state
            obj.load_window_size();
            self.export_popover.connect_local(
                "exported",
                false,
                clone!(@weak obj => @default-panic, move |args| {
                    let dest: std::path::PathBuf = args[1].get::<String>().unwrap().into();
                    let project = obj.open_project().unwrap();
                    let ctx = glib::MainContext::default();
                    ctx.spawn_local(async move {
                        if let Err(err) = project.export(dest).await {
                            tracing::error!("Failed to export project {}", err);
                        }
                    });
                    None
                }),
            );
            obj.set_view(View::Empty);
        }
    }

    impl WidgetImpl for Window {
        fn map(&self) {
            self.parent_map();
            if self.current_view.get() == View::Loading {
                self.spinner.start();
            }
        }
    }
    impl WindowImpl for Window {
        // Save window state on delete event
        fn close_request(&self) -> glib::Propagation {
            if let Err(err) = self.obj().save_window_size() {
                tracing::warn!("Failed to save window state, {}", &err);
            }

            // Pass close request on to the parent
            self.parent_close_request()
        }
    }

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

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

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

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

        let (width, height) = self.default_size();

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

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

        Ok(())
    }

    fn load_window_size(&self) {
        let settings = &self.imp().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);

        if is_maximized {
            self.maximize();
        }
    }

    pub fn open_project(&self) -> Option<Project> {
        self.imp().open_project.borrow().clone()
    }

    pub async fn parse_project(&self, file: gio::File) {
        self.set_view(View::Loading);
        if let Ok(project) = Project::parse(file).await {
            self.set_open_project(project);
        } else {
            tracing::error!("Failed to parse the project");
            self.set_view(View::Empty);
        }
    }

    pub fn set_open_project(&self, project: Project) {
        let imp = self.imp();
        imp.recent_popover.popdown();
        imp.window_title.set_subtitle(&project.name());
        imp.history.borrow().as_ref().unwrap().add(project.path());

        let app = self.application().and_downcast::<Application>().unwrap();
        let theme = &app.imp().icon_theme.get().unwrap();
        theme.add_search_path(project.icon_theme_dir());

        match project.project_type() {
            ProjectType::IconsSet => {
                let set = project.set().unwrap();
                imp.revealer.set_reveal_child(true);
                set.connect_category_parsed(clone!(@strong self as this => move |set| {
                    this.set_view(View::IconsSet);
                    let total = set.n_parsed_categories();
                    let n_categories = set.total_categories();
                    let fraction = total as f64 / n_categories as f64;
                    if fraction == 1.0 {
                        this.imp().revealer.set_reveal_child(false);
                    }
                    this.imp().progress.set_fraction(fraction);
                }));
                imp.icons_set_view.load(&project);
            }
            ProjectType::Symbolic => {
                self.set_view(View::Symbolic);
                imp.symbolic_view.load(&project);
            }
        }
        imp.open_project.replace(Some(project));
    }

    pub fn set_view(&self, view: View) {
        let imp = self.imp();
        imp.current_view.set(view);
        self.action_set_enabled("win.shuffle-icons", view == View::Symbolic);
        self.action_set_enabled("win.reload", view == View::IconsSet);

        match view {
            View::Empty => {
                self.set_title(Some(&gettext("Symbolic Preview")));
                imp.main_stack.set_visible_child_name("empty");
                imp.export_btn.set_visible(false);
                imp.spinner.stop();
            }
            View::IconsSet => {
                imp.main_stack.set_visible_child_name("icons");
                imp.export_btn.set_label(&gettext("Export All"));
                imp.export_btn.set_visible(true);
                imp.spinner.stop();
            }
            View::Symbolic => {
                imp.main_stack.set_visible_child_name("symbolic");
                imp.export_btn.set_label(&gettext("Export"));
                imp.export_btn.set_visible(true);
                imp.spinner.stop();
            }
            View::Loading => {
                imp.main_stack.set_visible_child_name("loading");
                imp.export_btn.set_visible(false);
                imp.spinner.start();
            }
        };
    }

    pub(crate) fn set_history(&self, history: RecentManager) {
        let imp = self.imp();
        // History Popover
        imp.recent_popover.set_model(&history.model);
        imp.history.replace(Some(history));
    }

    async fn open_file(&self) -> anyhow::Result<()> {
        let filters = gio::ListStore::new::<gtk::FileFilter>();
        let svg_filter = gtk::FileFilter::new();
        svg_filter.set_name(Some(&gettext("SVG images")));
        svg_filter.add_mime_type("image/svg+xml");
        filters.append(&svg_filter);

        let open_dialog = gtk::FileDialog::builder()
            .title(gettext("Open File"))
            .accept_label(gettext("_Open"))
            .modal(true)
            .filters(&filters)
            .build();

        let file = open_dialog.open_future(Some(self)).await?;
        self.parse_project(file).await;
        Ok(())
    }
}
