// 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 super::edit_bookmark_dialog::edit_bookmark_dialog;
use crate::{
    connection::{Connection, ConnectionExt, bookmark::Bookmark, list::ConnectionList},
    dir::Directory,
    file::FileOps,
    main_win::MainWindow,
    options::GeneralOptions,
    shortcuts::Shortcuts,
    utils::{ErrorMessage, WindowExt, bold},
};
use gettextrs::gettext;
use gtk::{gio, glib, prelude::*, subclass::prelude::*};

mod imp {
    use super::*;
    use crate::{
        options::utils::remember_window_size,
        utils::{ListRowSelector, SenderExt, dialog_button_box, display_help},
    };
    use std::{cell::RefCell, rc::Rc};

    pub struct BookmarksDialog {
        pub shortcuts: RefCell<Option<Shortcuts>>,
        pub flatten_model: gtk::FlattenListModel,
        pub selection_model: gtk::SingleSelection,
        pub view: gtk::ColumnView,
        row_selector: Rc<ListRowSelector>,
        pub edit_button: gtk::Button,
        pub remove_button: gtk::Button,
        pub up_button: gtk::Button,
        pub down_button: gtk::Button,
        jump_button: gtk::Button,
        sender: async_channel::Sender<Option<TaggedBookmark>>,
        pub receiver: async_channel::Receiver<Option<TaggedBookmark>>,
    }

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

