macro_rules! function_setter {
    ($interface_name:ident {
        $(
            $function_js_name:literal {
                $setter_name:ident(
                    $(
                        $function_argument_name:ident: $function_argument_type:ty
                    ),*
                ) -> $function_borrowed_return_type:ty;

                // TODO: remove this hack and properly handle types
                $function_owned_return_type:ty = $function_create_owned_return_type:expr;
            }
        )*
    }) => {
        impl<'env> $interface_name<'env> {
        $(
                pub fn $setter_name<F>(
                    &mut self,
                    env: &napi::Env,
                    function_callback: F,
                ) -> Result<(), napi::Error>
                where
                    F: for<'function_context> Fn(
                            &'function_context napi::Env,
                            $($function_argument_type,)*
                        ) -> Result<
                            Option<$function_borrowed_return_type>,
                            napi::Error,
                        >
                        + std::panic::RefUnwindSafe
                        + 'static,
                {
                    let function_callback: bindgen_prelude::Function<
                        // Treat arguments as `Unknown` to make sure type errors are handled inside the closure
                        bindgen_prelude::Unknown,
                        // Extend the return type's lifetime beyond the lifetime of the closure
                        Option<$function_owned_return_type>,
                    > =
                        env.create_function_from_closure($function_js_name, move |function_context| {
                            let ($($function_argument_name),*) = function_context.args()?;

                            let function_return_value = match std::panic::catch_unwind(|| function_callback(function_context.env, $($function_argument_name),*)) {
                                Ok(function_result) => function_result?,
                                Err(panic_payload) => {
                                    tracing::error!(
                                        message = "Function panic",
                                        event_handler = $function_js_name,
                                        ?panic_payload
                                    );

                                    return Err(napi::Error::from_reason("Function panic"));
                                }
                            };

                            $function_create_owned_return_type(function_return_value)
                        })?;

                    self.inner.set_named_property($function_js_name, function_callback)?;

                    Ok(())
                }
            )*
        }
    };
}

pub(crate) use function_setter;