use crate::sync::RwLock;
use std::{
    collections::HashSet,
    fmt, ptr,
    sync::atomic::{AtomicPtr, AtomicUsize, Ordering},
};
use tracing_core::{callsite, Metadata};

pub(crate) struct Callsites<const MAX_CALLSITES: usize> {
    ptrs: [AtomicPtr<Metadata<'static>>; MAX_CALLSITES],
    len: AtomicUsize,
    spill: RwLock<HashSet<callsite::Identifier>>,
}

impl<const MAX_CALLSITES: usize> Callsites<MAX_CALLSITES> {
    #[track_caller]
    pub(crate) fn insert(&self, callsite: &'static Metadata<'static>) {
        // The callsite may already have been inserted, if the callsite cache
        // was invalidated and is being rebuilt. In that case, don't insert it
        // again.'
        if self.contains(callsite) {
            return;
        }

        let idx = self.len.fetch_add(1, Ordering::AcqRel);
        if idx < MAX_CALLSITES {
            // If there's still room in the callsites array, stick the address
            // in there.
            self.ptrs[idx]
                .compare_exchange(
                    ptr::null_mut(),
                    callsite as *const _ as *mut _,
                    Ordering::AcqRel,
                    Ordering::Acquire,
                )
                .expect("a callsite would have been clobbered by `insert` (this is a bug)");
        } else {
            // Otherwise, we've filled the callsite array (sad!). Spill over
            // into a hash set.
            self.spill.write().insert(callsite.callsite());
        }
    }

    pub(crate) fn contains(&self, callsite: &'static Metadata<'static>) -> bool {
        let mut start = 0;
        let mut len = self.len.load(Ordering::Acquire);
        loop {
            for cs in &self.ptrs[start..len] {
                if ptr::eq(cs.load(Ordering::Acquire), callsite) {
                    return true;
                }
            }

            // Did the length change while we were iterating over the callsite array?
            let new_len = self.len.load(Ordering::Acquire);
            if new_len > len {
                // If so, check again to see if the callsite is contained in any
                // callsites that were pushed since the last time we loaded `self.len`.
                start = len;
                len = new_len;
                continue;
            }

            // If the callsite array is not full, we have checked everything.
            if len <= MAX_CALLSITES {
                return false;
            }

            // Otherwise, we may have spilled over to the slower fallback hash
            // set. Check that.
            return self.check_spill(callsite);
        }
    }

    #[cold]
    fn check_spill(&self, callsite: &'static Metadata<'static>) -> bool {
        self.spill.read().contains(&callsite.callsite())
    }
}

impl<const MAX_CALLSITES: usize> Default for Callsites<MAX_CALLSITES> {
    fn default() -> Self {
        // It's necessary to use a `const` value here to initialize the array,
        // because `AtomicPtr` is not `Copy`.
        //
        // Clippy does not like when `const` values have interior mutability. See:
        // https://rust-lang.github.io/rust-clippy/master/index.html#declare_interior_mutable_const
        //
        // This is a warning because the const value is always copied when it's
        // used, so mutations to it will not be reflected in the `const` itself.
        // In some cases, this is a footgun (when you meant to use a `static`
        // item instead). However, in this case, that is *precisely* what we
        // want; the `const` value is being used as an initializer for the array
        // and it is *supposed* to be copied. Clippy's docs recommend ignoring
        // the lint when used as a legacy const initializer for a static item;
        // this is a very similar case.
        #[allow(clippy::declare_interior_mutable_const)]
        const NULLPTR: AtomicPtr<Metadata<'static>> = AtomicPtr::new(ptr::null_mut());
        Self {
            ptrs: [NULLPTR; MAX_CALLSITES],
            len: AtomicUsize::new(0),
            spill: Default::default(),
        }
    }
}

impl<const MAX_CALLSITES: usize> fmt::Debug for Callsites<MAX_CALLSITES> {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let len = self.len.load(Ordering::Acquire);
        f.debug_struct("Callsites")
            .field("ptrs", &&self.ptrs[..len])
            .field("len", &len)
            .field("max_callsites", &MAX_CALLSITES)
            .field("spill", &self.spill)
            .finish()
    }
}