        fn new() -> Self {
            let flatten_model = gtk::FlattenListModel::new(None::<gio::ListModel>);
            let selection_model = gtk::SingleSelection::new(Some(flatten_model.clone()));
            let view = gtk::ColumnView::builder()
                .width_request(400)
                .height_request(250)
                .model(&selection_model)
                .build();
            let row_selector = ListRowSelector::new(&view);
            let (sender, receiver) = async_channel::bounded(1);
            Self {
                shortcuts: Default::default(),
                flatten_model,
                selection_model,
                view,
                row_selector,
                edit_button: gtk::Button::builder()
                    .label(gettext("_Edit"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                remove_button: gtk::Button::builder()
                    .label(gettext("_Remove"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                up_button: gtk::Button::builder()
                    .label(gettext("_Up"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                down_button: gtk::Button::builder()
                    .label(gettext("_Down"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                jump_button: gtk::Button::builder()
                    .label(gettext("_Jump to"))
                    .use_underline(true)
                    .sensitive(false)
                    .build(),
                sender,
                receiver,
            }
        }
    }

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

            let dialog = self.obj();

            dialog.add_css_class("dialog");
            dialog.set_title(Some(&gettext("Bookmarks")));
            dialog.set_resizable(true);
            dialog.set_destroy_with_parent(true);

            let options = GeneralOptions::new();
            remember_window_size(
                &*dialog,
                &options.bookmarks_window_width,
                &options.bookmarks_window_height,
            );

            let grid = gtk::Grid::builder().build();
            dialog.set_child(Some(&grid));

            self.view.set_header_factory(Some(&self.header_factory()));
            self.view.append_column(
                &gtk::ColumnViewColumn::builder()
                    .title(gettext("Name"))
                    .factory(&self.name_factory())
                    .resizable(true)
                    .build(),
            );
            self.view.append_column(
                &gtk::ColumnViewColumn::builder()
                    .title(gettext("Shortcut"))
                    .factory(&self.shortcut_factory())
                    .resizable(true)
                    .visible(false) // TODO: implement shortcut management
                    .build(),
            );
            self.view.append_column(
                &gtk::ColumnViewColumn::builder()
                    .title(gettext("Path"))
                    .factory(&self.path_factory())
                    .resizable(true)
                    .expand(true)
                    .build(),
            );
            self.view.connect_activate(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_, _| imp.jump_to_clicked()
            ));

            let scrolled_window = gtk::ScrolledWindow::builder()
                .hscrollbar_policy(gtk::PolicyType::Never)
                .vscrollbar_policy(gtk::PolicyType::Automatic)
                .has_frame(true)
                .hexpand(true)
                .vexpand(true)
                .child(&self.view)
                .build();
            grid.attach(&scrolled_window, 0, 0, 1, 1);

            self.selection_model.connect_selected_notify(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.selection_changed()
            ));

            let vbox = gtk::Box::builder()
                .orientation(gtk::Orientation::Vertical)
                .spacing(6)
                .build();
            vbox.append(&self.edit_button);
            vbox.append(&self.remove_button);
            vbox.append(&self.up_button);
            vbox.append(&self.down_button);
            grid.attach(&vbox, 1, 0, 1, 1);

            self.edit_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| {
                    glib::spawn_future_local(async move { imp.edit_clicked().await });
                }
            ));
            self.remove_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.remove_clicked()
            ));
            self.up_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.up_clicked()
            ));
            self.down_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.down_clicked()
            ));

            let help_button = gtk::Button::builder()
                .label(gettext("_Help"))
                .use_underline(true)
                .build();
            let close_button = gtk::Button::builder()
                .label(gettext("_Close"))
                .use_underline(true)
                .build();

            grid.attach(
                &dialog_button_box(&[&help_button], &[&close_button, &self.jump_button]),
                0,
                1,
                2,
                1,
            );

            help_button.connect_clicked(glib::clone!(
                #[weak]
                dialog,
                move |_| {
                    glib::spawn_future_local(async move {
                        display_help(dialog.upcast_ref(), Some("gnome-commander-bookmarks")).await
                    });
                }
            ));

            close_button.connect_clicked(glib::clone!(
                #[strong(rename_to = sender)]
                self.sender,
                move |_| sender.toss(None)
            ));

            self.jump_button.connect_clicked(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_| imp.jump_to_clicked()
            ));

            dialog.set_default_widget(Some(&self.jump_button));
            dialog.set_cancel_widget(&close_button);
        }
    }

    impl WidgetImpl for BookmarksDialog {}
    impl WindowImpl for BookmarksDialog {}

    impl BookmarksDialog {
        fn header_factory(&self) -> gtk::ListItemFactory {
            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListHeader>() else {
                    return;
                };
                item.set_child(Some(
                    &gtk::Label::builder()
                        .xalign(0.0)
                        .margin_start(6)
                        .margin_end(6)
                        .margin_top(12)
                        .margin_bottom(6)
                        .build(),
                ));
            });
            factory.connect_bind(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListHeader>() else {
                    return;
                };
                let Some(label) = item.child().and_downcast::<gtk::Label>() else {
                    return;
                };
                label.set_text("");
                let Some(obj) = item.item().and_downcast::<glib::BoxedAnyObject>() else {
                    return;
                };
                let Ok(node) = obj.try_borrow::<TaggedBookmark>() else {
                    return;
                };
                label.set_markup(&bold(
                    &node
                        .connection
                        .alias()
                        .unwrap_or_else(|| node.connection.uuid()),
                ));
            });
            factory.upcast()
        }

        fn name_factory(&self) -> gtk::ListItemFactory {
            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
                    return;
                };
                item.set_child(Some(&gtk::Label::builder().xalign(0.0).build()));
            });
            factory.connect_bind(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
                    return;
                };
                let Some(label) = item.child().and_downcast::<gtk::Label>() else {
                    return;
                };
                label.set_text("");
                let Some(obj) = item.item().and_downcast::<glib::BoxedAnyObject>() else {
                    return;
                };
                let Ok(node) = obj.try_borrow::<TaggedBookmark>() else {
                    return;
                };
                label.set_text(node.bookmark.name());
            });
            factory.upcast()
        }

        fn shortcut_factory(&self) -> gtk::ListItemFactory {
            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(|_, list_item| {
                let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
                    return;
                };
                let bx = gtk::Box::builder()
                    .orientation(gtk::Orientation::Vertical)
                    .spacing(6)
                    .build();
                list_item.set_child(Some(&bx));
            });
            factory.connect_bind(glib::clone!(
                #[weak(rename_to = imp)]
                self,
                move |_, list_item| {
                    let Some(list_item) = list_item.downcast_ref::<gtk::ListItem>() else {
                        return;
                    };
                    let Some(obj) = list_item.item().and_downcast::<glib::BoxedAnyObject>() else {
                        return;
                    };
                    let Ok(node) = obj.try_borrow::<TaggedBookmark>() else {
                        return;
                    };
                    let Some(bx) = list_item.child().and_downcast::<gtk::Box>() else {
                        return;
                    };

                    while let Some(child) = bx.first_child() {
                        child.unparent();
                    }
                    let shortcuts = imp.shortcuts.borrow();
                    if let Some(shortcuts) = shortcuts
                        .as_ref()
                        .map(|s| s.bookmark_shortcuts(node.bookmark.name()))
                    {
                        for shortcut in shortcuts {
                            bx.append(
                                &gtk::ShortcutLabel::builder()
                                    .accelerator(shortcut.name())
                                    .build(),
                            );
                        }
                    }
                }
            ));
            factory.upcast()
        }

        fn path_factory(&self) -> gtk::ListItemFactory {
            let factory = gtk::SignalListItemFactory::new();
            factory.connect_setup(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
                    return;
                };
                item.set_child(Some(&gtk::Label::builder().xalign(0.0).build()));
            });
            factory.connect_bind(|_, item| {
                let Some(item) = item.downcast_ref::<gtk::ListItem>() else {
                    return;
                };
                let Some(label) = item.child().and_downcast::<gtk::Label>() else {
                    return;
                };
                label.set_text("");
                let Some(obj) = item.item().and_downcast::<glib::BoxedAnyObject>() else {
                    return;
                };
                let Ok(node) = obj.try_borrow::<TaggedBookmark>() else {
                    return;
                };
                label.set_text(node.bookmark.path());
                label.set_tooltip_text(Some(node.bookmark.path()));
            });
            factory.upcast()
        }

        fn selected(&self) -> Option<TaggedBookmark> {
            Some(
                self.selection_model
                    .selected_item()
                    .and_downcast::<glib::BoxedAnyObject>()?
                    .try_borrow::<TaggedBookmark>()
                    .ok()?
                    .clone(),
            )
        }

        fn selection_changed(&self) {
            if let Some(bookmark) = self.selected() {
                self.jump_button.set_sensitive(true);
                self.edit_button.set_sensitive(true);
                self.remove_button.set_sensitive(true);
                self.up_button.set_sensitive(!bookmark.is_first());
                self.down_button.set_sensitive(!bookmark.is_last());
            } else {
                self.jump_button.set_sensitive(false);
                self.edit_button.set_sensitive(false);
                self.remove_button.set_sensitive(false);
                self.up_button.set_sensitive(false);
                self.down_button.set_sensitive(false);
            }
        }

        fn select(&self, position: u32) {
            self.row_selector.select_row(position);
            self.selection_changed();
        }

        async fn edit_clicked(&self) {
            if let Some(bookmark) = self.selected()
                && let Some(changed_bookmark) = edit_bookmark_dialog(
                    self.obj().upcast_ref(),
                    false,
                    &bookmark.connection,
                    &bookmark.bookmark,
                )
                .await
            {
                let old_position = self.selection_model.selected();
                if bookmark
                    .connection
                    .replace_bookmark(&bookmark.bookmark, changed_bookmark)
                {
                    self.select(old_position);
                }
            }
        }

        fn remove_clicked(&self) {
            if let Some(bookmark) = self.selected() {
                let old_position = self.selection_model.selected();
                if bookmark.connection.remove_bookmark(&bookmark.bookmark) {
                    if old_position > 0 {
                        self.select(old_position - 1);
                    } else {
                        self.selection_changed();
                    }
                }
            }
        }

        fn up_clicked(&self) {
            let old_position = self.selection_model.selected();
            if let Some(bookmark) = self.selected()
                && bookmark.connection.move_bookmark_up(&bookmark.bookmark)
            {
                self.select(old_position - 1);
            }
        }

        fn down_clicked(&self) {
            let old_position = self.selection_model.selected();
            if let Some(bookmark) = self.selected()
                && bookmark.connection.move_bookmark_down(&bookmark.bookmark)
            {
                self.select(old_position + 1);
            }
        }

        fn jump_to_clicked(&self) {
            if let Some(bookmark) = self.selected() {
                self.sender.toss(Some(bookmark));
            }
        }
    }
}

