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

use async_channel::{Receiver, Sender};
use gio::prelude::*;
use glib::subclass::prelude::{ObjectSubclass, *};
use glib::subclass::Signal;
use glib::{clone, Properties, SourceId};
use once_cell::sync::Lazy;
use transmission_client::{
    Authentication, Client, ClientError, Torrent, TorrentFiles, TorrentPeers,
};
use url::Url;

use crate::{TrAuthentication, TrSession, TrSessionStats, TrTorrent, TrTorrentModel};

mod imp {
    use super::*;

    #[derive(Properties)]
    #[properties(wrapper_type = super::TrClient)]
    pub struct TrClient {
        #[property(get)]
        address: RefCell<String>,
        #[property(get=Self::polling_rate, set=Self::set_polling_rate)]
        polling_rate: Cell<u64>,
        #[property(get)]
        torrents: TrTorrentModel,
        #[property(get)]
        session: OnceCell<TrSession>,
        #[property(get)]
        session_stats: TrSessionStats,
        #[property(get = Self::is_busy)]
        is_busy: std::marker::PhantomData<bool>,
        #[property(get = Self::is_connected)]
        is_connected: std::marker::PhantomData<bool>,

        pub client: RefCell<Option<Client>>,
        pub polling_source_id: RefCell<Option<SourceId>>,
        pub authentication: RefCell<Option<TrAuthentication>>,

        pub do_connect: Cell<bool>,
        pub do_disconnect: Cell<bool>,

        pub sender: Sender<bool>,
        pub receiver: Receiver<bool>,
    }

    impl TrClient {
        fn is_busy(&self) -> bool {
            self.do_connect.get() || self.do_disconnect.get()
        }

        fn is_connected(&self) -> bool {
            self.client.borrow().is_some()
        }
    }

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

        fn new() -> Self {
            let (sender, receiver) = async_channel::bounded(1);

            Self {
                address: RefCell::default(),
                polling_rate: Cell::new(1000),
                torrents: TrTorrentModel::default(),
                session: OnceCell::default(),
                session_stats: TrSessionStats::default(),
                is_busy: Default::default(),
                is_connected: Default::default(),
                client: RefCell::default(),
                polling_source_id: RefCell::default(),
                authentication: RefCell::default(),
                do_connect: Cell::default(),
                do_disconnect: Cell::default(),
                sender,
                receiver,
            }
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for TrClient {
        fn constructed(&self) {
            self.parent_constructed();

            let session = TrSession::new(&self.obj());
            self.session.set(session).unwrap();
        }

        fn signals() -> &'static [Signal] {
            static SIGNALS: Lazy<Vec<Signal>> = Lazy::new(|| {
                vec![
                    Signal::builder("connection-failure").build(),
                    Signal::builder("torrent-added")
                        .param_types([TrTorrent::static_type()])
                        .build(),
                    Signal::builder("torrent-downloaded")
                        .param_types([TrTorrent::static_type()])
                        .build(),
                ]
            });
            SIGNALS.as_ref()
        }
    }

    impl TrClient {
        pub async fn connect_internal(&self, address: String) -> Result<(), ClientError> {
            if self.is_connected() {
                self.obj().disconnect(false).await;
            }

            debug!("Connect to {} ...", address);

            // Check if the address has changed, and delete cached auth information if
            // needed
            if address != self.obj().address() {
                *self.authentication.borrow_mut() = None;
            }

            // Create new client for the rpc communication
            let url = Url::parse(&address).unwrap();
            let client = Client::new(url);

            // Authenticate if we have information...
            if self.authentication.borrow().is_some() {
                self.rpc_auth(Some(client.clone()));
            }

            // Set properties
            *self.address.borrow_mut() = address.clone();
            self.obj().notify_address();

            // Check server version
            let session = client.session().await?;
            debug!(
                "Connected to transmission session version {}",
                session.version
            );

            // Make first poll
            *self.client.borrow_mut() = Some(client.clone());
            self.poll_data(true).await?;
            self.obj().notify_is_connected();

            // Start polling
            self.start_polling();

            Ok(())
        }

