use gio::prelude::*;
use gio::subclass::prelude::*;
use glib::subclass::Signal;
use once_cell::sync::Lazy;

use crate::torrent::TrTorrent;

mod imp {
    use std::cell::RefCell;

    use super::*;

    #[derive(Debug, Default)]
    pub struct TrTorrentModel {
        pub vec: RefCell<Vec<TrTorrent>>,
    }

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

    impl ObjectImpl for TrTorrentModel {
        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![Signal::builder("status-changed")
                    .flags(glib::SignalFlags::ACTION)
                    .build()]
            });
            SIGNALS.as_ref()
        }
    }

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

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

        fn item(&self, position: u32) -> Option<glib::Object> {
            self.vec
                .borrow()
                .get(position as usize)
                .map(|o| o.clone().upcast::<glib::Object>())
        }
    }

    impl TrTorrentModel {
        pub fn find(&self, hash: String) -> Option<u32> {
            for pos in 0..self.obj().n_items() {
                let obj = self.obj().item(pos)?;
                let s = obj.downcast::<TrTorrent>().unwrap();
                if s.hash() == hash {
                    return Some(pos);
                }
            }
            None
        }
    }
}

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

impl TrTorrentModel {
    pub(crate) fn add_torrent(&self, torrent: &TrTorrent) {
        let imp = self.imp();

        if self.imp().find(torrent.hash()).is_some() {
            warn!("Torrent {:?} already exists in model", torrent.name());
            return;
        }

        // Own scope to avoid "already mutably borrowed: BorrowError"
        let pos = {
            let mut data = imp.vec.borrow_mut();
            data.push(torrent.clone());
            (data.len() - 1) as u32
        };

        self.items_changed(pos, 0, 1);
    }

    pub(crate) fn remove_torrent(&self, torrent: &TrTorrent) {
        let imp = self.imp();

        match self.imp().find(torrent.hash()) {
            Some(pos) => {
                imp.vec.borrow_mut().remove(pos as usize);
                self.items_changed(pos, 1, 0);
            }
            None => warn!("Torrent {:?} not found in model", torrent.name()),
        }
    }

    pub fn torrent_by_hash(&self, hash: String) -> Option<TrTorrent> {
        if let Some(index) = self.imp().find(hash) {
            return Some(self.item(index).unwrap().downcast().unwrap());
        }
        None
    }

    pub(crate) fn clear(&self) {
        let imp = self.imp();
        let len = self.n_items();
        imp.vec.borrow_mut().clear();
        self.items_changed(0, len, 0);
    }

    pub(crate) fn get_hashes(&self) -> Vec<String> {
        let mut hashes = Vec::new();

        for pos in 0..self.n_items() {
            let obj = self.item(pos).unwrap();
            let s = obj.downcast::<TrTorrent>().unwrap();
            hashes.insert(0, s.hash());
        }

        hashes
    }
}

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