macro_rules! interface_builder {
    (
        $interface_name:ident {
            $(
                (
                    $required_field_js_name:literal: $required_field_js_type:ty,
                    $required_field_rust_name:ident: $required_field_rust_type:ty
                ),
            )*

            $(
                $required_function_generic:ident: $required_function_setter:ident =
                    $required_function_rust_name:ident(
                        $(
                            $required_function_argument_name:ident: $required_function_argument_type:ty
                        ),*
                    ) -> $required_function_return_type:ty;
            )*
        }

        $(
            ($optional_field_js_name:literal, $optional_field_rust_name:ident: $optional_field_type:ty),
        )*
    ) => {
        #[derive(Clone, Copy)]
        pub struct $interface_name<'env> {
            inner: ::napi::bindgen_prelude::Object<'env>
        }

        impl<'env> $interface_name<'env> {
            pub fn new<$($required_function_generic),*>(
                env: &napi::Env,
                $($required_field_rust_name: $required_field_rust_type),*
                $($required_function_rust_name: $required_function_generic),*
            ) -> Result<Self, napi::Error>
            where
                $(
                    $required_function_generic: for<'function_context> Fn(
                        &'function_context napi::Env,
                        $($required_function_argument_type,)*
                    ) -> Result<
                        Option<$required_function_return_type>,
                        napi::Error,
                    >
                    + std::panic::RefUnwindSafe
                    + 'static,
                )*
            {
                // TODO: generate more idiomatic code depending on if the object actually gets mutated
                #[allow(unused_mut)]
                let mut builder = Self {
                    inner: bindgen_prelude::Object::new(env)?,
                };

                // TODO: use Rust setters here
                $(builder.inner.set_named_property($required_field_js_name, $required_field_rust_name)?;)*

                $(builder.$required_function_setter(env, $required_function_rust_name)?;)*

                Ok(builder)
            }

            $(
                pub fn $optional_field_rust_name(mut self, $optional_field_rust_name: $optional_field_type) -> Result<Self, ::napi::Error> {
                    self.inner.set_named_property($optional_field_js_name, $optional_field_rust_name)?;

                    Ok(self)
                }
            )*
        }

        impl<'env> bindgen_prelude::ToNapiValue for &$interface_name<'env> {
            unsafe fn to_napi_value(env: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value, napi::Error> {
                unsafe { bindgen_prelude::Object::to_napi_value(env, val.inner) }
            }
        }

        impl<'env> bindgen_prelude::ToNapiValue for $interface_name<'env> {
            unsafe fn to_napi_value(env: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value, napi::Error> {
                unsafe { bindgen_prelude::Object::to_napi_value(env, val.inner) }
            }
        }

        impl<'env> bindgen_prelude::TypeName for $interface_name<'env> {
            fn type_name() -> &'static str {
                bindgen_prelude::Object::type_name()
            }

            fn value_type() -> napi::ValueType {
                bindgen_prelude::Object::value_type()
            }
        }

        // TODO: validate non-optional fields
        impl<'env> bindgen_prelude::FromNapiValue for $interface_name<'env> {
            unsafe fn from_napi_value(
                raw_env: napi_sys::napi_env,
                napi_val: napi_sys::napi_value,
            ) -> Result<Self, napi::Error> {
                let inner = unsafe { bindgen_prelude::Object::from_napi_value(raw_env, napi_val) }?;

                Ok(Self { inner })
            }
        }

        // TODO: validate functions
        impl<'env> bindgen_prelude::ValidateNapiValue for $interface_name<'env> {
            unsafe fn validate(
                raw_env: napi_sys::napi_env,
                raw_interface_value: napi_sys::napi_value,
            ) -> Result<napi_sys::napi_value, napi::Error> {
                // TODO: only generate when needed
                #[allow(unused)]
                let interface = match unsafe {
                    <bindgen_prelude::Object as bindgen_prelude::ValidateNapiValue>::validate(raw_env, raw_interface_value)
                } {
                    Ok(_validated_object) => bindgen_prelude::Object::from_raw(raw_env, raw_interface_value),
                    Err(error) => {
                        return Err(napi::Error::from_reason(format!(
                            "{} should be an Object: {}",
                            stringify!($interface_name),
                            error
                        )));
                    }
                };

                $(
                    let _property_value: $required_field_js_type = interface.get_named_property($required_field_js_name).map_err(|mut error| {
                        error.reason = format!(
                            "Error validating property {} of {}: {}",
                            $required_field_js_name,
                            stringify!($interface_name),
                            error.reason
                        );

                        error
                    })?;
                )*

                Ok(raw_interface_value)
            }
        }
    };
}

pub(crate) use interface_builder;