// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

#![warn(missing_docs)]

/*!
This module contains types that are public and re-exported in the slint-rs as well as the slint-interpreter crate as public API,
in particular the `BackendSelector` type.
*/

use alloc::boxed::Box;
use alloc::format;
use alloc::string::{String, ToString};

use i_slint_core::api::PlatformError;
use i_slint_core::graphics::{RequestedGraphicsAPI, RequestedOpenGLVersion};

#[i_slint_core_macros::slint_doc]
/// Use the BackendSelector to configure one of Slint's built-in [backends with a renderer](slint:backends_and_renderers)
/// to accommodate specific needs of your application. This is a programmatic substitute for
/// the `SLINT_BACKEND` environment variable.
///
/// For example, to configure Slint to use a renderer that supports OpenGL ES 3.0, configure
/// the `BackendSelector` as follows:
/// ```rust,no_run
/// # use i_slint_backend_selector::api::BackendSelector;
/// let selector = BackendSelector::new().require_opengl_es_with_version(3, 0);
/// if let Err(err) = selector.select() {
///     eprintln!("Error selecting backend with OpenGL ES support: {err}");
/// }
/// ```
#[derive(Default)]
pub struct BackendSelector {
    requested_graphics_api: Option<RequestedGraphicsAPI>,
    backend: Option<String>,
    renderer: Option<String>,
    selected: bool,
    #[cfg(feature = "unstable-winit-030")]
    winit_window_attributes_hook: Option<
        Box<
            dyn Fn(
                i_slint_backend_winit::winit::window::WindowAttributes,
            ) -> i_slint_backend_winit::winit::window::WindowAttributes,
        >,
    >,
    #[cfg(feature = "unstable-winit-030")]
    winit_event_loop_builder: Option<i_slint_backend_winit::EventLoopBuilder>,
    #[cfg(feature = "unstable-winit-030")]
    winit_custom_application_handler:
        Option<Box<dyn i_slint_backend_winit::CustomApplicationHandler>>,
    #[cfg(all(target_os = "linux", feature = "unstable-libinput-09"))]
    libinput_event_hook: Option<Box<dyn Fn(&input::Event) -> bool>>,
}

impl BackendSelector {
    /// Creates a new BackendSelector.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Adds the requirement to the selector that the backend must render with OpenGL ES
    /// and the specified major and minor version.
    #[must_use]
    pub fn require_opengl_es_with_version(mut self, major: u8, minor: u8) -> Self {
        self.requested_graphics_api =
            Some(RequestedOpenGLVersion::OpenGLES(Some((major, minor))).into());
        self
    }

    /// Adds the requirement to the selector that the backend must render with OpenGL ES.
    #[must_use]
    pub fn require_opengl_es(mut self) -> Self {
        self.requested_graphics_api = Some(RequestedOpenGLVersion::OpenGLES(None).into());
        self
    }

    /// Adds the requirement to the selector that the backend must render with OpenGL.
    #[must_use]
    pub fn require_opengl(mut self) -> Self {
        self.requested_graphics_api = Some(RequestedOpenGLVersion::OpenGL(None).into());
        self
    }

    /// Adds the requirement to the selector that the backend must render with OpenGL
    /// and the specified major and minor version.
    #[must_use]
    pub fn require_opengl_with_version(mut self, major: u8, minor: u8) -> Self {
        self.requested_graphics_api =
            Some(RequestedOpenGLVersion::OpenGL(Some((major, minor))).into());
        self
    }

    /// Adds the requirement to the selector that the backend must render with Apple's Metal framework.
    #[must_use]
    pub fn require_metal(mut self) -> Self {
        self.requested_graphics_api = Some(RequestedGraphicsAPI::Metal);
        self
    }

    /// Adds the requirement to the selector that the backend must render with Vulkan.
    #[must_use]
    pub fn require_vulkan(mut self) -> Self {
        self.requested_graphics_api = Some(RequestedGraphicsAPI::Vulkan);
        self
    }

