use std::cell::{OnceCell, RefCell};

use adw::subclass::window::AdwWindowImpl;
use gtk::{
    glib::{self, Object, Variant},
    prelude::*,
    subclass::{
        prelude::*,
        widget::{
            CompositeTemplateCallbacksClass, CompositeTemplateClass,
            CompositeTemplateInitializingExt, WidgetImpl,
        },
    },
};
use tracing::{debug, error, info, warn};

use crate::{
    systemd::{self, runtime},
    systemd_gui::new_settings,
    upgrade,
    widget::{
        preferences::data::KEY_PREF_PROP_ORIENTATION_MODE,
        unit_list::{UnitListPanel, column::SysdColumn},
        unit_properties_selector::{
            data_browser::{INTERFACE_NAME, PropertyBrowseItem, SPECIAL_INTERFACE_NAME},
            unit_properties_selection::UnitPropertiesSelectionPanel,
        },
    },
};

use super::UnitPropertiesSelectorDialog;

const WINDOW_SIZE: &str = "unit-property-window-size";
const PANED_SEPARATOR_POSITION: &str = "unit-property-paned-separator-position";

#[derive(Default, gtk::CompositeTemplate)]
#[template(resource = "/io/github/plrigaux/sysd-manager/unit_properties_selector.ui")]
pub struct UnitPropertiesSelectorDialogImp {
    #[template_child]
    properties_selector: TemplateChild<gtk::ColumnView>,

    #[template_child]
    interface_column: TemplateChild<gtk::ColumnViewColumn>,

    #[template_child]
    property_column: TemplateChild<gtk::ColumnViewColumn>,

    #[template_child]
    signature_column: TemplateChild<gtk::ColumnViewColumn>,

    #[template_child]
    access_column: TemplateChild<gtk::ColumnViewColumn>,

    #[template_child]
    search_entry: TemplateChild<gtk::SearchEntry>,

    #[template_child]
    toogle_button: TemplateChild<gtk::ToggleButton>,

    #[template_child]
    unit_properties_selection_panel: TemplateChild<UnitPropertiesSelectionPanel>,

    #[template_child]
    paned: TemplateChild<gtk::Paned>,

    #[template_child]
    orientation_button: TemplateChild<gtk::Button>,

    #[template_child]
    toast_overlay: TemplateChild<adw::ToastOverlay>,

    last_filter_string: RefCell<String>,

    custom_filter: OnceCell<gtk::CustomFilter>,

    tree_list_model: OnceCell<gtk::TreeListModel>,
}

#[gtk::template_callbacks]
impl UnitPropertiesSelectorDialogImp {
    #[template_callback]
    fn search_entry_changed(&self, search_entry: &gtk::SearchEntry) {
        let text: glib::GString = search_entry.text();

        let mut last_filter = self.last_filter_string.borrow_mut();

        let text_is_empty = text.is_empty();
        if !text_is_empty {
            self.toogle_button.set_active(true);
        }

        let change_type = if text_is_empty {
            gtk::FilterChange::LessStrict
        } else if text.len() > last_filter.len() && text.contains(last_filter.as_str()) {
            gtk::FilterChange::MoreStrict
        } else if text.len() < last_filter.len() && last_filter.contains(text.as_str()) {
            gtk::FilterChange::LessStrict
        } else {
            gtk::FilterChange::Different
        };

        debug!("Search text. Current \"{text}\" Prev \"{last_filter}\"");
        last_filter.replace_range(.., text.as_str());

        if let Some(custom_filter) = self.custom_filter.get() {
            custom_filter.changed(change_type);
        }
    }

    #[template_callback]
    fn expand_toggled(&self, toogle_button: &gtk::ToggleButton) {
        info!("expand_toggled {}", toogle_button.is_active());

        self.expand_interfaces(toogle_button.is_active());

        if toogle_button.is_active() {
            toogle_button.set_icon_name("go-down-symbolic");
            toogle_button.set_tooltip_text(Some("Collapse all interfaces"));
        } else {
            toogle_button.set_icon_name("go-next-symbolic");
            toogle_button.set_tooltip_text(Some("Expand all interfaces"));
        }
    }

    #[template_callback]
    fn orientation_toggled(&self, _orientation_button: &gtk::Button) {
        info!("orientation_toggled clicked ");

        let orientation = self.paned.orientation();

        let new_orientation = self.handle_paned_orientation(orientation);
        warn!("New orientation {new_orientation:?}, Old {orientation:?}");
        self.paned.set_orientation(new_orientation);
    }

