// Copyright 2026 California Institute of Technology
//
// You should have received a copy of the licensing terms for this
// software included in the file “LICENSE” located in the top-level
// directory of this package. If you did not, you can view a copy at
// http://dcc.ligo.org/M1500244/LICENSE.txt

use crate::{EpicsError, EpicsResult};
pub use epics_rs_sys::{connection_handler_args, event_handler_args};
use std::ffi::{CString, c_int, c_long, c_ulong, c_void};
use std::sync::Mutex;

/// Callbacks run on CA's own threads (preemptive callbacks are enabled),
/// so they must be Send.
pub type ConnectionCallback = Box<dyn FnMut(connection_handler_args) + Send>;
pub type EventCallback = Box<dyn FnMut(event_handler_args) + Send>;

const fn insert_msg_no(number: u32) -> u32 {
    (number << epics_rs_sys::CA_V_MSG_NO) & epics_rs_sys::CA_M_MSG_NO
}

const fn insert_severity(severity: u32) -> u32 {
    (severity << epics_rs_sys::CA_V_SEVERITY) & epics_rs_sys::CA_M_SEVERITY
}

const fn def_msg(severity: u32, number: u32) -> u32 {
    insert_msg_no(number) | insert_severity(severity)
}

pub(crate) const ECA_NORMAL: u32 = def_msg(epics_rs_sys::CA_K_SUCCESS, 0);

fn result(rc: c_int) -> EpicsResult<()> {
    if rc != (ECA_NORMAL as i32) {
        Err(rc.into())
    } else {
        Ok(())
    }
}

pub struct Context {}

impl Context {
    pub fn create() -> EpicsResult<Self> {
        // SAFETY: FFI call with no pointer arguments.  CA allows at most one
        // context per thread; a second create returns an error code, which we
        // surface, so no invariants are violated.
        result(unsafe {
            epics_rs_sys::ca_context_create(
                epics_rs_sys::ca_preemptive_callback_select_ca_enable_preemptive_callback,
            )
        })?;
        Ok(Self {})
    }

    /// Flush buffered requests (channel creates, subscriptions, gets) out to
    /// the servers.  Requests otherwise sit in the send buffer until it fills.
    pub fn flush_io(&self) -> EpicsResult<()> {
        // SAFETY: FFI call with no arguments.
        result(unsafe { epics_rs_sys::ca_flush_io() })
    }

    pub fn create_channel(
        &self,
        name: &str,
        mut callback: Option<ConnectionCallback>,
    ) -> EpicsResult<Channel> {
        let name = CString::new(name)
            .map_err(|_| EpicsError::General("Unable to create channel".to_string()))?;
        let mut chid: epics_rs_sys::chid = std::ptr::null_mut();

        let mut cleanup_f: Option<Box<dyn Fn()>> = None;
        let mut private: *mut c_void = std::ptr::null_mut();

        let callback_wrapper: epics_rs_sys::caCh = if let Some(callback_fn) = callback.take() {
            // Double-box so we have a thin pointer (*mut Box<dyn FnMut(...)>) that
            // fits in a *mut c_void.  The inner Box holds the fat pointer (data +
            // vtable); the outer Box gives us a stable heap address for the raw cast.
            // see Claude and https://users.rust-lang.org/t/why-cant-a-convert-a-mut-c-void-into-mut-dyn-std-read/95780/11
            let raw = Box::into_raw(Box::new(callback_fn));
            private = raw as *mut c_void;

            cleanup_f = Some(Box::new(move || {
                // SAFETY: `raw` came from Box::into_raw above and is reclaimed
                // exactly once: either on the create_channel error path below,
                // or in Channel::drop after ca_clear_channel has stopped CA
                // from invoking the callback.
                let _p: Box<ConnectionCallback> = unsafe { Box::from_raw(raw) };
            }));
            Some(channel_connection_fn_mut_trampoline)
        } else {
            None
        };

        // NOTE if this fails we MUST manually clean up.
        // SAFETY: `name` is a valid NUL-terminated CString, `chid` is a valid
        // out-pointer, and `private` (possibly null) points to the boxed
        // callback, which stays alive until the Channel is dropped.
        if let Err(e) = result(unsafe {
            epics_rs_sys::ca_create_channel(name.as_ptr(), callback_wrapper, private, 0, &mut chid)
        }) {
            if let Some(f) = cleanup_f.take() {
                f();
            }
            return Err(e);
        }
        Ok(Channel {
            chan_id: chid,
            cleanup_connection: cleanup_f,
            cleanup_subscription: None,
            pending_gets: Vec::new(),
        })
    }
}
impl Drop for Context {
    fn drop(&mut self) {
        // SAFETY: destroys the context created in Context::create; called at
        // most once per context since Context is neither Clone nor Copy.
        unsafe {
            epics_rs_sys::ca_context_destroy();
        }
    }
}

