use super::item_row_dnd::ItemRowDnD;
use crate::app::App;
use crate::gobject_models::{GCategoryID, GFeedListItem, GFeedListItemID};
use crate::i18n::i18n;
use crate::infrastructure::{FaviconCache, TokioRuntime};
use crate::main_window::MainWindow;
use crate::settings::DEFAULT_TIMEOUT_SECONDS;
use crate::sidebar::feed_list::models::FeedListItemID;
use crate::util::{Elapsed, JOIN_ERROR};
use gdk4::{ContentProvider, Drag, DragAction, DragCancelReason, Rectangle, Texture};
use gio::{Menu, MenuItem};
use glib::{Properties, SignalHandlerId, clone, subclass, subclass::*};
use gtk4::{
    Accessible, Actionable, Buildable, CompositeTemplate, ConstraintTarget, DragIcon, DragSource, DropTarget,
    EventSequenceState, GestureClick, ListBoxRow, PopoverMenu, TreeListRow, Widget, prelude::*, subclass::prelude::*,
};
use news_flash::models::{FeedID, NEWSFLASH_TOPLEVEL, PluginCapabilities};
use once_cell::sync::Lazy;
use std::cell::RefCell;
use std::time::Duration;
use tokio_util::sync::CancellationToken;

mod imp {

    use super::*;

    #[derive(Debug, Default, CompositeTemplate, Properties)]
    #[properties(wrapper_type = super::ItemRow)]
    #[template(file = "data/resources/ui_templates/sidebar/feedlist_item.blp")]
    pub struct ItemRow {
        #[template_child]
        pub drag_source: TemplateChild<DragSource>,
        #[template_child]
        pub drop_target: TemplateChild<DropTarget>,
        #[template_child]
        pub context_popover: TemplateChild<PopoverMenu>,
        #[template_child]
        pub left_click: TemplateChild<GestureClick>,

        #[property(get, set)]
        pub model: RefCell<GFeedListItem>,

        #[property(get, set, nullable, name = "tree-list-row")]
        pub tree_list_row: RefCell<Option<TreeListRow>>,

        #[property(get, set, name = "favicon-texture", nullable)]
        pub favicon_texture: RefCell<Option<Texture>>,

        pub cancellation_token: RefCell<CancellationToken>,
    }

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

        fn class_init(klass: &mut Self::Class) {
            klass.bind_template();
            klass.bind_template_callbacks();
        }

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

    #[glib::derived_properties]
    impl ObjectImpl for ItemRow {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("activated")
                        .param_types([super::ItemRow::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }

        fn constructed(&self) {
            self.drop_target.set_types(&[GFeedListItemID::static_type()]);
            self.drop_target.connect_drop(clone!(
                #[weak(rename_to = this)]
                self,
                #[upgrade_or_panic]
                move |_drop_target, value, _x, y| {
                    this.clear_dnd_highlight();
                    let (parent_id, sort_index) = this.calc_drop_destination(y);
                    let item_id: FeedListItemID = value.get::<GFeedListItemID>().unwrap().into();

                    if let FeedListItemID::Category(category_id) = item_id {
                        let category_id = category_id.as_str().to_string();
                        let parent_id = parent_id.as_ref().as_str().to_string();

                        tracing::debug!(%category_id, %parent_id, "move category");
                        MainWindow::activate_action(
                            "move-category",
                            Some(&(category_id, parent_id, sort_index).to_variant()),
                        );
                    } else if let FeedListItemID::Feed(current_mapping) = item_id {
                        let feed_id = current_mapping.feed_id.as_str().to_string();
                        let from_id = current_mapping.category_id.as_str().to_string();
                        let to_id = parent_id.as_ref().as_str().to_string();

                        tracing::debug!(%feed_id, %from_id, %to_id, "move feed");
                        MainWindow::activate_action(
                            "move-feed",
                            Some(&(feed_id, from_id, to_id, sort_index).to_variant()),
                        );
                    }
                    true
                }
            ));

            self.parent_constructed();
        }

        fn dispose(&self) {
            self.cancellation_token.borrow().cancel();
        }
    }