glib::wrapper! {
    pub struct BookmarksDialog(ObjectSubclass<imp::BookmarksDialog>)
        @extends gtk::Window, gtk::Widget,
        @implements gtk::Accessible, gtk::Buildable, gtk::ConstraintTarget, gtk::ShortcutManager, gtk::Root, gtk::Native;
}

impl BookmarksDialog {
    fn new(
        parent_window: &gtk::Window,
        connection_list: &ConnectionList,
        shortcuts: &Shortcuts,
    ) -> Self {
        let dialog: Self = glib::Object::builder()
            .property("transient-for", parent_window)
            .build();
        *dialog.imp().shortcuts.borrow_mut() = Some(shortcuts.clone());
        dialog.set_model(connection_list);
        dialog
    }

    fn set_model(&self, connection_list: &ConnectionList) {
        let model = gtk::MapListModel::new(Some(connection_list.all()), move |connection| {
            let model = gio::ListStore::new::<glib::BoxedAnyObject>();
            if let Some(connection) = connection.downcast_ref::<Connection>() {
                Self::update_connection_bookmarks(connection, &model);
                connection.connect_updated(glib::clone!(
                    #[weak]
                    connection,
                    #[weak]
                    model,
                    move || Self::update_connection_bookmarks(&connection, &model)
                ));
            }
            model.upcast()
        });
        self.imp().flatten_model.set_model(Some(&model));
    }