        pub fn start_polling(&self) {
            if let Some(id) = self.polling_source_id.borrow_mut().take() {
                warn!("Polling source id wasn't None");
                id.remove();
            }

            let duration = Duration::from_millis(self.polling_rate.get());
            let id = glib::source::timeout_add_local(
                duration,
                clone!(@weak self as this => @default-panic, move ||{
                    let disconnect = this.do_disconnect.get();

                    if disconnect {
                        debug!("Stop polling loop...");

                        // Send message back to the disconnect method to indicate that the
                        // polling loop has been stopped
                        let sender = this.sender.clone();
                        sender.send_blocking(true).unwrap();

                        this.do_disconnect.set(false);
                        this.obj().notify_is_busy();
                    }else{
                        let fut = clone!(@weak this => async move {
                            debug!("Poll... ({}ms)", this.polling_rate.get());
                            this.obj().refresh_data().await;
                        });
                        glib::spawn_future_local(fut);
                    }

                    if disconnect {
                        *this.polling_source_id.borrow_mut() = None;
                        glib::ControlFlow::Break
                    } else {
                        glib::ControlFlow::Continue
                    }

                }),
            );
            *self.polling_source_id.borrow_mut() = Some(id);
        }

        pub fn rpc_auth(&self, rpc_client: Option<Client>) {
            if let Some(auth) = &*self.authentication.borrow() {
                if let Some(rpc_client) = rpc_client {
                    let rpc_auth = Authentication {
                        username: auth.username(),
                        password: auth.password(),
                    };
                    rpc_client.set_authentication(Some(rpc_auth));
                } else {
                    warn!("Unable to authenticate, no rpc connection");
                }
            } else {
                warn!("Unable to authenticate, no information stored");
            }
        }

        pub async fn poll_data(&self, is_initial_poll: bool) -> Result<(), ClientError> {
            if let Some(client) = self.obj().rpc_client() {
                let torrents = client.torrents(None).await?;

                // Transmission assigns each torrent a "global" queue position, which means
                // *every* torrent has a queue position, ignoring the torrent status. Therefore
                // that position doesn't match the actual position of the download / seed queue,
                // so we determine the position and set it as as property of TrTorrent.
                let download_queue = Self::sorted_queue_by_status(&torrents, 3); // 3 -> DownloadWait
                let seed_queue = Self::sorted_queue_by_status(&torrents, 5); // 5 -> SeedWait

                // We have to find out which torrent isn't included anymore,
                // so we can remove it from the model too.
                let mut hashes_delta = self.torrents.get_hashes();

                // Count downloaded torrents so we can use that value for TrSessionStats
                let mut downloaded_torrents = 0;

                for rpc_torrent in torrents {
                    let hash = rpc_torrent.hash_string.clone();
                    let download_queue_pos = Self::queue_pos(&download_queue, &rpc_torrent);
                    let seed_queue_pos = Self::queue_pos(&seed_queue, &rpc_torrent);

                    // Increase downloaded torrents count if it's SeedWait(5) or Seed(6)
                    if rpc_torrent.status == 5 || rpc_torrent.status == 6 {
                        downloaded_torrents += 1;
                    }

                    if let Some(torrent) = self.torrents.torrent_by_hash(hash.clone()) {
                        let id = Some(vec![rpc_torrent.id]);

                        // Update the download / seed queue position
                        torrent.refresh_queue_positions(download_queue_pos, seed_queue_pos);

                        // Only retrieve and deserialize files/peers information
                        // on demand to save resources. Often those information aren't needed.
                        let (torrent_files, torrent_peers) = if torrent.update_extra_info() {
                            (
                                client
                                    .torrents_files(id.clone())
                                    .await?
                                    .first()
                                    .cloned()
                                    .unwrap_or_default(),
                                client
                                    .torrents_peers(id)
                                    .await?
                                    .first()
                                    .cloned()
                                    .unwrap_or_default(),
                            )
                        } else {
                            (TorrentFiles::default(), TorrentPeers::default())
                        };

                        // Check if torrent status or "global" queue position changed
                        if torrent.status() as u32 != rpc_torrent.status as u32
                            || torrent.queue_position() != rpc_torrent.queue_position
                        {
                            // Update torrent properties
                            torrent.refresh_values(&rpc_torrent, &torrent_files, &torrent_peers);

                            // ... yes -> emit torrent model `status-changed` signal
                            self.torrents.emit_by_name::<()>("status-changed", &[]);
                        } else {
                            // Update torrent properties, without emiting the status-changed signal
                            torrent.refresh_values(&rpc_torrent, &torrent_files, &torrent_peers);
                        }

                        let index = hashes_delta.iter().position(|x| *x == hash).unwrap();
                        hashes_delta.remove(index);
                    } else {
                        debug!("Add new torrent: {}", rpc_torrent.name);

                        let torrent = TrTorrent::from_rpc_torrent(&rpc_torrent, &self.obj());
                        torrent.refresh_queue_positions(download_queue_pos, seed_queue_pos);

                        // Set dynamic values
                        torrent.refresh_values(
                            &rpc_torrent,
                            &TorrentFiles::default(),
                            &TorrentPeers::default(),
                        );

                        self.torrents.add_torrent(&torrent);

                        if !is_initial_poll {
                            self.obj().emit_by_name::<()>("torrent-added", &[&torrent]);
                        }
                    }
                }

                // Remove the deltas from the model
                for hash in hashes_delta {
                    let torrent = self.torrents.torrent_by_hash(hash).unwrap();
                    self.torrents.remove_torrent(&torrent);
                }

                let rpc_session = client.session().await?;
                self.obj().session().refresh_values(rpc_session);

                let rpc_session_stats = client.session_stats().await?;
                self.obj()
                    .session_stats()
                    .refresh_values(rpc_session_stats, downloaded_torrents);
            } else {
                warn!("Unable to poll transmission data, no rpc connection.");
            }

            Ok(())
        }

