// Copyright (c) 2024 GNOME Foundation Inc.

use std::marker::PhantomData;
use std::os::fd::OwnedFd;
use std::os::unix::net::UnixStream;
use std::sync::{Arc, Mutex, MutexGuard};

use futures_util::FutureExt;
use zbus::zvariant::OwnedObjectPath;

use crate::error::*;
use crate::{SharedMemory, api};

pub struct Loader<T: api::LoaderImplementation> {
    pub loader: PhantomData<T>,
    pub image_id: Mutex<u64>,
}

#[zbus::interface(name = "org.gnome.glycin.Loader")]
impl<T: api::LoaderImplementation> Loader<T> {
    async fn init(
        &self,
        init_request: api::InitRequest,
        #[zbus(connection)] dbus_connection: &zbus::Connection,
    ) -> Result<api::RemoteImage<SharedMemory>, RemoteError> {
        let fd = OwnedFd::from(init_request.fd);
        let stream = UnixStream::from(fd);

        let (loader_state, image_info) = blocking::unblock(|| {
            super::catch_unwind(|| {
                T::load(stream, init_request.mime_type, init_request.details)
                    .map_err(|x| x.into_loader_error())
            })
        })
        .await
        .flatten()?;

        let image_id = {
            let lock = self.image_id.lock();
            let mut image_id = match lock {
                Ok(id) => id,
                Err(err) => return Err(RemoteError::InternalLoaderError(err.to_string())),
            };
            let id = *image_id;
            *image_id = id + 1;
            id
        };

        let path = OwnedObjectPath::try_from(format!("/org/gnome/glycin/image/{image_id}"))
            .internal_error()
            .map_err(|x| x.into_loader_error())?;

        let dbus_image = api::RemoteImage::new(image_info, path.clone());

        dbus_connection
            .object_server()
            .at(
                &path,
                Image {
                    loader_implementation: Arc::new(Mutex::new(Box::new(loader_state))),
                    path: path.clone(),
                    dropped: Default::default(),
                },
            )
            .await
            .internal_error()
            .map_err(|x| x.into_loader_error())?;

        Ok(dbus_image)
    }
}

pub struct Image<T: api::LoaderImplementation> {
    pub loader_implementation: Arc<Mutex<Box<T>>>,
    pub path: OwnedObjectPath,
    dropped: async_lock::OnceCell<()>,
}

impl<T: api::LoaderImplementation> Image<T> {
    pub fn get_loader_state(&self) -> Result<MutexGuard<'_, Box<T>>, RemoteError> {
        self.loader_implementation.lock().map_err(|err| {
            RemoteError::InternalLoaderError(format!(
                "Failed to lock loader state for operation: {err}"
            ))
        })
    }
}

#[zbus::interface(name = "org.gnome.glycin.Image")]
impl<T: api::LoaderImplementation> Image<T> {
    async fn frame(
        &self,
        frame_request: api::FrameRequest,
    ) -> Result<api::Frame<SharedMemory>, RemoteError> {
        let loader_implementation = self.loader_implementation.clone();
        let mut frame_request = blocking::unblock(move || {
            let mut loader_implementation = loader_implementation.lock().map_err(|err| {
                RemoteError::InternalLoaderError(format!(
                    "Failed to lock loader state for operation: {err}"
                ))
            })?;

            super::catch_unwind(move || {
                loader_implementation
                    .specific_frame(frame_request)
                    .map_err(|x| x.into_loader_error())
            })
            .flatten()
        })
        .fuse();

        futures_util::select! {
            result = frame_request => result,
            _ = self.dropped.wait().fuse() => Err(RemoteError::Aborted),
        }
    }

    async fn done(
        &self,
        #[zbus(object_server)] object_server: &zbus::ObjectServer,
    ) -> Result<(), RemoteError> {
        log::debug!("Disconnecting {}", self.path);
        let removed = object_server.remove::<Image<T>, _>(&self.path).await?;
        if removed {
            log::debug!("Removed {}", self.path);
        } else {
            log::error!("Failed to remove {}", self.path);
        }
        let _ = self.dropped.set(()).await;
        Ok(())
    }
}