    /// Adds the requirement to the selector that the backend must render with Direct 3D.
    #[must_use]
    pub fn require_d3d(mut self) -> Self {
        self.requested_graphics_api = Some(RequestedGraphicsAPI::Direct3D);
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Adds the requirement to the selector that the backend must render using [WGPU](http://wgpu.rs).
    /// Use this when you integrate other WGPU-based renderers with a Slint UI.
    ///
    /// *Note*: This function is behind the [`unstable-wgpu-30` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major WGPU releases become available.
    ///
    /// See also the [`slint::wgpu_30`](slint:rust:slint/wgpu_30) module.
    #[cfg(feature = "unstable-wgpu-30")]
    #[must_use]
    pub fn require_wgpu_30(
        mut self,
        configuration: i_slint_core::graphics::wgpu_30::api::WGPUConfiguration,
    ) -> Self {
        self.requested_graphics_api = Some(RequestedGraphicsAPI::WGPU30(configuration));
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Adds the requirement to the selector that the backend must render using [WGPU](http://wgpu.rs).
    /// Use this when you integrate other WGPU-based renderers with a Slint UI.
    ///
    /// *Note*: This function is behind the [`unstable-wgpu-29` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major WGPU releases become available.
    ///
    /// See also the [`slint::wgpu_29`](slint:rust:slint/wgpu_29) module.
    #[cfg(feature = "unstable-wgpu-29")]
    #[must_use]
    pub fn require_wgpu_29(
        mut self,
        configuration: i_slint_core::graphics::wgpu_29::api::WGPUConfiguration,
    ) -> Self {
        self.requested_graphics_api = Some(RequestedGraphicsAPI::WGPU29(configuration));
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Configures this builder to use the specified winit hook that will be called before a Window is created.
    ///
    /// It can be used to adjust settings of window that will be created.
    ///
    /// # Example
    ///
    /// ```rust,no_run
    /// let mut backend = slint::BackendSelector::new()
    ///     .with_winit_window_attributes_hook(|attributes| attributes.with_content_protected(true))
    ///     .select()
    ///     .unwrap();
    /// ```
    ///
    /// *Note*: This function is behind the [`unstable-winit-030` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major Winit releases become available.
    ///
    /// See also the [`slint::winit_030`](slint:rust:slint/winit_030) module
    #[must_use]
    #[cfg(feature = "unstable-winit-030")]
    pub fn with_winit_window_attributes_hook(
        mut self,
        hook: impl Fn(
            i_slint_backend_winit::winit::window::WindowAttributes,
        ) -> i_slint_backend_winit::winit::window::WindowAttributes
        + 'static,
    ) -> Self {
        self.winit_window_attributes_hook = Some(Box::new(hook));
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Configures this builder to use the specified winit event loop builder when creating the event
    /// loop.
    ///
    /// *Note*: This function is behind the [`unstable-winit-030` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major Winit releases become available.
    ///
    /// See also the [`slint::winit_030`](slint:rust:slint/winit_030) module
    #[must_use]
    #[cfg(feature = "unstable-winit-030")]
    pub fn with_winit_event_loop_builder(
        mut self,
        event_loop_builder: i_slint_backend_winit::EventLoopBuilder,
    ) -> Self {
        self.winit_event_loop_builder = Some(event_loop_builder);
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Configures this builder to invoke the functions on the supplied application handler whenever winit wakes up the
    /// event loop.
    ///
    /// *Note*: This function is behind the [`unstable-winit-030` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major Winit releases become available.
    ///
    /// See also the [`slint::winit_030`](slint:rust:slint/winit_030) module
    #[must_use]
    #[cfg(feature = "unstable-winit-030")]
    pub fn with_winit_custom_application_handler(
        mut self,
        custom_application_handler: impl i_slint_backend_winit::CustomApplicationHandler + 'static,
    ) -> Self {
        self.winit_custom_application_handler = Some(Box::new(custom_application_handler));
        self
    }

    #[i_slint_core_macros::slint_doc]
    /// Configures this builder to use the specified libinput event filter hook when the LinuxKMS backend
    /// is selected.
    ///
    /// The provided hook is invoked for every event received. If the function returns true, the event is
    /// not dispatched further.
    ///
    /// *Note*: This function is behind the [`unstable-libinput-09` feature flag](slint:rust:slint/docs/cargo_features/#backends)
    ///         and may be removed or changed in future minor releases, as new major Winit releases become available.
    #[must_use]
    #[cfg(all(target_os = "linux", feature = "unstable-libinput-09"))]
    pub fn with_libinput_event_hook(
        mut self,
        event_hook: impl Fn(&input::Event) -> bool + 'static,
    ) -> Self {
        self.libinput_event_hook = Some(Box::new(event_hook));
        self
    }

    /// Adds the requirement that the selected renderer must match the given name. This is
    /// equivalent to setting the `SLINT_BACKEND=name` environment variable and requires
    /// that the corresponding renderer feature is enabled. For example, to select the Skia renderer,
    /// enable the `renderer-skia` feature and call this function with `skia` as argument.
    #[must_use]
    pub fn renderer_name(mut self, name: String) -> Self {
        self.renderer = Some(name);
        self
    }

    /// Adds the requirement that the selected backend must match the given name. This is
    /// equivalent to setting the `SLINT_BACKEND=name` environment variable and requires
    /// that the corresponding backend feature is enabled. For example, to select the winit backend,
    /// enable the `backend-winit` feature and call this function with `winit` as argument.
    #[must_use]
    pub fn backend_name(mut self, name: String) -> Self {
        let lowercase = name.to_lowercase();
        let (backend, renderer) = crate::parse_backend_env_var(&lowercase);
        self.backend = Some(backend.to_string());
        if self.renderer.is_none() && !renderer.is_empty() {
            self.renderer = Some(renderer.to_string())
        }
        self
    }

    /// Completes the backend selection process and tries to combine with specified requirements
    /// with the different backends and renderers enabled at compile time. On success, the selected
    /// backend is automatically set to be active. Returns an error if the requirements could not be met.
    pub fn select(mut self) -> Result<(), PlatformError> {
        self.select_internal()
    }

    #[cfg(not(target_os = "android"))]
    fn select_internal(&mut self) -> Result<(), PlatformError> {
        self.selected = true;

        #[cfg(any(
            feature = "i-slint-backend-qt",
            feature = "i-slint-backend-winit",
            feature = "i-slint-backend-linuxkms"
        ))]
        if (self.backend.is_none() || self.renderer.is_none())
            && let Ok(backend_config) = std::env::var("SLINT_BACKEND")
        {
            let backend_config = backend_config.to_lowercase();
            let (backend, renderer) = super::parse_backend_env_var(backend_config.as_str());
            if !backend.is_empty() {
                self.backend.get_or_insert_with(|| backend.to_owned());
            }
            if !renderer.is_empty() {
                self.renderer.get_or_insert_with(|| renderer.to_owned());
            }
        }

        let backend_name = match self.backend.as_deref() {
            Some(name) => name,
            None => {
                // Only the winit backend supports graphics API requests right now, so prefer that over
                // aborting.
                #[cfg(feature = "i-slint-backend-winit")]
                if self.requested_graphics_api.is_some() {
                    "winit"
                } else {
                    super::DEFAULT_BACKEND_NAME
                }
                #[cfg(not(feature = "i-slint-backend-winit"))]
                super::DEFAULT_BACKEND_NAME
            }
        };

        // Fail fast when wgpu rendering was required but no GPU-backed adapter
        // is available for the requested backends. Otherwise the winit/linuxkms
        // backends silently fall through to a non-wgpu renderer (e.g. the
        // standalone software renderer), and the failure surfaces much later
        // as an Unsupported error from set_rendering_notifier.
        #[cfg(feature = "unstable-wgpu-30")]
        if matches!(self.requested_graphics_api, Some(RequestedGraphicsAPI::WGPU30(_)))
            && !i_slint_core::graphics::wgpu_30::any_wgpu30_adapters_with_gpu(
                self.requested_graphics_api.clone(),
            )
        {
            return Err(
                "WGPU 30.x rendering was required but no GPU-backed WGPU adapter is available \
                 for the requested backends. Set SLINT_WGPU_CPU=1 to allow CPU adapters."
                    .into(),
            );
        }
        #[cfg(feature = "unstable-wgpu-29")]
        if matches!(self.requested_graphics_api, Some(RequestedGraphicsAPI::WGPU29(_)))
            && !i_slint_core::graphics::wgpu_29::any_wgpu29_adapters_with_gpu(
                self.requested_graphics_api.clone(),
            )
        {
            return Err(
                "WGPU 29.x rendering was required but no GPU-backed WGPU adapter is available \
                 for the requested backends. Set SLINT_WGPU_CPU=1 to allow CPU adapters."
                    .into(),
            );
        }

        let backend: Box<dyn i_slint_core::platform::Platform> = match backend_name {
            #[cfg(all(feature = "i-slint-backend-linuxkms", target_os = "linux"))]
            "linuxkms" => {
                let mut builder = i_slint_backend_linuxkms::BackendBuilder::default();

                if let Some(api) = self.requested_graphics_api.take() {
                    builder = builder.request_graphics_api(api);
                }

                if let Some(renderer_name) = self.renderer.as_ref() {
                    builder = builder.with_renderer_name(renderer_name.into());
                }

                #[cfg(all(target_os = "linux", feature = "unstable-libinput-09"))]
                if let Some(event_hook) = self.libinput_event_hook.take() {
                    builder = builder.with_libinput_event_hook(event_hook);
                }

                Box::new(builder.build()?)
            }
            #[cfg(feature = "i-slint-backend-winit")]
            "winit" => {
                let builder = i_slint_backend_winit::Backend::builder();

                let builder = match self.requested_graphics_api.as_ref() {
                    Some(api) => builder.request_graphics_api(api.clone()),
                    None => builder,
                };

                let builder = match self.renderer.as_ref() {
                    Some(name) => builder.with_renderer_name(name),
                    None => builder,
                };

                #[cfg(feature = "unstable-winit-030")]
                let builder = match self.winit_window_attributes_hook.take() {
                    Some(hook) => builder.with_window_attributes_hook(hook),
                    None => builder,
                };

                #[cfg(feature = "unstable-winit-030")]
                let builder = match self.winit_event_loop_builder.take() {
                    Some(event_loop_builder) => builder.with_event_loop_builder(event_loop_builder),
                    None => builder,
                };

                #[cfg(feature = "unstable-winit-030")]
                let builder = match self.winit_custom_application_handler.take() {
                    Some(custom_application_handler) => {
                        builder.with_custom_application_handler(custom_application_handler)
                    }
                    None => builder,
                };

                Box::new(builder.build()?)
            }
            #[cfg(feature = "i-slint-backend-qt")]
            "qt" => {
                if self.requested_graphics_api.is_some() {
                    return Err(
                        "The qt backend does not implement renderer selection by graphics API"
                            .into(),
                    );
                }
                if self.renderer.is_some() {
                    return Err(
                        "The qt backend does not implement renderer selection by name".into()
                    );
                }
                Box::new(i_slint_backend_qt::Backend::new())
            }
            requested_backend => {
                return Err(format!(
                    "{requested_backend} backend requested but it is not available"
                )
                .into());
            }
        };

        let result =
            i_slint_core::platform::set_platform(backend).map_err(PlatformError::SetPlatformError);

        #[cfg(any(feature = "system-testing", feature = "mcp"))]
        if result.is_ok() {
            super::init_testing_backends();
        }

        result
    }

    #[cfg(target_os = "android")]
    fn select_internal(&mut self) -> Result<(), PlatformError> {
        self.selected = true;
        if self.backend.as_ref().is_some_and(|b| !b.starts_with("android-activity-")) {
            return Err(
                format!("Only the android-activity-* backend is supported on Android").into()
            );
        }
        if self.renderer.as_ref().is_some_and(|r| r != "skia") {
            return Err(format!("Only the Skia renderer is supported on Android").into());
        }

        #[cfg(feature = "backend-android-activity")]
        {
            i_slint_backend_android_activity::set_requested_graphics_api(
                self.requested_graphics_api.clone(),
            )
        }
        #[cfg(not(feature = "backend-android-activity"))]
        {
            Err(format!(
                "The BackendSelector is only supported with the backend-android-activity backend"
            )
            .into())
        }
    }
}

impl Drop for BackendSelector {
    fn drop(&mut self) {
        if !self.selected {
            self.select_internal().unwrap();
        }
    }
}
