use std::cell::{Cell, OnceCell, RefCell};
use std::collections::{HashMap, HashSet};

use gio::prelude::*;
use glib::subclass::prelude::{ObjectSubclass, *};
use glib::{clone, Properties};
use transmission_client::{File, FileStat};

use crate::{TrRelatedModel, TrTorrent};

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type = super::TrFile)]
    pub struct TrFile {
        #[property(get, set, construct_only)]
        torrent: OnceCell<TrTorrent>,

        #[property(get, set, construct_only)]
        id: Cell<i32>,
        #[property(get, set, construct_only)]
        name: OnceCell<String>,
        #[property(get, set, construct_only)]
        title: OnceCell<String>,
        #[property(get, set, construct_only)]
        is_folder: Cell<bool>,
        #[property(get)]
        pub bytes_completed: Cell<i64>,
        #[property(get, set, construct_only)]
        length: Cell<i64>,
        #[property(get, set = Self::set_wanted)]
        pub wanted: Cell<bool>,
        #[property(get)]
        wanted_inconsistent: Cell<bool>,
        #[property(get)]
        related: TrRelatedModel,

        related_wanted: RefCell<HashSet<String>>,
        related_length: RefCell<HashMap<String, i64>>,
        related_bytes_completed: RefCell<HashMap<String, i64>>,
    }

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

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

    impl TrFile {
        fn set_wanted(&self, wanted: bool) {
            let fut = clone!(@weak self as this => async move {
                // Collect ids of files which shall get updated
                let ids = if this.obj().is_folder(){
                    let path = this.obj().name();

                    // We can't use `related()` here, since that wouldn't return all nested folders / files
                    let files = this.obj().torrent().files().related_files_by_path(&path);

                    let mut ids = Vec::new();
                    for file in files {
                        ids.push(file.id());
                    }

                    ids
                }else{
                    vec![this.obj().id()]
                };

                if wanted {
                    this.torrent.get().unwrap().set_wanted_files(ids).await.unwrap();
                }else{
                    this.torrent.get().unwrap().set_unwanted_files(ids).await.unwrap();
                }

                this.wanted.set(wanted);
                this.obj().notify_wanted();
            });
            glib::spawn_future_local(fut);
        }

        pub fn find_title(name: &str) -> String {
            if !name.contains('/') {
                return name.to_string();
            }

            let slashes = name.match_indices('/');
            name[(slashes.clone().last().unwrap().0) + 1..name.len()].to_string()
        }

        /// Update `self` when a related file changes its `wanted` state
        pub fn update_related_wanted(&self, related_file: &super::TrFile) {
            assert!(self.obj().is_folder());

            if related_file.wanted() && !related_file.wanted_inconsistent() {
                self.related_wanted.borrow_mut().insert(related_file.name());
            } else {
                self.related_wanted
                    .borrow_mut()
                    .remove(&related_file.name());
            }

            // Check if this folder is in a inconsistent state
            // (mixed wanted/not wanted related files)
            let files_count = self.obj().related().n_items() as usize;
            let wanted_count = self.related_wanted.borrow().len();

            if files_count == wanted_count {
                self.wanted.set(true);
                self.wanted_inconsistent.set(false);
            } else if wanted_count == 0 {
                self.wanted.set(false);
                self.wanted_inconsistent.set(false);
            } else {
                self.wanted.set(true);
                self.wanted_inconsistent.set(true);
            }

            self.obj().notify_wanted();
            self.obj().notify_wanted_inconsistent();
        }

        /// Update `self` when a related file changes its `length` state
        pub fn update_related_length(&self, related_file: &super::TrFile) {
            assert!(self.obj().is_folder());

            let obj = self.obj();
            let mut related_length = self.related_length.borrow_mut();

            let mut value_changed = false;
            let mut previous_value: i64 = 0;

            if let Some(length) = related_length.get(&related_file.name()) {
                if length != &related_file.length() {
                    value_changed = true;
                    previous_value = *length;
                }
            } else {
                related_length.insert(related_file.name(), related_file.length());
                self.length.set(obj.length() + related_file.length());
                self.obj().notify_length();
            }

            if value_changed {
                related_length.insert(related_file.name(), related_file.length());
                self.length.set(obj.length() - previous_value);
                self.length.set(obj.length() + related_file.length());
                self.obj().notify_length();
            }
        }

        /// Update `self` when a related file changes its `bytes_completed`
        /// state
        pub fn update_related_bytes_completed(&self, related_file: &super::TrFile) {
            assert!(self.obj().is_folder());

            let obj = self.obj();
            let mut related_bytes_completed = self.related_bytes_completed.borrow_mut();

            let mut value_changed = false;
            let mut previous_value: i64 = 0;

            if let Some(bytes_completed) = related_bytes_completed.get(&related_file.name()) {
                if bytes_completed != &related_file.bytes_completed() {
                    value_changed = true;
                    previous_value = *bytes_completed;
                }
            } else {
                related_bytes_completed.insert(related_file.name(), related_file.bytes_completed());
                self.bytes_completed
                    .set(obj.bytes_completed() + related_file.bytes_completed());
                self.obj().notify_bytes_completed();
            }

            if value_changed {
                related_bytes_completed.insert(related_file.name(), related_file.bytes_completed());
                self.bytes_completed
                    .set(obj.bytes_completed() - previous_value);
                self.bytes_completed
                    .set(obj.bytes_completed() + related_file.bytes_completed());
                self.obj().notify_bytes_completed();
            }
        }
    }
}