    fn update_connection_bookmarks(connection: &Connection, model: &gio::ListStore) {
        model.remove_all();
        for bookmark in &*connection.bookmarks() {
            model.append(&glib::BoxedAnyObject::new(TaggedBookmark {
                connection: connection.clone(),
                bookmark: bookmark.clone(),
            }));
        }
    }

    pub async fn show(
        parent_window: &MainWindow,
        connection_list: &ConnectionList,
        shortcuts: &Shortcuts,
    ) -> Option<TaggedBookmark> {
        if let Some(dialog) = parent_window.get_dialog::<Self>("bookmarks") {
            dialog.present();
            None
        } else {
            let dialog = parent_window.set_dialog(
                "bookmarks",
                Self::new(parent_window.upcast_ref(), connection_list, shortcuts),
            );
            dialog.present();
            dialog.imp().view.grab_focus();
            let result = dialog.imp().receiver.recv().await.ok().flatten();
            dialog.close();
            result
        }
    }
}

#[derive(Clone)]
pub struct TaggedBookmark {
    pub connection: Connection,
    pub bookmark: Bookmark,
}

impl TaggedBookmark {
    fn is_first(&self) -> bool {
        self.connection
            .bookmarks()
            .first()
            .is_some_and(|b| b == &self.bookmark)
    }

    fn is_last(&self) -> bool {
        self.connection
            .bookmarks()
            .last()
            .is_some_and(|b| b == &self.bookmark)
    }
}

pub async fn bookmark_directory(window: &gtk::Window, dir: &Directory, options: &GeneralOptions) {
    let is_local = dir.is_local();
    let path = dir.path_from_root();

    let Some(path_str) = path.to_str() else {
        ErrorMessage::new(gettext("To bookmark a directory the whole search path to the directory must be in valid UTF-8 encoding"), None::<String>)
            .show(window).await;
        return;
    };

    let connection_list = ConnectionList::get();

    let con = if is_local {
        connection_list.home().upcast()
    } else {
        dir.connection()
    };

    let mut name = path
        .file_name()
        .and_then(|n| n.to_str())
        .unwrap_or_default()
        .to_string();
    let name_len = name.len();
    let mut index = 1;

    // Choose a name that doesn't exist already
    while con.bookmarks().iter().any(|b| b.name() == name) {
        index += 1;
        name.truncate(name_len);
        name.push_str(&format!(" ({index})"));
    }

    let bookmark = Bookmark::new(&name, path_str);

    if let Some(changed_bookmark) = edit_bookmark_dialog(window, true, &con, &bookmark).await {
        con.add_bookmark(changed_bookmark);

        if let Err(error) = options.bookmarks.set(connection_list.save_bookmarks()) {
            eprintln!("Failed to save bookmarks: {error}");
        }
    }
}
