//! Runtime utilities for signal handling
use core::ffi::c_int;

use abi::{
    error::Error,
    mem::{MappingFlags, MappingSource},
    process::{ExitCode, Signal, SignalEntryData},
};

use crate::{
    process::{self, thread},
    sync::rwlock::RwLock,
};

/// Describes how a signal should be handled
#[derive(Debug, Clone, Copy)]
pub enum SignalHandler {
    /// Terminate when received
    Terminate,
    /// Silently ignore the signal
    Ignore,
    /// Pass the signal to a custom function (Rust version)
    Rust(fn(Signal)),
    /// Pass the signal to a custom function (C compat version)
    C(unsafe extern "C" fn(c_int)),
}

unsafe impl Sync for SignalHandler {}

const MAX_SIGNALS: usize = 16;
static TABLE: RwLock<[SignalHandler; MAX_SIGNALS]> =
    RwLock::new([const { SignalHandler::Terminate }; MAX_SIGNALS]);

unsafe extern "C" fn common_signal_entry(data: &SignalEntryData) -> ! {
    let index = data.signal.into_raw() as usize;

    if index >= MAX_SIGNALS {
        terminate_by_signal(data.signal);
    }

    match TABLE.read()[index] {
        SignalHandler::Rust(function) => function(data.signal),
        SignalHandler::C(function) => {
            let signum = data.signal.into_raw() as i32;
            function(signum);
        }
        SignalHandler::Terminate => terminate_by_signal(data.signal),
        SignalHandler::Ignore => (),
    }

    crate::sys::exit_signal(data)
}

// TODO add signal print hook function to dump signal info here
fn terminate_by_signal(signal: Signal) -> ! {
    crate::debug_trace!(Warn, "runtime: terminated by signal: {signal:?}");
    unsafe { crate::sys::exit_process(ExitCode::BySignal(Ok(signal))) };
}

/// Updates the handler for a particular signal. Returns the old handler used for that signal.
pub fn set_handler(signal: Signal, handler: SignalHandler) -> SignalHandler {
    let mut table = TABLE.write();
    let entry = &mut table[signal.into_raw() as usize];
    core::mem::replace(entry, handler)
}

/// Updates all signal handlers to point to a single handler. Intended to be used with custom
/// signal "routers".
pub fn set_all_handlers(handler: SignalHandler) {
    let mut table = TABLE.write();
    for entry in table.iter_mut() {
        *entry = handler;
    }
}

/// Sets the stack that will be used to handle signals **on this thread**.
///
/// # Safety
///
/// To be legible for signal handling, `sp` must be non-NULL and have at least 4K of space
/// below it. It is up to the caller to make sure `sp` points to a proper stack's top.
///
/// TLDR: just use [setup_signal_stack].
pub unsafe fn set_signal_stack(base: usize, size: usize) {
    process::set_thread_option::<thread::options::SignalStack>(
        &thread::options::ThreadSignalStack { base, size },
    )
    .expect("set_signal_stack() failed")
}

/// Returns the currently set signal stack base:size.
pub fn get_signal_stack() -> (usize, usize) {
    let stack = process::get_thread_option!(thread::options::SignalStack)
        .expect("get_signal_stack() failed");
    (stack.base, stack.size)
}

/// Sets the program's signal entry function.
///
/// # Safety
///
/// `entry` must be an address of a valid signal entry function:
///
/// 1. It must not return, it must call `exit_thread` instead.
/// 2. It must conform to the kernel's signal entry ABI.
pub unsafe fn set_signal_entry(entry: usize) {
    process::set_process_option::<process::options::SignalEntry>(None, &entry)
        .expect("set_signal_entry() failed");
}

fn allocate_signal_stack(mut size: usize) -> Result<(usize, usize), Error> {
    if size == 0 {
        size = 4096;
    }
    size = (size + 0xFFF) & !0xFFF;
    let base = unsafe {
        crate::sys::map_memory(None, size, MappingFlags::WRITE, &MappingSource::Anonymous)
    }?;
    Ok((base, size))
}

/// Allocates a signal stack and sets it **for this thread**. Returns an error if allocation fails.
///
/// # Note
///
/// This function may allocate more memory than requested in the `size` parameter. It will allocate
/// at least 4KiB of memory, rounding the size up to a page boundary. This is required for the
/// stack to be legible for entry from the kernel.
///
/// The allocated stack is expected to live forever or at least until it is replaced by another
/// one, so current implementation just leaks the stack. The implementation also provides no
/// guarantees on what will happen if a raw syscall to [crate::sys::set_signal_entry] is done
/// after calling this function.
pub fn setup_signal_stack(size: usize) -> Result<(), Error> {
    let (base, size) = allocate_signal_stack(size)?;
    unsafe { set_signal_stack(base, size) };
    Ok(())
}

/// Performs full signal setup for the process:
///
/// * Allocates a signal stack for **the current thread**.
/// * Sets up a signal entry into the runtime-managed handler for the whole process.
///
/// # Note
///
/// This function is intended to be called from the main thread, while [setup_signal_stack] is
/// intended for setting up thread signal stacks. A caller *may* call this function elsewhere,
/// and this will be safe, but likely will not make any sense.
///
/// Also see notes for [setup_signal_stack].
pub fn setup_signal_full(size: usize) -> Result<(), Error> {
    let (base, size) = allocate_signal_stack(size)?;
    unsafe {
        set_signal_stack(base, size);
        set_signal_entry(imp::signal_entry as usize);
    }
    Ok(())
}

// implementation details

mod imp {
    pub(super) use super::common_signal_entry as signal_entry;
}