        pub fn sorted_queue_by_status(rpc_torrents: &[Torrent], status_code: i32) -> Vec<Torrent> {
            let mut download_queue: BTreeMap<u64, &Torrent> = BTreeMap::new();

            // Get all torrents with given status code
            for torrent in rpc_torrents {
                if torrent.status == status_code {
                    download_queue.insert(torrent.queue_position.try_into().unwrap(), torrent);
                }
            }

            // Insert the sorted torrents into a vec
            let mut result = Vec::new();
            for torrent in download_queue {
                result.push(torrent.1.clone());
            }

            result
        }

        pub fn queue_pos(queue: &[Torrent], rpc_torrent: &Torrent) -> i32 {
            queue
                .iter()
                .position(|t| t == rpc_torrent)
                .map(|pos| pos as i32)
                .unwrap_or(-1)
        }

        pub fn polling_rate(&self) -> u64 {
            self.polling_rate.get()
        }

        pub fn set_polling_rate(&self, ms: u64) {
            self.polling_rate.set(ms);

            let id = self.polling_source_id.borrow_mut().take();
            if let Some(id) = id {
                id.remove();
                self.start_polling();
            }
        }
    }
}

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

impl Default for TrClient {
    fn default() -> Self {
        Self::new()
    }
}

impl TrClient {
    pub fn new() -> Self {
        glib::Object::new()
    }

    pub async fn test_connectivity(
        address: String,
        auth: Option<TrAuthentication>,
    ) -> Result<(), ClientError> {
        let url = Url::parse(&address).unwrap();
        let rpc_client = Client::new(url);

        if let Some(auth) = auth {
            let rpc_auth = Authentication {
                username: auth.username(),
                password: auth.password(),
            };
            rpc_client.set_authentication(Some(rpc_auth));
        }

        let session = rpc_client.session().await?;
        debug!(
            "Connectivity test to {} succeeded: Transmission daemon version {}",
            address, session.version
        );

        Ok(())
    }

    pub async fn test_port(&self) -> Result<bool, ClientError> {
        if let Some(rpc_client) = self.rpc_client() {
            rpc_client.port_test().await
        } else {
            warn!("Unable to test port, no rpc connection");
            Ok(false)
        }
    }

    pub async fn connect(&self, address: String) -> Result<(), ClientError> {
        let imp = self.imp();

        if self.is_busy() {
            warn!("Client is currently busy, unable to connect to new address.");
            return Ok(());
        }

        // With the do_connect/disconnect variables we avoid race conditions
        // eg. doing another connect attempt, while the client is already busy
        imp.do_connect.set(true);
        self.notify_is_busy();

        // Do the actual connecting work
        let result = imp.connect_internal(address).await;

        // Work is done -> unblock it again.
        imp.do_connect.set(false);
        self.notify_is_busy();

        result
    }