    fn handle_paned_orientation(&self, orientation: gtk::Orientation) -> gtk::Orientation {
        if orientation == gtk::Orientation::Vertical {
            self.orientation_button.set_icon_name("top-down");
            self.orientation_button
                .set_tooltip_text(Some("Switch to top down layout"));

            gtk::Orientation::Horizontal
        } else {
            self.orientation_button.set_icon_name("side-by-side");
            self.orientation_button
                .set_tooltip_text(Some("Switch to side by side layout"));

            gtk::Orientation::Vertical
        }
    }

    fn expand_interfaces(&self, expand: bool) {
        let Some(tree_list_model) = self.tree_list_model.get() else {
            warn!("Can't find tree list model");
            return;
        };

        let nb_item = tree_list_model.model().n_items();
        warn!("tree_list_model {}", nb_item);
        for i in 0..nb_item {
            if let Some(row) = tree_list_model.child_row(i) {
                warn!("set_expanded {}", i);
                row.set_expanded(expand);
            }
        }
    }

    fn create_filter(&self) -> gtk::CustomFilter {
        let search_entry = self.search_entry.clone();

        gtk::CustomFilter::new(move |object| {
            let text_gs = search_entry.text();
            if text_gs.is_empty() {
                return true;
            }

            let Some(tree_list_row) = object.downcast_ref::<gtk::TreeListRow>() else {
                error!("some wrong downcast_ref {object:?}");
                return false;
            };

            debug!("Depth {} ", tree_list_row.depth());

            if let Some(children) = tree_list_row.children() {
                let item = tree_list_row.item();
                if let Some(prop_selector) = item.and_downcast_ref::<PropertyBrowseItem>() {
                    debug!(
                        "Child model {} {}",
                        children.n_items(),
                        prop_selector.interface()
                    );
                } else {
                    error!("some wrong downcast_ref {object:?}");
                };

                return true;
            }

            let item = tree_list_row.item();
            let Some(prop_selector) = item.and_downcast_ref::<PropertyBrowseItem>() else {
                error!("some wrong downcast_ref {object:?}");
                return false;
            };

            debug!(
                "Interface {:?} Prop {:?}",
                prop_selector.interface(),
                prop_selector.unit_property()
            );

            let texts = text_gs.as_str();

            //if an upper case --> filter
            if text_gs.chars().any(|c| c.is_ascii_uppercase()) {
                prop_selector.unit_property().contains(texts)
            } else {
                prop_selector
                    .unit_property()
                    .to_ascii_lowercase()
                    .contains(texts)
            }
        })
    }

    pub(super) fn set_unit_list(&self, unit_list_panel: &UnitListPanel, column_id: Option<String>) {
        self.unit_properties_selection_panel
            .set_unit_list(unit_list_panel, column_id);

        let Some(tree_list_model) = self.tree_list_model.get() else {
            warn!("Not None");
            return;
        };

        let default = PropertyBrowseItem::new_interface(INTERFACE_NAME.to_owned());
        // list_store.append(&default);

        for default_column in unit_list_panel.default_displayed_columns() {
            let new_property_object = PropertyBrowseItem::from_column(&default_column.column());
            default.add_child(new_property_object);
        }

        let model = tree_list_model.model();
        let store = model.downcast_ref::<gio::ListStore>().unwrap();
        store.append(&default);

        let default = PropertyBrowseItem::new_interface(SPECIAL_INTERFACE_NAME.to_owned());
        // list_store.append(&default);

        for default_column in SysdColumn::specials() {
            let new_property_object = PropertyBrowseItem::from_sysd(&default_column);
            default.add_child(new_property_object);
        }

        let model = tree_list_model.model();
        let store = model.downcast_ref::<gio::ListStore>().unwrap();
        store.append(&default);
    }

    fn load_window_size(&self) {
        // Get the window state from `settings`
        let settings = new_settings();

        let size = settings.value(WINDOW_SIZE);

        let (mut width, mut height) = size.get::<(i32, i32)>().unwrap();

        let mut separator_position = settings.int(PANED_SEPARATOR_POSITION);

        info!(
            "Window settings: width {width}, height {height},  panes position {separator_position}"
        );

        let obj = self.obj();
        let (def_width, def_height) = obj.default_size();

        if width < 0 {
            width = def_width;
            if width < 0 {
                width = 1280;
            }
        }

        if height < 0 {
            height = def_height;
            if height < 0 {
                height = 720;
            }
        }

        // Set the size of the window
        obj.set_default_size(width, height);

        if separator_position < 0 {
            separator_position = width / 2;
        }

        self.paned.set_position(separator_position);

        let (orientation, invert_orientation) = if settings.boolean(KEY_PREF_PROP_ORIENTATION_MODE)
        {
            (gtk::Orientation::Vertical, gtk::Orientation::Horizontal)
        } else {
            (gtk::Orientation::Horizontal, gtk::Orientation::Vertical)
        };

        self.handle_paned_orientation(invert_orientation);
        self.paned.set_orientation(orientation);
    }

