use objc2::{Message, extern_protocol, msg_send, rc::Retained, runtime::ProtocolObject};
use objc2_foundation::{NSError, NSObjectProtocol, NSString};

use crate::{
    MTLCommandBufferHandler, MTLCommandBufferStatus, MTLCommandQueue, MTLDevice, MTLDrawable,
    MTLEvent, MTLLogContainer,
};

extern_protocol!(
    /// A serial list of commands for the device to execute.
    ///
    /// Availability: macOS 10.11+, iOS 8.0+
    pub unsafe trait MTLCommandBuffer: NSObjectProtocol {
        /// The device this resource was created against.
        #[unsafe(method(device))]
        #[unsafe(method_family = none)]
        fn device(&self) -> Retained<ProtocolObject<dyn MTLDevice>>;

        /// The command queue this command buffer was created from.
        #[unsafe(method(commandQueue))]
        #[unsafe(method_family = none)]
        fn command_queue(&self) -> Retained<ProtocolObject<dyn MTLCommandQueue>>;

        /// If YES, this command buffer holds strong references to objects needed to execute this command buffer.
        #[unsafe(method(retainedReferences))]
        #[unsafe(method_family = none)]
        fn retained_references(&self) -> bool;

        /// Append this command buffer to the end of its MTLCommandQueue.
        #[unsafe(method(enqueue))]
        #[unsafe(method_family = none)]
        fn enqueue(&self);

        /// Commit a command buffer so it can be executed as soon as possible.
        #[unsafe(method(commit))]
        #[unsafe(method_family = none)]
        fn commit(&self);

        /// Synchronously wait for this command buffer to be scheduled.
        #[unsafe(method(waitUntilScheduled))]
        #[unsafe(method_family = none)]
        fn wait_until_scheduled(&self);

        /// Synchronously wait for this command buffer to complete.
        #[unsafe(method(waitUntilCompleted))]
        #[unsafe(method_family = none)]
        fn wait_until_completed(&self);

        /// If an error occurred during execution, the NSError may contain more details about the problem.
        #[unsafe(method(error))]
        #[unsafe(method_family = none)]
        fn error(&self) -> Option<Retained<NSError>>;

        /// Status reports the current stage in the lifetime of MTLCommandBuffer,
        /// as it proceeds to enqueued, committed, scheduled, and completed.
        #[unsafe(method(status))]
        #[unsafe(method_family = none)]
        fn status(&self) -> MTLCommandBufferStatus;

        /// Logs generated by the command buffer during execution of the GPU commands.
        /// Valid after GPU execution is completed.
        ///
        /// Availability: macOS 11.0+, iOS 14.0+
        #[unsafe(method(logs))]
        #[unsafe(method_family = none)]
        fn logs(&self) -> Retained<ProtocolObject<dyn MTLLogContainer>>;

        /// Add a drawable present that will be invoked when this command buffer has been scheduled for execution.
        #[unsafe(method(presentDrawable:))]
        #[unsafe(method_family = none)]
        fn present_drawable(&self, drawable: &ProtocolObject<dyn MTLDrawable>);

        /// Encodes a command that pauses execution of this command buffer until the specified event reaches a given value.
        ///
        /// This method may only be called if there is no current command encoder on the receiver.
        ///
        /// Availability: macOS 10.14+, iOS 12.0+
        #[unsafe(method(encodeWaitForEvent:value:))]
        #[unsafe(method_family = none)]
        fn encode_wait_for_event_value(&self, event: &ProtocolObject<dyn MTLEvent>, value: u64);

        /// Encodes a command that signals an event with a given value.
        ///
        /// This method may only be called if there is no current command encoder on the receiver.
        ///
        /// Availability: macOS 10.14+, iOS 12.0+
        #[unsafe(method(encodeSignalEvent:value:))]
        #[unsafe(method_family = none)]
        fn encode_signal_event_value(&self, event: &ProtocolObject<dyn MTLEvent>, value: u64);

        /// Returns a compute command encoder to encode into this command buffer.
        #[unsafe(method(computeCommandEncoder))]
        #[unsafe(method_family = none)]
        fn new_compute_command_encoder(
            &self,
        ) -> Option<Retained<ProtocolObject<dyn crate::MTLComputeCommandEncoder>>>;

        /// Returns a blit command encoder to encode into this command buffer.
        #[unsafe(method(blitCommandEncoder))]
        #[unsafe(method_family = none)]
        fn new_blit_command_encoder(
            &self,
        ) -> Option<Retained<ProtocolObject<dyn crate::MTLBlitCommandEncoder>>>;
    }
);