/// Did this connection event report the channel coming up?
pub fn connection_up(args: &connection_handler_args) -> bool {
    args.op == epics_rs_sys::CA_OP_CONN_UP as c_long
}

/// Did this get/subscription event deliver valid data?
pub fn event_succeeded(args: &event_handler_args) -> bool {
    args.status == ECA_NORMAL as c_int && !args.dbr.is_null()
}

unsafe extern "C" fn channel_connection_fn_mut_trampoline(args: connection_handler_args) {
    // SAFETY: CA hands us the chid we registered with; ca_puser just reads
    // the user pointer stored on it by ca_create_channel.
    let private = unsafe { epics_rs_sys::ca_puser(args.chid) };
    if !private.is_null() {
        // SAFETY: non-null `private` is the *mut ConnectionCallback installed
        // by create_channel; it stays valid until Channel::drop, which calls
        // ca_clear_channel (stopping callbacks) before freeing it.  CA
        // serializes callbacks per channel, so no aliasing &mut is possible.
        let cb = unsafe { &mut *(private as *mut ConnectionCallback) };
        cb(args);
    }
}

unsafe extern "C" fn channel_subscription_fn_mut_trampoline(args: event_handler_args) {
    let private = args.usr;
    if !private.is_null() {
        // SAFETY: non-null `args.usr` is the *mut EventCallback installed by
        // subscribe_simple; same lifetime argument as the connection
        // trampoline above.
        let cb = unsafe { &mut *(private as *mut EventCallback) };
        cb(args);
    }
}

/// Holder for a one-shot get callback.  The trampoline takes the closure out
/// under the lock, so it is called exactly once and freed the moment it fires;
/// the small empty slot is pruned on the next get or freed in Channel::drop.
type OneShotSlot = Mutex<Option<EventCallback>>;

unsafe extern "C" fn channel_get_fn_once_trampoline(args: event_handler_args) {
    let private = args.usr;
    if !private.is_null() {
        // SAFETY: non-null `args.usr` is the *mut OneShotSlot installed by
        // get_with_callback.  It stays valid until Channel::drop (which runs
        // ca_clear_channel first) or until the owning Channel observes, under
        // this same lock, that the slot has fired.
        let slot = unsafe { &*(private as *const OneShotSlot) };
        let callback = slot.lock().ok().and_then(|mut s| s.take());
        // The slot is not touched past this point: the Channel may free it as
        // soon as it sees the empty Option.  Dropping `callback` after the
        // call frees the closure and everything it captured.
        if let Some(mut callback) = callback {
            callback(args);
        }
    }
}

pub struct Channel {
    chan_id: epics_rs_sys::chid,
    cleanup_connection: Option<Box<dyn Fn()>>,
    cleanup_subscription: Option<Box<dyn Fn()>>,
    // Slots for outstanding one-shot gets, kept separate from subscription
    // state.  Fired slots are already empty (the trampoline consumed the
    // closure); they are pruned on the next get and freed at the latest in
    // drop, so nothing accumulates over a long-lived channel.
    pending_gets: Vec<*mut OneShotSlot>,
}

impl Channel {
    /// The channel's native DBF_* field type, or None while disconnected.
    pub fn field_type(&self) -> Option<u32> {
        // SAFETY: `chan_id` is a live channel from ca_create_channel.
        let field_type = unsafe { epics_rs_sys::ca_field_type(self.chan_id) };
        if field_type < 0 {
            None // TYPENOTCONN
        } else {
            Some(field_type as u32)
        }
    }

    /// The channel's native element count (0 while disconnected).
    pub fn element_count(&self) -> u64 {
        // SAFETY: `chan_id` is a live channel from ca_create_channel.
        unsafe { epics_rs_sys::ca_element_count(self.chan_id) as u64 }
    }

    /// The server host ("host:port"), copied out immediately since the
    /// underlying string can change on a reconnect.  CA returns a static
    /// "<disconnected>" placeholder while the channel is down.
    pub fn host_name(&self) -> String {
        // SAFETY: `chan_id` is a live channel from ca_create_channel; the
        // returned pointer stays valid at least until the channel's
        // connection state next changes, and it is copied before returning.
        let p = unsafe { epics_rs_sys::ca_host_name(self.chan_id) };
        if p.is_null() {
            String::new()
        } else {
            // SAFETY: CA returns a NUL-terminated C string.
            unsafe { std::ffi::CStr::from_ptr(p) }
                .to_string_lossy()
                .into_owned()
        }
    }

    /// Does the client have read access to the channel?
    pub fn read_access(&self) -> bool {
        // SAFETY: `chan_id` is a live channel from ca_create_channel.
        unsafe { epics_rs_sys::ca_read_access(self.chan_id) != 0 }
    }