    pub fn save_window_context(&self) -> Result<(), glib::BoolError> {
        // Get the size of the window

        let obj = self.obj();
        let size = obj.default_size();

        let settings = new_settings();

        let value: Variant = size.into();
        settings.set_value(WINDOW_SIZE, &value)?;

        let separator_position = self.paned.position();
        settings.set_int(PANED_SEPARATOR_POSITION, separator_position)?;

        settings.set_boolean(
            KEY_PREF_PROP_ORIENTATION_MODE,
            self.paned.orientation() == gtk::Orientation::Vertical,
        )?;

        Ok(())
    }

    fn add_toast_message(&self, message: &str, use_markup: bool) {
        /*         let msg = if use_markup {
            let out = self.replace_tags(message);
            Cow::from(out)
        } else {
            Cow::from(message)
        }; */

        let toast = adw::Toast::builder()
            .title(message)
            .use_markup(use_markup)
            .build();

        self.toast_overlay.dismiss_all();
        self.toast_overlay.add_toast(toast);
    }
}

// The central trait for subclassing a GObject
#[glib::object_subclass]
impl ObjectSubclass for UnitPropertiesSelectorDialogImp {
    const NAME: &'static str = "UnitPropertiesSelectorDialog";
    type Type = UnitPropertiesSelectorDialog;
    type ParentType = adw::Window;

    fn class_init(klass: &mut Self::Class) {
        // The layout manager determines how child widgets are laid out.
        klass.bind_template();
        klass.bind_template_callbacks();
    }

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

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

        self.load_window_size();

        let list_store = gio::ListStore::new::<PropertyBrowseItem>();

        let tree_list_model =
            gtk::TreeListModel::new(list_store.clone(), false, false, add_tree_node);

        self.tree_list_model
            .set(tree_list_model.clone())
            .expect("set once only");

        let filter = self.create_filter();

        self.custom_filter
            .set(filter.clone())
            .expect("custom filter set once");

        let filtering_model = gtk::FilterListModel::new(Some(tree_list_model), Some(filter));
        /*         warn!("incremental {}", filtering_model.is_incremental());
        filtering_model.set_incremental(true); */

        let selection_model = gtk::SingleSelection::builder()
            .model(&filtering_model)
            .can_unselect(true)
            .autoselect(false)
            .build();

        self.properties_selector.set_model(Some(&selection_model));

        let factory_interface = gtk::SignalListItemFactory::new();

