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

use gio::prelude::*;
use gio::File;
use glib::subclass::prelude::{ObjectSubclass, *};
use glib::{clone, Properties};
use transmission_client::{ClientError, Torrent, TorrentFiles, TorrentMutator, TorrentPeers};

use crate::{TrClient, TrFileModel, TrTorrentStatus};

mod imp {
    use super::*;

    #[derive(Debug, Default, Properties)]
    #[properties(wrapper_type = super::TrTorrent)]
    pub struct TrTorrent {
        // Static values
        #[property(get, set, construct_only)]
        pub name: OnceCell<String>,
        #[property(get, set, construct_only)]
        pub hash: OnceCell<String>,
        #[property(get, set, construct_only)]
        pub primary_mime_type: OnceCell<String>,
        #[property(get, set, construct_only)]
        pub magnet_link: OnceCell<String>,

        #[property(get, set, construct_only)]
        pub client: OnceCell<TrClient>,

        // Dynamic values
        #[property(get)]
        pub download_dir: RefCell<String>,
        #[property(get)]
        pub size: RefCell<i64>,
        #[property(get, builder(Default::default()))]
        pub status: RefCell<TrTorrentStatus>,
        #[property(get)]
        pub is_stalled: RefCell<bool>,
        #[property(get)]
        pub eta: RefCell<i64>,
        #[property(get)]
        pub error: RefCell<i32>,
        #[property(get)]
        pub error_string: RefCell<String>,
        #[property(get)]
        pub progress: RefCell<f32>,
        #[property(get)]
        pub queue_position: RefCell<i32>,
        #[property(get)]
        pub download_queue_position: RefCell<i32>,
        #[property(get)]
        pub seed_queue_position: RefCell<i32>,

        #[property(get)]
        pub seeders_active: RefCell<i32>,
        #[property(get)]
        pub seeders: RefCell<i32>,
        #[property(get)]
        pub leechers: RefCell<i32>,

        #[property(get)]
        pub downloaded: RefCell<i64>,
        #[property(get)]
        pub uploaded: RefCell<i64>,
        #[property(get)]
        pub download_speed: RefCell<i32>,
        #[property(get)]
        pub upload_speed: RefCell<i32>,

        #[property(get)]
        pub metadata_percent_complete: RefCell<f32>,
        #[property(get)]
        pub files: TrFileModel,

        /// Whether to poll extra info like files or available peers, which data
        /// can be pretty expensive to deserialize
        #[property(get, set = Self::set_update_extra_info)]
        pub update_extra_info: RefCell<bool>,
    }

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

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

    impl TrTorrent {
        fn set_update_extra_info(&self, value: bool) {
            self.update_extra_info.set(value);
            self.obj().notify_update_extra_info();

            let fut = clone!(@weak self as this => async move {
                this.do_poll().await;
            });
            glib::MainContext::default().spawn_local(fut);
        }

        pub async fn do_poll(&self) {
            self.obj().client().refresh_data().await;
        }
    }
}

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

impl TrTorrent {
    pub fn from_rpc_torrent(rpc_torrent: &Torrent, client: &TrClient) -> Self {
        glib::Object::builder()
            .property("hash", &rpc_torrent.hash_string)
            .property("name", &rpc_torrent.name)
            .property("primary-mime-type", &rpc_torrent.primary_mime_type)
            .property("magnet-link", &rpc_torrent.magnet_link)
            .property("client", client)
            .build()
    }