    impl WidgetImpl for ItemRow {}

    impl ListBoxRowImpl for ItemRow {}

    #[gtk4::template_callbacks]
    impl ItemRow {
        #[template_callback]
        pub fn on_left_click(&self, press_count: i32, _x: f64, _y: f64) {
            if press_count == 2 {
                self.left_click.set_state(EventSequenceState::Claimed);
                if let Some(row) = self.tree_list_row.borrow().as_ref() {
                    row.set_expanded(!row.is_expanded());
                }
            }
        }

        #[template_callback]
        pub fn on_right_click(&self, press_count: i32, x: f64, y: f64) {
            if press_count != 1 {
                return;
            }

            if App::default().is_offline() {
                return;
            }

            let rect = Rectangle::new(x as i32, y as i32, 0, 0);
            self.context_popover.set_pointing_to(Some(&rect));
            self.context_popover.popup();
        }

        #[template_callback]
        pub fn on_long_press(&self, x: f64, y: f64) {
            if App::default().is_offline() {
                return;
            }

            let rect = Rectangle::new(x as i32, y as i32, 0, 0);
            self.context_popover.set_pointing_to(Some(&rect));
            self.context_popover.popup();
        }

        #[template_callback]
        pub fn on_drop_enter(&self, _x: f64, y: f64) -> DragAction {
            self.clear_dnd_highlight();
            self.hightlight_dnd(y);
            DragAction::MOVE
        }

        #[template_callback]
        pub fn on_drop_motion(&self, _x: f64, y: f64) -> DragAction {
            self.clear_dnd_highlight();
            self.hightlight_dnd(y);
            DragAction::MOVE
        }

        #[template_callback]
        pub fn on_drop_leave(&self) {
            self.clear_dnd_highlight();
        }

        #[template_callback]
        pub fn on_drag_begin(&self, drag: &Drag) {
            let obj = self.obj();

            obj.set_opacity(0.5);

            if let Some(row) = self.tree_list_row.borrow().as_ref() {
                row.set_expanded(false);
                obj.set_sensitive(false);
            }

            let drag_icon = DragIcon::for_drag(drag).downcast::<DragIcon>().unwrap();
            let widget = ItemRowDnD::new(self.model.borrow().clone());
            let width = obj.parent().map(|p| p.width() - 16).unwrap_or(100).max(100);
            widget.set_width_request(width);
            widget.set_height_request(obj.height() - 2);
            drag_icon.set_child(Some(&widget));
        }

        #[template_callback]
        pub fn on_drag_cancel(&self, _drag: &Drag, _reason: DragCancelReason) -> bool {
            let obj = self.obj();
            obj.set_opacity(1.0);
            obj.set_sensitive(true);
            true
        }

        #[template_callback]
        pub fn on_drag_end(&self, _drag: &Drag, _delete_data: bool) {
            let obj = self.obj();
            obj.set_opacity(1.0);
            obj.set_sensitive(true);
        }

        #[template_callback]
        pub fn is_item_label_visible(&self, count: u32) -> bool {
            count > 0
        }

        #[template_callback]
        pub fn is_error_icon_visible(&self, message: &str) -> bool {
            !message.is_empty()
        }

        #[template_callback]
        pub fn is_favicon_visible(&self, texture: Option<Texture>) -> bool {
            texture.is_some() && !self.model.borrow().is_category()
        }

        #[template_callback]
        pub fn is_generic_favicon_visible(&self, texture: Option<Texture>) -> bool {
            texture.is_none() && !self.model.borrow().is_category()
        }