#[allow(unused)]
pub trait MTLCommandBufferExt: MTLCommandBuffer + Message {
    /// A string to help identify this object.
    fn label(&self) -> Option<String>;
    /// Sets a string to help identify this object.
    fn set_label(&self, label: Option<&str>);
    /// Adds a block to be called when this command buffer has been scheduled for execution.
    fn add_scheduled_handler(&self, handler: &MTLCommandBufferHandler);
    /// Adds a block to be called when this command buffer has completed execution.
    fn add_completed_handler(&self, handler: &MTLCommandBufferHandler);
    /// Push a new named string onto a stack of string labels.
    fn push_debug_group(&self, string: &str);
    /// Pop the latest named string off of the stack.
    fn pop_debug_group(&self);
    /// The host time, in seconds, when the CPU began scheduling this command buffer for execution.
    ///
    /// Returns `None` if the command buffer has not been scheduled yet.
    ///
    /// Availability: macOS 10.15+, iOS 10.3+
    fn kernel_start_time(&self) -> Option<f64>;
    /// The host time, in seconds, when the CPU finished scheduling this command buffer for execution.
    ///
    /// Returns `None` if the command buffer has not been scheduled yet.
    ///
    /// Availability: macOS 10.15+, iOS 10.3+
    fn kernel_end_time(&self) -> Option<f64>;
    /// The host time, in seconds, when the GPU started executing this command buffer.
    ///
    /// Returns `None` if the command buffer has not started executing yet.
    /// This usually can be called in command buffer completion handler.
    ///
    /// Availability: macOS 10.15+, iOS 10.3+
    fn gpu_start_time(&self) -> Option<f64>;
    /// The host time, in seconds, when the GPU finished executing this command buffer.
    ///
    /// Returns `None` if the GPU has not finished executing yet.
    /// This usually can be called in command buffer completion handler.
    ///
    /// Availability: macOS 10.15+, iOS 10.3+
    fn gpu_end_time(&self) -> Option<f64>;
}

impl MTLCommandBufferExt for ProtocolObject<dyn MTLCommandBuffer> {
    fn label(&self) -> Option<String> {
        let label: Option<Retained<NSString>> = unsafe { msg_send![self, label] };
        label.map(|s| s.to_string())
    }

    fn set_label(&self, label: Option<&str>) {
        unsafe {
            let _: () = msg_send![self, setLabel: label.map(NSString::from_str).as_deref()];
        }
    }

    fn add_scheduled_handler(&self, handler: &MTLCommandBufferHandler) {
        unsafe {
            let _: () = msg_send![self, addScheduledHandler: &**handler];
        }
    }

    fn add_completed_handler(&self, handler: &MTLCommandBufferHandler) {
        unsafe {
            let _: () = msg_send![self, addCompletedHandler: &**handler];
        }
    }

    fn push_debug_group(&self, string: &str) {
        unsafe {
            let _: () = msg_send![self, pushDebugGroup: &*NSString::from_str(string)];
        }
    }

    fn pop_debug_group(&self) {
        unsafe {
            let _: () = msg_send![self, popDebugGroup];
        }
    }

    fn kernel_start_time(&self) -> Option<f64> {
        let start_time: f64 = unsafe { msg_send![self, kernelStartTime] };
        if start_time > 0.0 {
            Some(start_time)
        } else {
            None
        }
    }

    fn kernel_end_time(&self) -> Option<f64> {
        let end_time: f64 = unsafe { msg_send![self, kernelEndTime] };
        if end_time > 0.0 {
            Some(end_time)
        } else {
            None
        }
    }

    fn gpu_start_time(&self) -> Option<f64> {
        let start_time: f64 = unsafe { msg_send![self, GPUStartTime] };
        if start_time > 0.0 {
            Some(start_time)
        } else {
            None
        }
    }

    fn gpu_end_time(&self) -> Option<f64> {
        let end_time: f64 = unsafe { msg_send![self, GPUEndTime] };
        if end_time > 0.0 {
            Some(end_time)
        } else {
            None
        }
    }
}
