use core::ffi::c_void;
use std::sync::Arc;

use ffi_sdk::BoxedAttachmentHandle;
use_prelude!();

use crate::{
    ditto::{DittoHandleWrapper, WeakDittoHandleWrapper},
    error::DittoError,
    store::{
        ditto_attachment::DittoAttachment, ditto_attachment_fetch_event::DittoAttachmentFetchEvent,
        ditto_attachment_token::DittoAttachmentToken,
    },
};

/// These objects are returned by calls to
/// [`fetch_attachment`](crate::store::collection::Collection::fetch_attachment) on `Collection`s.
/// They must be kept alive for the fetching of the attachment to proceed and for you to be notified
/// once the status of the fetch request has changed.
pub struct DittoAttachmentFetcher<'a> {
    context: Arc<DittoAttachmentFetcherCtx<'a>>,
    cancel_token: u64,
}

impl<'a> DittoAttachmentFetcher<'a> {
    /// Create a new DittoAttachmentFetcher
    pub(crate) fn new(
        token: DittoAttachmentToken,
        ditto: DittoHandleWrapper,
        on_fetch_event: impl Fn(DittoAttachmentFetchEvent) + Send + Sync + 'a,
    ) -> Result<Self, DittoError> {
        let context = DittoAttachmentFetcherCtx::new(
            token.clone(),
            Arc::downgrade(&ditto),
            Box::new(on_fetch_event),
        );
        let arc_context = Arc::new(context);
        let raw_context = Arc::as_ptr(&arc_context) as *mut c_void;

        let cancel_token = unsafe {
            ffi_sdk::ditto_resolve_attachment(
                &ditto,
                token.id.as_ref().into(),
                raw_context,
                Some(DittoAttachmentFetcherCtx::retain),
                Some(DittoAttachmentFetcherCtx::release),
                DittoAttachmentFetcherCtx::on_complete_cb,
                DittoAttachmentFetcherCtx::on_progress_cb,
                DittoAttachmentFetcherCtx::on_deleted_cb,
            )
            .ok()?
        };

        Ok(Self {
            context: arc_context,
            cancel_token,
        })
    }
}

impl<'a> crate::observer::Observer for DittoAttachmentFetcher<'a> {}

impl<'a> Drop for DittoAttachmentFetcher<'a> {
    fn drop(&mut self) {
        if let Some(ditto) = self.context.ditto.upgrade() {
            let ret = unsafe {
                ffi_sdk::ditto_cancel_resolve_attachment(
                    &ditto,
                    self.context.token.id.as_ref().into(),
                    self.cancel_token,
                )
            };

            if ret != 0 {
                log::warn!("Failed to clean up attachment fetcher.",);
            }
        }
    }
}

struct DittoAttachmentFetcherCtx<'a> {
    token: DittoAttachmentToken,
    ditto: WeakDittoHandleWrapper,
    on_fetch_event: Box<dyn Fn(DittoAttachmentFetchEvent) + Send + Sync + 'a>,
}

impl<'a> DittoAttachmentFetcherCtx<'a> {
    fn new(
        token: DittoAttachmentToken,
        ditto: WeakDittoHandleWrapper,
        on_fetch_event: Box<dyn Fn(DittoAttachmentFetchEvent) + Send + Sync + 'a>,
    ) -> Self {
        Self {
            token,
            ditto,
            on_fetch_event,
        }
    }

    pub(crate) unsafe extern "C" fn retain(ctx: *mut c_void) {
        let ptr = ctx.cast::<DittoAttachmentFetcherCtx<'_>>();
        Arc::increment_strong_count(ptr);
    }

    pub(crate) unsafe extern "C" fn release(ctx: *mut c_void) {
        let ptr = ctx.cast::<DittoAttachmentFetcherCtx<'_>>();
        Arc::decrement_strong_count(ptr);
    }

    pub(crate) unsafe extern "C" fn on_complete_cb(
        ctx: *mut c_void,
        attachment_handle: BoxedAttachmentHandle,
    ) {
        let ctx_ref = ctx
            .cast::<DittoAttachmentFetcherCtx<'_>>()
            .as_ref()
            .expect("got null");

        let ditto_attachment = DittoAttachment::new_with_token(
            ctx_ref.token.clone(),
            ctx_ref.ditto.clone(),
            attachment_handle,
        );
        let event = DittoAttachmentFetchEvent::Completed {
            attachment: ditto_attachment,
        };
        (ctx_ref.on_fetch_event)(event);
    }

    pub(crate) unsafe extern "C" fn on_progress_cb(
        ctx: *mut c_void,
        downloaded_bytes: u64,
        total_bytes: u64,
    ) {
        let ctx_ref = ctx
            .cast::<DittoAttachmentFetcherCtx<'_>>()
            .as_ref()
            .expect("got null");

        let event = DittoAttachmentFetchEvent::Progress {
            downloaded_bytes,
            total_bytes,
        };
        (ctx_ref.on_fetch_event)(event);
    }

    pub(crate) unsafe extern "C" fn on_deleted_cb(ctx: *mut c_void) {
        let ctx_ref = ctx
            .cast::<DittoAttachmentFetcherCtx<'_>>()
            .as_ref()
            .expect("got null");

        let event = DittoAttachmentFetchEvent::Deleted;
        (ctx_ref.on_fetch_event)(event);
    }
}
