use std::cell::RefCell;

use futures_channel::{
    mpsc::{Receiver, Sender},
    oneshot,
};
use futures_util::StreamExt;
use zbus::names::WellKnownName;
use zbus::zvariant::ObjectPath;

use crate::{ResultID, ResultMeta, SearchProviderImpl};

enum Action {
    ActivateResult(ResultID, Vec<String>, u32),
    InitialResult(Vec<String>, oneshot::Sender<Vec<String>>),
    SubsearchResult(Vec<ResultID>, Vec<String>, oneshot::Sender<Vec<String>>),
    ResultsMeta(Vec<ResultID>, oneshot::Sender<Vec<ResultMeta>>),
    LaunchSearch(Vec<String>, u32),
}

///  The main entry for using the search-provider crate.
pub struct SearchProvider<T>
where
    T: SearchProviderImpl + 'static,
{
    receiver: RefCell<Option<Receiver<Action>>>,
    imp: T,
}

impl<T> SearchProvider<T>
where
    T: SearchProviderImpl + 'static,
{
    pub async fn new<N: TryInto<WellKnownName<'static>>, P: TryInto<ObjectPath<'static>>>(
        imp: T,
        name: N,
        path: P,
    ) -> zbus::Result<Self>
    where
        zbus::Error: From<<N as TryInto<WellKnownName<'static>>>::Error>,
        zbus::Error: From<<P as TryInto<ObjectPath<'static>>>::Error>,
    {
        let (sender, receiver) = futures_channel::mpsc::channel(10);
        let iface = SearchProviderInterface::new(sender);
        let cnx = zbus::Connection::session().await.unwrap();
        let object_server = cnx.object_server();

        // Work-around zbus caching properties by default which is not allowed by the flatpak sandbox (it seems?)
        let proxy = zbus::fdo::DBusProxy::builder(&cnx)
            .cache_properties(zbus::proxy::CacheProperties::No)
            .build()
            .await?;
        proxy
            .request_name(
                name.try_into()?,
                zbus::fdo::RequestNameFlags::ReplaceExisting.into(),
            )
            .await?;

        object_server.at(path.try_into()?, iface).await?;
        let mut provider = Self {
            receiver: RefCell::new(Some(receiver)),
            imp,
        };
        provider.wait().await?;
        Ok(provider)
    }

    async fn wait(&mut self) -> zbus::fdo::Result<()> {
        let mut receiver = self.receiver.borrow_mut().take().unwrap();
        loop {
            let response = receiver.next().await;
            match response {
                Some(Action::ActivateResult(identifier, terms, timestamp)) => {
                    self.imp.activate_result(identifier, &terms, timestamp);
                }
                Some(Action::InitialResult(terms, sender)) => {
                    let results = self.imp.initial_result_set(&terms);
                    let _ = sender.send(results);
                }
                Some(Action::SubsearchResult(previous_terms, terms, sender)) => {
                    let results = self.imp.subsearch_result_set(&previous_terms, &terms);
                    let _ = sender.send(results);
                }
                Some(Action::ResultsMeta(identifiers, sender)) => {
                    let results = self.imp.result_metas(identifiers.as_slice());
                    let _ = sender.send(results);
                }
                Some(Action::LaunchSearch(terms, timestamp)) => {
                    self.imp.launch_search(&terms, timestamp);
                }
                None => (),
            }
        }
    }
}

struct SearchProviderInterface {
    sender: Sender<Action>,
}

impl SearchProviderInterface {
    pub fn new(sender: Sender<Action>) -> Self {
        Self { sender }
    }
}

#[zbus::interface(name = "org.gnome.Shell.SearchProvider2")]
impl SearchProviderInterface {
    // ActivateResult
    async fn activate_result(
        &mut self,
        identifier: ResultID,
        terms: Vec<String>,
        timestamp: u32,
    ) -> zbus::fdo::Result<()> {
        let _ = self
            .sender
            .try_send(Action::ActivateResult(identifier, terms, timestamp));
        Ok(())
    }

    // GetInitialResultSet
    async fn get_initial_result_set(
        &mut self,
        terms: Vec<String>,
    ) -> zbus::fdo::Result<Vec<ResultID>> {
        let (sender, receiver) = futures_channel::oneshot::channel();
        let _ = self.sender.try_send(Action::InitialResult(terms, sender));
        let response = receiver.await.unwrap();
        Ok(response)
    }

    // GetSubsearchResultSet
    async fn get_subsearch_result_set(
        &mut self,
        previous_results: Vec<ResultID>,
        terms: Vec<String>,
    ) -> zbus::fdo::Result<Vec<ResultID>> {
        let (sender, receiver) = futures_channel::oneshot::channel();
        let _ = self
            .sender
            .try_send(Action::SubsearchResult(previous_results, terms, sender));
        let response = receiver.await.unwrap();
        Ok(response)
    }

    // GetResultMetas
    async fn get_result_metas(
        &mut self,
        identifiers: Vec<ResultID>,
    ) -> zbus::fdo::Result<Vec<ResultMeta>> {
        let (sender, receiver) = futures_channel::oneshot::channel();
        let _ = self
            .sender
            .try_send(Action::ResultsMeta(identifiers, sender));
        let response = receiver.await.unwrap();
        Ok(response)
    }

    // LaunchSearch
    async fn launch_search(&mut self, terms: Vec<String>, timestamp: u32) -> zbus::fdo::Result<()> {
        let _ = self.sender.try_send(Action::LaunchSearch(terms, timestamp));
        Ok(())
    }
}