    /// Does the client have write access to the channel?
    pub fn write_access(&self) -> bool {
        // SAFETY: `chan_id` is a live channel from ca_create_channel.
        unsafe { epics_rs_sys::ca_write_access(self.chan_id) != 0 }
    }

    pub fn subscribe_simple(
        &mut self,
        dbr_type: u32,
        count: u64,
        callback: EventCallback,
    ) -> EpicsResult<()> {
        if self.cleanup_subscription.is_some() {
            return Err(EpicsError::MultipleSubscriptions);
        }
        let raw = Box::into_raw(Box::new(callback));
        let private = raw as *mut c_void;

        let cleanup_f = Box::new(move || {
            // SAFETY: `raw` came from Box::into_raw above and is reclaimed
            // exactly once: either on the subscribe error path below, or in
            // Channel::drop after ca_clear_channel has stopped the callbacks.
            let _p: Box<EventCallback> = unsafe { Box::from_raw(raw) };
        });
        // SAFETY: `chan_id` is a live channel from ca_create_channel, and
        // `private` points to the boxed callback, which stays alive until the
        // Channel is dropped (ca_clear_channel runs before it is freed).
        if let Err(e) = result(unsafe {
            epics_rs_sys::ca_create_subscription(
                dbr_type as epics_rs_sys::chtype,
                count as c_ulong,
                self.chan_id,
                epics_rs_sys::DBE_VALUE as c_long,
                Some(channel_subscription_fn_mut_trampoline),
                private,
                std::ptr::null_mut(),
            )
        }) {
            cleanup_f();
            return Err(e);
        }
        self.cleanup_subscription = Some(cleanup_f);
        Ok(())
    }

    /// Issue a one-shot get; `callback` fires at most once (CA reports a
    /// failure status if the channel disconnects first).  The closure is
    /// freed as soon as it fires.  Gets are isolated from subscription state
    /// and impose no ordering requirements on subscribe_simple.
    pub fn get_with_callback(
        &mut self,
        dbr_type: u32,
        count: u64,
        callback: EventCallback,
    ) -> EpicsResult<()> {
        self.prune_fired_gets();

        let raw = Box::into_raw(Box::new(Mutex::new(Some(callback))));
        let private = raw as *mut c_void;

        // SAFETY: `chan_id` is a live channel from ca_create_channel, and
        // `private` points to the slot, which stays alive until the trampoline
        // has consumed it or Channel::drop frees it after ca_clear_channel.
        if let Err(e) = result(unsafe {
            epics_rs_sys::ca_array_get_callback(
                dbr_type as epics_rs_sys::chtype,
                count as c_ulong,
                self.chan_id,
                Some(channel_get_fn_once_trampoline),
                private,
            )
        }) {
            // SAFETY: `raw` came from Box::into_raw above; the request failed,
            // so CA holds no reference to it and this is its only owner.
            let _p = unsafe { Box::from_raw(raw) };
            return Err(e);
        }
        self.pending_gets.push(raw);
        Ok(())
    }

    /// Free the slots of one-shot gets that have already fired.  A fired slot
    /// (empty Option, observed under its lock) is never touched by CA again.
    fn prune_fired_gets(&mut self) {
        self.pending_gets.retain(|&slot_ptr| {
            // SAFETY: `slot_ptr` was installed by get_with_callback and is
            // still owned by this channel (not yet freed).
            let fired = unsafe { &*slot_ptr }
                .lock()
                .map(|slot| slot.is_none())
                .unwrap_or(false);
            if fired {
                // SAFETY: the trampoline consumed the closure and no longer
                // touches the slot once the Option is empty; CA invokes a get
                // callback at most once, so nothing else references it.
                drop(unsafe { Box::from_raw(slot_ptr) });
            }
            !fired
        });
    }
}

impl Drop for Channel {
    fn drop(&mut self) {
        if !self.chan_id.is_null() {
            // SAFETY: `chan_id` is a live channel from ca_create_channel.
            // Clearing it also cancels its subscriptions and outstanding gets
            // and guarantees no further callbacks, making it safe to free the
            // boxes below.
            unsafe {
                epics_rs_sys::ca_clear_channel(self.chan_id);
            }
        }
        if let Some(f) = self.cleanup_subscription.take() {
            f();
        }
        for slot_ptr in self.pending_gets.drain(..) {
            // SAFETY: installed by get_with_callback and still owned here;
            // ca_clear_channel above guarantees the trampoline can no longer
            // run, so this is the sole remaining reference.
            drop(unsafe { Box::from_raw(slot_ptr) });
        }
        if let Some(f) = self.cleanup_connection.take() {
            f();
        }
    }
}