        pub(super) fn context_menu(id: GFeedListItemID, label: String) -> Menu {
            let model = Menu::new();
            let mark_read_item = MenuItem::new(Some(&i18n("Set Read")), None);
            let edit_item = MenuItem::new(Some(&i18n("Edit")), None);
            let delete_item = MenuItem::new(Some(&i18n("Delete")), None);

            match id.as_ref() {
                FeedListItemID::Feed(feed_mapping) => {
                    let variant = feed_mapping.feed_id.as_str().to_variant();
                    mark_read_item.set_action_and_target_value(Some("win.mark-feed-read"), Some(&variant));

                    let tuple_variant = (
                        feed_mapping.feed_id.as_str().to_string(),
                        feed_mapping.category_id.as_str().to_string(),
                    )
                        .to_variant();
                    edit_item.set_action_and_target_value(Some("win.edit-feed-dialog"), Some(&tuple_variant));

                    let tuple_variant = (feed_mapping.feed_id.as_str().to_string(), label).to_variant();
                    delete_item.set_action_and_target_value(Some("win.enqueue-delete-feed"), Some(&tuple_variant));
                }
                FeedListItemID::Category(category_id) => {
                    let variant = category_id.as_str().to_variant();
                    mark_read_item.set_action_and_target_value(Some("win.mark-category-read"), Some(&variant));
                    edit_item.set_action_and_target_value(Some("win.edit-category-dialog"), Some(&variant));

                    let tuple_variant = (category_id.as_str().to_string(), label).to_variant();
                    delete_item.set_action_and_target_value(Some("win.enqueue-delete-category"), Some(&tuple_variant));
                }
            }

            model.append_item(&mark_read_item);
            model.append_item(&edit_item);
            model.append_item(&delete_item);
            model
        }

        pub(super) async fn load_favicon(&self, feed_id: FeedID, cancellation_token: CancellationToken) {
            let header_map = crate::app::App::default().settings().get_feed_header_map(&feed_id);
            let timeout = App::default()
                .settings()
                .advanced()
                .timeout_seconds
                .unwrap_or(DEFAULT_TIMEOUT_SECONDS);

            let future = TokioRuntime::spawn(async move {
                let client = App::client().await;
                let favicon_future = FaviconCache::get_icon(&feed_id, App::news_flash(), client, header_map);

                tokio::select! {
                    favicon = favicon_future => {
                        Ok(favicon)
                    }
                    _ = tokio::time::sleep(Duration::from_secs(timeout)) => {
                        tracing::warn!("favicon loading timed out");
                        Err(Elapsed::new())
                    }
                    _ = cancellation_token.cancelled() => {
                        tracing::debug!("favicon loading was cancelled");
                        Err(Elapsed::new())
                    }
                }
            });

            let texture = future.await.expect(JOIN_ERROR).unwrap_or_default();
            self.obj().set_favicon_texture(texture)
        }

        fn clear_dnd_highlight(&self) {
            let obj = self.obj();
            if let Some(prev) = obj
                .parent()
                .and_then(|p| p.prev_sibling())
                .and_then(|c| c.first_child())
            {
                prev.remove_css_class("dnd-bottom");
            }
            if let Some(next) = obj
                .parent()
                .and_then(|p| p.next_sibling())
                .and_then(|c| c.first_child())
            {
                next.remove_css_class("dnd-bottom");
            }
            obj.remove_css_class("dnd-top");
            obj.remove_css_class("dnd-bottom");
        }

        fn hightlight_dnd(&self, y: f64) {
            let obj = self.obj();
            if y < obj.height() as f64 / 2.0 {
                if let Some(prev) = obj
                    .parent()
                    .and_then(|p| p.prev_sibling())
                    .and_then(|c| c.first_child())
                {
                    prev.add_css_class("dnd-bottom");
                } else {
                    obj.add_css_class("dnd-top");
                }
            } else {
                obj.add_css_class("dnd-bottom");
            }
        }

