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

use gio::prelude::*;
use gio::subclass::prelude::*;
use glib::Properties;
use indexmap::map::IndexMap;
use transmission_client::TorrentFiles;

use crate::{TrFile, TrTorrent};

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type = super::TrFileModel)]
    pub struct TrFileModel {
        /// The top level file
        #[property(get, nullable)]
        pub top_level: RefCell<Option<TrFile>>,
        /// Whether this contains data, and can be consumed
        #[property(get)]
        pub is_ready: Cell<bool>,

        /// All files and folders (Full path + TrFile)
        pub map: RefCell<IndexMap<String, TrFile>>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for TrFileModel {
        const NAME: &'static str = "TrFileModel";
        type ParentType = glib::Object;
        type Type = super::TrFileModel;
        type Interfaces = (gio::ListModel,);
    }

    #[glib::derived_properties]
    impl ObjectImpl for TrFileModel {}

    impl ListModelImpl for TrFileModel {
        fn item_type(&self) -> glib::Type {
            TrFile::static_type()
        }

        fn n_items(&self) -> u32 {
            self.map.borrow().len() as u32
        }

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.map
                .borrow()
                .get_index(position.try_into().unwrap())
                .map(|(_, o)| o.clone().upcast::<glib::Object>())
        }
    }

    impl TrFileModel {
        pub fn add_file(&self, file: &TrFile, torrent: &TrTorrent) {
            if self.obj().file_by_name(&file.name()).is_some() {
                warn!("File {:?} alis_ready exists in model", file.name());
                return;
            }

            let mut map = self.map.borrow_mut();
            let full_path = file.name();

            // Resolve individual folders to make sure that they're added as TrFile to the
            // model eg. "there/can/be/nested/folders/file.txt"
            // -> there, can, be, nested, folders
            let mut folder_names = Vec::new();
            let folder_name = if full_path.contains('/') {
                let slashes = full_path.match_indices('/');
                let folder_name = full_path[..slashes.clone().last().unwrap().0].to_string();

                for slash in slashes {
                    let folder_name = full_path[..slash.0].to_string();
                    folder_names.push(folder_name);
                }

                Some(folder_name)
            } else {
                // No folders, it's a single file torrent
                self.top_level.borrow_mut().replace(file.clone());
                self.obj().notify_top_level();
                None
            };

            // Make sure that nested folders are related to their parent
            let mut parent: Option<TrFile> = None;
            for folder_name in folder_names {
                let folder = if let Some(folder) = map.get(&folder_name) {
                    folder.clone()
                } else {
                    let folder = TrFile::new_folder(&folder_name, torrent);

                    // We know that the folder is related / a subfolder of the parent
                    if let Some(parent) = parent {
                        parent.add_related(&folder);
                    } else {
                        // No parent -> the current file is the toplevel folder
                        self.top_level.borrow_mut().replace(folder.clone());
                        self.obj().notify_top_level();
                    }

                    map.insert(folder_name.clone(), folder.clone());
                    folder
                };

                parent = Some(folder);
            }

            // Now since we made sure that all folders are added,
            // we can take care of the actual file
            map.insert(full_path.clone(), file.clone());
            let pos = (map.len() - 1) as u32;
            self.obj().items_changed(pos, 0, 1);

            if let Some(folder_name) = folder_name {
                // Get the actual folder to add the file as related file (child)
                map.get_mut(&folder_name).unwrap().add_related(file);
            }
        }
    }
}

glib::wrapper! {
    pub struct TrFileModel(ObjectSubclass<imp::TrFileModel>) @implements gio::ListModel;
}

impl TrFileModel {
    pub(crate) fn refresh_files(&self, rpc_files: &TorrentFiles, torrent: &TrTorrent) {
        let is_initial = self.top_level().is_none();

        for (index, rpc_file) in rpc_files.files.iter().enumerate() {
            let rpc_file_stat = rpc_files.file_stats.get(index).cloned().unwrap_or_default();
            if let Some(file) = self.file_by_name(&rpc_file.name) {
                file.refresh_values(&rpc_file_stat);
            } else {
                let file = TrFile::from_rpc_file(index.try_into().unwrap(), rpc_file, torrent);
                file.refresh_values(&rpc_file_stat);

                self.imp().add_file(&file, torrent);
            }
        }

        if is_initial && self.top_level().is_some() {
            self.imp().is_ready.set(true);
            self.notify_is_ready();
        }
    }

    pub(crate) fn related_files_by_path(&self, path: &str) -> Vec<TrFile> {
        let imp = self.imp();
        let mut result = Vec::new();

        for (file_path, file) in &*imp.map.borrow() {
            if file_path.contains(path) && !file.is_folder() {
                result.push(file.clone());
            }
        }

        result
    }

    /// Returns a [TrFile] based on its name (path)
    pub fn file_by_name(&self, name: &str) -> Option<TrFile> {
        self.imp()
            .map
            .borrow()
            .get(name)
            .map(|o| o.clone().downcast().unwrap())
    }

    /// Returns parent folder for [TrFile]
    pub fn parent(&self, folder: &TrFile) -> Option<TrFile> {
        let folder_name = folder.name();
        if folder_name.contains('/') {
            let slashes = folder_name.match_indices('/');
            let parent_name = folder.name()[0..slashes.clone().last().unwrap().0].to_string();

            let parent = self.file_by_name(&parent_name);
            return parent;
        }
        None
    }
}

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