        factory_interface.connect_setup(|_fac, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();
            item.set_selectable(true);

            let label = gtk::Label::builder().xalign(0.0).build();
            let expander = gtk::TreeExpander::builder().child(&label).build();
            item.set_child(Some(&expander));
        });

        factory_interface.connect_bind(|_fac, item| {
            let item = item.downcast_ref::<gtk::ListItem>().unwrap();

            let expander = item
                .child()
                .unwrap()
                .downcast::<gtk::TreeExpander>()
                .unwrap();

            let label = expander.child().unwrap().downcast::<gtk::Label>().unwrap();

            let tree_list_row = item.item().unwrap().downcast::<gtk::TreeListRow>().unwrap();

            expander.set_list_row(Some(&tree_list_row));

            let property_object = tree_list_row
                .item()
                .and_downcast::<PropertyBrowseItem>()
                .unwrap();

            let interface = property_object.interface();
            let val = if tree_list_row.depth() == 0 {
                interface.split('.').next_back().unwrap_or_default()
            } else {
                ""
            };

            label.set_text(val);
        });
        self.interface_column.set_factory(Some(&factory_interface));

        let factory_property = gtk::SignalListItemFactory::new();
        factory_property.connect_setup(setup);
        factory_property.connect_bind(|_fac, item| {
            bind(item, PropertyBrowseItem::unit_property);
        });
        self.property_column.set_factory(Some(&factory_property));

        let signature_factory = gtk::SignalListItemFactory::new();
        signature_factory.connect_setup(setup);
        signature_factory.connect_bind(|_fac, item| {
            bind(item, |property_object: &PropertyBrowseItem| {
                PropertyBrowseItem::signature(property_object).unwrap_or_default()
            });
        });

        self.signature_column.set_factory(Some(&signature_factory));

        let access_factory = gtk::SignalListItemFactory::new();

        access_factory.connect_setup(setup);

        access_factory.connect_bind(|_fac, item| {
            bind(item, |property_object: &PropertyBrowseItem| {
                PropertyBrowseItem::access(property_object).unwrap_or_default()
            });
        });

        self.access_column.set_factory(Some(&access_factory));

        let unit_properties_selection = self.unit_properties_selection_panel.downgrade();
        let unit_properties_selector = self.obj().downgrade();

        selection_model.connect_selected_item_notify(move |single_selection| {
            let unit_properties_selection = upgrade!(unit_properties_selection);
            let unit_properties_selector = upgrade!(unit_properties_selector);

            debug!(
                "connect_selected_notify idx {}",
                single_selection.selected()
            );

            let Some(object) = single_selection.selected_item() else {
                //warn!("No object selected");
                return;
            };

            let tree_list_row = object.downcast::<gtk::TreeListRow>().unwrap();

            if tree_list_row.is_expandable() {
                tree_list_row.set_expanded(!tree_list_row.is_expanded());
            }

            let property_object = tree_list_row
                .item()
                .and_downcast::<PropertyBrowseItem>()
                .unwrap();

            if property_object.unit_property().is_empty() {
                //single_selection.set_selected(gtk::INVALID_LIST_POSITION);
                single_selection.unselect_item(single_selection.selected());
                debug!("Can't select interface  {property_object:?}");
                return;
            }

            info!("Select {property_object:?}");

            let interface = tree_list_row
                .parent()
                .expect("has a parent")
                .item()
                .and_downcast::<PropertyBrowseItem>()
                .unwrap();

            let new_property_object = PropertyBrowseItem::from_parent(interface, property_object);

            let new_property = new_property_object.unit_property();
            unit_properties_selection.add_new_property(new_property_object);

            unit_properties_selector
                .imp()
                .add_toast_message(&format!("New property {} added", new_property), false);
        });

        glib::spawn_future_local(async move {
            let (sender, receiver) = tokio::sync::oneshot::channel();

            runtime().spawn(async move {
                match systemd::fetch_unit_interface_properties().await {
                    Ok(map) => {
                        if let Err(e) = sender.send(map) {
                            error!("Channel closed unexpectedly: {e:?}");
                        }
                    }
                    Err(err) => error!("Fetch unit properties {err:?}"),
                }
            });

            let Ok(unit_properties_map) = receiver
                .await
                .inspect_err(|err| error!("Tokio channel dropped {err:?}"))
            else {
                return;
            };

            for (inteface, mut properties) in unit_properties_map
                .into_iter()
                .filter(|(k, _)| k.starts_with("org.freedesktop.systemd1"))
            {
                let obj = PropertyBrowseItem::new_interface(inteface);
                properties.sort();
                for property in properties {
                    let prop_object = PropertyBrowseItem::from(property);
                    obj.add_child(prop_object);
                }

                list_store.append(&obj);
            }
        });
    }
}

fn setup(_fac: &gtk::SignalListItemFactory, item: &Object) {
    let item = item.downcast_ref::<gtk::ListItem>().unwrap();
    let label = gtk::Label::builder().xalign(0.0).build();
    item.set_child(Some(&label));
}

fn bind(item: &Object, func: fn(&PropertyBrowseItem) -> String) {
    let item = item.downcast_ref::<gtk::ListItem>().unwrap();

    let widget = item.child();

    let label = widget.and_downcast_ref::<gtk::Label>().unwrap();

    let tree_list_row = item.item().unwrap().downcast::<gtk::TreeListRow>().unwrap();
    let property_object = tree_list_row
        .item()
        .and_downcast::<PropertyBrowseItem>()
        .unwrap();

    let value = func(&property_object);
    label.set_text(&value)
}

fn add_tree_node(object: &Object) -> Option<gio::ListModel> {
    let Some(prop_selector) = object.downcast_ref::<PropertyBrowseItem>() else {
        warn!("object type: {:?} {object:?}", object.type_());
        return None;
    };

    let store = gio::ListStore::new::<PropertyBrowseItem>();

    let binding = prop_selector.children();
    let children = (*binding).as_ref()?;

    for child in children.iter() {
        store.append(child)
    }
    Some(store.into())
}

impl WidgetImpl for UnitPropertiesSelectorDialogImp {}

impl WindowImpl for UnitPropertiesSelectorDialogImp {
    // Save window state right before the window will be closed
    fn close_request(&self) -> glib::Propagation {
        // Save window size
        debug!("Close window");
        if let Err(_err) = self.save_window_context() {
            error!("Failed to save window state");
        }

        self.parent_close_request();
        // Allow to invoke other event handlers
        glib::Propagation::Proceed
    }
}

impl AdwWindowImpl for UnitPropertiesSelectorDialogImp {}

#[cfg(test)]
mod test {
    #[test]
    fn test_last() {
        let var = "org.freedesktop.systemd1.some_stuff";

        let token = var.split('.').next_back();

        assert_eq!(token, Some("some_stuff"))
    }
}