glib::wrapper! {
    pub struct TrFile(ObjectSubclass<imp::TrFile>);
}

impl TrFile {
    pub(crate) fn from_rpc_file(id: i32, rpc_file: &File, torrent: &TrTorrent) -> Self {
        let name = rpc_file.name.clone();
        let title = imp::TrFile::find_title(&name);

        glib::Object::builder()
            .property("id", id)
            .property("name", &name)
            .property("title", &title)
            .property("length", rpc_file.length)
            .property("is-folder", false)
            .property("torrent", torrent)
            .build()
    }

    pub(crate) fn new_folder(name: &str, torrent: &TrTorrent) -> Self {
        let title = imp::TrFile::find_title(name);

        glib::Object::builder()
            .property("id", -1)
            .property("name", name)
            .property("title", &title)
            .property("is-folder", true)
            .property("torrent", torrent)
            .build()
    }

    /// Add a new related [TrFile] to `self` (which is a folder)
    pub(crate) fn add_related(&self, file: &TrFile) {
        assert!(self.is_folder());
        let imp = self.imp();

        self.related().add_file(file);

        file.connect_notify_local(
            Some("wanted"),
            clone!(@weak imp as this => move |file, _|{
                this.update_related_wanted(file);
            }),
        );
        file.connect_notify_local(
            Some("wanted-inconsistent"),
            clone!(@weak imp as this => move |file, _|{
                this.update_related_wanted(file);
            }),
        );
        imp.update_related_wanted(file);

        file.connect_notify_local(
            Some("length"),
            clone!(@weak imp as this => move |file, _|{
                this.update_related_length(file);
            }),
        );
        imp.update_related_length(file);

        file.connect_notify_local(
            Some("bytes-completed"),
            clone!(@weak imp as this => move |file, _|{
                this.update_related_bytes_completed(file);
            }),
        );
        imp.update_related_bytes_completed(file);
    }

    /// Updates the values of `self` (which is not a folder)
    pub(crate) fn refresh_values(&self, rpc_file_stat: &FileStat) {
        assert!(!self.is_folder());
        let imp = self.imp();

        // bytes_completed
        if imp.bytes_completed.get() != rpc_file_stat.bytes_completed {
            imp.bytes_completed.set(rpc_file_stat.bytes_completed);
            self.notify_bytes_completed();
        }

        // wanted
        if imp.wanted.get() != rpc_file_stat.wanted {
            imp.wanted.set(rpc_file_stat.wanted);
            self.notify_wanted();
        }
    }
}