        fn calc_drop_destination(&self, y: f64) -> (GCategoryID, i32) {
            let obj = self.obj();
            let item_id = self.model.borrow().item_id();
            let sort_index = self.model.borrow().sort_index();
            tracing::debug!(?item_id, %sort_index, "drop onto");
            if y < obj.height() as f64 / 2.0 {
                if let Some(prev) = obj
                    .parent()
                    .and_then(|p| p.prev_sibling())
                    .and_then(|c| c.first_child())
                    .and_downcast::<super::ItemRow>()
                {
                    if item_id.is_category() && obj.parent_id().as_ref() == &*NEWSFLASH_TOPLEVEL {
                        (NEWSFLASH_TOPLEVEL.clone().into(), prev.sort_index() + 1)
                    } else if let FeedListItemID::Category(category) = prev.model().item_id().as_ref() {
                        (category.clone().into(), obj.sort_index() - 1)
                    } else {
                        (prev.parent_id(), prev.sort_index() + 1)
                    }
                } else {
                    // no prev row -> we at the very top of the list
                    (NEWSFLASH_TOPLEVEL.clone().into(), 0)
                }
            } else if let FeedListItemID::Category(category) = item_id.as_ref() {
                if self
                    .tree_list_row
                    .borrow()
                    .as_ref()
                    .map(|row| row.is_expanded())
                    .unwrap_or(false)
                {
                    (category.clone().into(), 0)
                } else {
                    (NEWSFLASH_TOPLEVEL.clone().into(), obj.sort_index() + 1)
                }
            } else {
                (obj.parent_id(), obj.sort_index() + 1)
            }
        }
    }
}

glib::wrapper! {
    pub struct ItemRow(ObjectSubclass<imp::ItemRow>)
        @extends Widget, ListBoxRow,
        @implements Accessible, Actionable, Buildable, ConstraintTarget;
}

impl Default for ItemRow {
    fn default() -> Self {
        glib::Object::new()
    }
}

impl ItemRow {
    pub fn new() -> Self {
        Self::default()
    }

    pub fn bind_model(&self, model: &GFeedListItem, list_row: &TreeListRow) {
        let imp = self.imp();
        let is_same_item = imp.model.borrow().item_id() == model.item_id();

        let features = App::default().features();

        self.set_model(model.clone());
        self.set_tree_list_row(Some(list_row));

        if features.as_ref().support_mutation() {
            self.setup_drag_source(features.as_ref().contains(PluginCapabilities::SUPPORT_SUBCATEGORIES));
        }

        let new_token = CancellationToken::new();
        let old_token = imp.cancellation_token.replace(new_token.clone());
        old_token.cancel();

        if !is_same_item {
            if let FeedListItemID::Feed(feed_mapping) = model.item_id().as_ref() {
                let feed_id = feed_mapping.feed_id.clone();
                glib::spawn_future_local(clone!(
                    #[weak]
                    imp,
                    async move {
                        imp.load_favicon(feed_id, new_token).await;
                    }
                ));
            } else {
                self.set_favicon_texture(Texture::NONE);
            }
        }

        let menu_model = imp::ItemRow::context_menu(model.item_id(), model.label());
        self.imp().context_popover.set_menu_model(Some(&menu_model));
    }

    fn setup_drag_source(&self, support_subcategories: bool) {
        let imp = self.imp();
        let item_id = imp.model.borrow().item_id();
        let parent_id = imp.model.borrow().parent_id();
        if item_id.is_category() && !support_subcategories && parent_id.as_ref() != &*NEWSFLASH_TOPLEVEL {
            return;
        }

        let content_provider = ContentProvider::for_value(&item_id.to_value());
        imp.drag_source.set_content(Some(&content_provider));
    }

    pub fn activate(&self) {
        self.emit_by_name::<()>("activated", &[&self.clone()]);
    }

    pub fn connect_activated<F: Fn(&Self) + 'static>(&self, f: F) -> SignalHandlerId {
        self.connect_local("activated", false, move |args| {
            let row = args[1].get::<Self>().expect("The value needs to be of type `ItemRow`");
            f(&row);
            None
        })
    }

    pub fn cancel_recource_loading(&self) {
        self.imp().cancellation_token.borrow().cancel();
    }

    fn parent_id(&self) -> GCategoryID {
        self.model().parent_id()
    }

    fn sort_index(&self) -> i32 {
        self.model().sort_index()
    }
}