    pub async fn start(&self, bypass_queue: bool) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            client
                .torrent_start(Some(vec![self.hash()]), bypass_queue)
                .await?;
            self.imp().do_poll().await;
        } else {
            warn!("Unable to start torrent, no rpc connection.");
        }

        Ok(())
    }

    pub async fn stop(&self) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            client.torrent_stop(Some(vec![self.hash()])).await?;

            // The daemon needs a short time to stop the torrent
            glib::timeout_future(Duration::from_millis(500)).await;
            self.imp().do_poll().await;
        } else {
            warn!("Unable to stop torrent, no rpc connection.");
        }

        Ok(())
    }

    pub async fn remove(&self, delete_local_data: bool) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            client
                .torrent_remove(Some(vec![self.hash()]), delete_local_data)
                .await?;
            self.imp().do_poll().await;
        } else {
            warn!("Unable to stop torrent, no rpc connection.");
        }

        Ok(())
    }

    pub async fn reannounce(&self) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            client.torrent_reannounce(Some(vec![self.hash()])).await?;
            self.imp().do_poll().await;
        } else {
            warn!("Unable to stop torrent, no rpc connection.");
        }

        Ok(())
    }

    pub async fn set_location(&self, location: File, move_data: bool) -> Result<(), ClientError> {
        let path = location.path().unwrap().to_str().unwrap().to_string();

        if let Some(client) = self.client().rpc_client() {
            client
                .torrent_set_location(Some(vec![self.hash()]), path, move_data)
                .await?;
            self.imp().do_poll().await;
        } else {
            warn!("Unable to set torrent location, no rpc connection.");
        }

        Ok(())
    }

    pub(crate) async fn set_wanted_files(&self, wanted: Vec<i32>) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            let mutator = TorrentMutator {
                files_wanted: Some(wanted),
                ..Default::default()
            };
            client.torrent_set(Some(vec![self.hash()]), mutator).await?;
        } else {
            warn!("Unable to update files, no rpc connection.");
        }

        Ok(())
    }

    pub(crate) async fn set_unwanted_files(&self, wanted: Vec<i32>) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            let mutator = TorrentMutator {
                files_unwanted: Some(wanted),
                ..Default::default()
            };
            client.torrent_set(Some(vec![self.hash()]), mutator).await?;
        } else {
            warn!("Unable to update files, no rpc connection.");
        }

        Ok(())
    }

    pub(crate) fn refresh_values(
        &self,
        rpc_torrent: &Torrent,
        rpc_files: &TorrentFiles,
        rpc_peers: &TorrentPeers,
    ) {
        let imp = self.imp();

        // download_dir
        if *imp.download_dir.borrow() != rpc_torrent.download_dir {
            *imp.download_dir.borrow_mut() = rpc_torrent.download_dir.clone();
            self.notify_download_dir();
        }

        // size
        if *imp.size.borrow() != rpc_torrent.size_when_done {
            *imp.size.borrow_mut() = rpc_torrent.size_when_done;
            self.notify_size();
        }

        // status
        if *imp.status.borrow() as u32 != rpc_torrent.status as u32 {
            let status = TrTorrentStatus::try_from(rpc_torrent.status as u32).unwrap();

            // Check if status changed from "Downloading" to "Seeding"
            // and emit TrClient `torrent-downloaded` signal
            if self.status() == TrTorrentStatus::Download
                && (status == TrTorrentStatus::SeedWait || status == TrTorrentStatus::Seed)
            {
                imp.client
                    .get()
                    .unwrap()
                    .emit_by_name::<()>("torrent-downloaded", &[&self]);
            }

            *imp.status.borrow_mut() = status;
            self.notify_status();
        }

        // is_stalled
        if *imp.is_stalled.borrow() as u32 != rpc_torrent.is_stalled as u32 {
            *imp.is_stalled.borrow_mut() = rpc_torrent.is_stalled;
            self.notify_is_stalled();
        }

        // eta
        if *imp.eta.borrow() != rpc_torrent.eta {
            *imp.eta.borrow_mut() = rpc_torrent.eta;
            self.notify_eta();
        }

        // error
        if *imp.error.borrow() != rpc_torrent.error {
            *imp.error.borrow_mut() = rpc_torrent.error;
            self.notify_error();
        }

        // error-string
        if *imp.error_string.borrow() != rpc_torrent.error_string {
            *imp.error_string.borrow_mut() = rpc_torrent.error_string.clone();
            self.notify_error_string();
        }

        // progress
        if *imp.progress.borrow() != rpc_torrent.percent_done {
            *imp.progress.borrow_mut() = rpc_torrent.percent_done;
            self.notify_progress();
        }

        // queue_position
        if *imp.queue_position.borrow() != rpc_torrent.queue_position {
            *imp.queue_position.borrow_mut() = rpc_torrent.queue_position;
            self.notify_queue_position();
        }

        // downloaded
        if *imp.downloaded.borrow() != rpc_torrent.have_valid {
            *imp.downloaded.borrow_mut() = rpc_torrent.have_valid;
            self.notify_downloaded();
        }

        // uploaded
        if *imp.uploaded.borrow() != rpc_torrent.uploaded_ever {
            *imp.uploaded.borrow_mut() = rpc_torrent.uploaded_ever;
            self.notify_uploaded();
        }

        // downloaded
        if *imp.download_speed.borrow() != rpc_torrent.rate_download {
            *imp.download_speed.borrow_mut() = rpc_torrent.rate_download;
            self.notify_download_speed();
        }

        // uploaded
        if *imp.upload_speed.borrow() != rpc_torrent.rate_upload {
            *imp.upload_speed.borrow_mut() = rpc_torrent.rate_upload;
            self.notify_upload_speed();
        }

        // metadata_percent_complete
        if *imp.metadata_percent_complete.borrow() != rpc_torrent.metadata_percent_complete {
            *imp.metadata_percent_complete.borrow_mut() = rpc_torrent.metadata_percent_complete;
            self.notify_metadata_percent_complete();
        }

        // Torrent peers

        // seeders_active
        if *imp.seeders_active.borrow() != rpc_peers.peers_sending_to_us {
            *imp.seeders_active.borrow_mut() = rpc_peers.peers_sending_to_us;
            self.notify("seeders_active");
        }

        // seeders
        if *imp.seeders.borrow() != rpc_peers.peers_connected {
            *imp.seeders.borrow_mut() = rpc_peers.peers_connected;
            self.notify("seeders");
        }

        // leechers
        if *imp.leechers.borrow() != rpc_peers.peers_getting_from_us {
            *imp.leechers.borrow_mut() = rpc_peers.peers_getting_from_us;
            self.notify("leechers");
        }

        // Torrent files
        self.files().refresh_files(rpc_files, self);
    }

    pub(crate) fn refresh_queue_positions(&self, download_queue_pos: i32, seed_queue_pos: i32) {
        let imp = self.imp();

        // download_queue_position
        if *imp.download_queue_position.borrow() != download_queue_pos {
            *imp.download_queue_position.borrow_mut() = download_queue_pos;
            self.notify_download_queue_position();
        }

        // seed_queue_position
        if *imp.seed_queue_position.borrow() != seed_queue_pos {
            *imp.seed_queue_position.borrow_mut() = seed_queue_pos;
            self.notify_seed_queue_position();
        }
    }

    pub async fn set_queue_position(&self, pos: i32) -> Result<(), ClientError> {
        if let Some(client) = self.client().rpc_client() {
            let mutator = TorrentMutator {
                queue_position: Some(pos),
                ..Default::default()
            };
            client.torrent_set(Some(vec![self.hash()]), mutator).await?;
        } else {
            warn!("Unable set queue position, no rpc connection.");
        }

        Ok(())
    }

    pub async fn set_download_queue_position(&self, pos: i32) -> Result<(), ClientError> {
        let torrents = self.client().torrents();

        for i in 0..torrents.n_items() {
            let torrent: TrTorrent = torrents.item(i).unwrap().downcast().unwrap();
            if torrent.download_queue_position() == pos {
                self.set_queue_position(torrent.queue_position()).await?;
                break;
            }
        }
        self.imp().do_poll().await;

        Ok(())
    }

    pub async fn set_seed_queue_position(&self, pos: i32) -> Result<(), ClientError> {
        let torrents = self.client().torrents();

        for i in 0..torrents.n_items() {
            let torrent: TrTorrent = torrents.item(i).unwrap().downcast().unwrap();
            if torrent.seed_queue_position() == pos {
                self.set_queue_position(torrent.queue_position()).await?;
                break;
            }
        }
        self.imp().do_poll().await;

        Ok(())
    }
}