    pub async fn disconnect(&self, close_session: bool) {
        let imp = self.imp();
        if !self.is_connected() {
            warn!("Unable to disconnect, is not connected.");
            return;
        }

        if close_session {
            let client = imp.client.borrow().as_ref().unwrap().clone();
            client.session_close().await.unwrap();
        }

        let _client = imp.client.borrow_mut().take().unwrap();
        self.notify_is_connected();

        // Wait till polling loop has stopped
        imp.do_disconnect.set(true);
        self.notify_is_busy();

        // Wait from message from polling loop to ensure we're not polling anymore.
        let r = imp.receiver.recv().await.unwrap();
        debug!("Stopped polling: {:?}", r);

        // Not connected anymore -> clear torrents model
        self.torrents().clear();

        debug!("Disconnected from transmission session");
    }

    pub fn set_authentication(&self, auth: TrAuthentication) {
        let imp = self.imp();
        *imp.authentication.borrow_mut() = Some(auth);
        imp.rpc_auth(self.rpc_client());
    }

    /// `filename` can be a magnet url or a local file path
    pub async fn add_torrent_by_filename(&self, filename: String) -> Result<(), ClientError> {
        if let Some(client) = self.rpc_client() {
            client.torrent_add_filename(&filename).await?;
            self.refresh_data().await;
        } else {
            warn!("Unable to add new torrent, no rpc connection");
        }

        Ok(())
    }

    /// `metainfo` is the base64 encoded content of a .torrent file
    pub async fn add_torrent_by_metainfo(&self, metainfo: String) -> Result<(), ClientError> {
        if let Some(client) = self.rpc_client() {
            client.torrent_add_metainfo(&metainfo).await?;
            self.refresh_data().await;
        } else {
            warn!("Unable to add new torrent, no rpc connection");
        }

        Ok(())
    }

    pub async fn remove_torrents(
        &self,
        only_downloaded: bool,
        delete_local_data: bool,
    ) -> Result<(), ClientError> {
        let model = self.torrents();
        let mut remove_hashes = vec![];

        for i in 0..model.n_items() {
            let torrent = model.item(i).unwrap().downcast::<TrTorrent>().unwrap();

            if !only_downloaded
                || (torrent.size() == torrent.downloaded() && torrent.downloaded() != 0)
            {
                remove_hashes.push(torrent.hash());
            }
        }

        if let Some(client) = self.rpc_client() {
            client
                .torrent_remove(Some(remove_hashes), delete_local_data)
                .await?;
            self.refresh_data().await;
        } else {
            warn!("Unable to remove torrents, no rpc connection");
        }

        Ok(())
    }

    pub async fn start_torrents(&self) -> Result<(), ClientError> {
        let model = self.torrents();
        let mut start_hashes = vec![];

        // We need to get the hashes of the torrents to be able to start all of them
        for i in 0..model.n_items() {
            let torrent = model.item(i).unwrap().downcast::<TrTorrent>().unwrap();
            start_hashes.push(torrent.hash());
        }

        if let Some(client) = self.rpc_client() {
            client.torrent_start(Some(start_hashes), false).await?;
            self.refresh_data().await;
        } else {
            warn!("Unable to start torrents, no rpc connection")
        }

        Ok(())
    }

    pub async fn stop_torrents(&self) -> Result<(), ClientError> {
        let model = self.torrents();
        let mut stop_hashes = vec![];

        for i in 0..model.n_items() {
            let torrent = model.item(i).unwrap().downcast::<TrTorrent>().unwrap();
            stop_hashes.push(torrent.hash());
        }

        if let Some(client) = self.rpc_client() {
            client.torrent_stop(Some(stop_hashes)).await?;

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

        Ok(())
    }

    /// Polls the latest information without waiting for the next polling
    /// timeout
    pub async fn refresh_data(&self) {
        if let Err(err) = self.imp().poll_data(false).await {
            warn!("Couldn't poll transmission data: {}", err.to_string());
            self.clone().disconnect(false).await;
            self.emit_by_name::<()>("connection-failure", &[]);
        }
    }

    pub(crate) fn rpc_client(&self) -> Option<Client> {
        let imp = self.imp();
        imp.client.borrow().clone()
    }
}
