// 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

use alloc::boxed::Box;
use alloc::rc::Rc;
use core::pin::Pin;

use crate::api::PlatformError;
use crate::graphics::{Rgba8Pixel, SharedPixelBuffer};
use crate::item_tree::ItemTreeRef;
use crate::items::{ItemRc, TextWrap};
use crate::lengths::{LogicalLength, LogicalPoint, LogicalRect, LogicalSize, ScaleFactor};
use crate::window::WindowAdapter;

/// Result of a single rendering attempt. WGPU-backed renderers can report `Occluded`,
/// `Timeout` or `Skipped` instead of rendering a frame; in those cases the caller should
/// re-arm a redraw rather than wait for the next external event.
#[must_use]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DrawOutcome {
    Success,
    Occluded,
    Timeout,
    /// The renderer wasn't ready to draw yet (e.g. the WGPU surface is still being set up
    /// asynchronously). No frame was produced; the caller should re-arm a redraw.
    Skipped,
}

/// The content widths of a text, as used for its layout constraints.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ContentWidths {
    /// The width of the widest chunk that cannot be broken up: the longest word for
    /// word-wrap. A wrapping text cannot be laid out narrower than this without breaking
    /// words apart.
    pub min: LogicalLength,
    /// The width the text takes without wrapping.
    pub max: LogicalLength,
}

/// This trait represents a Renderer that can render a slint scene.
///
/// This trait is [sealed](https://rust-lang.github.io/api-guidelines/future-proofing.html#sealed-traits-protect-against-downstream-implementations-c-sealed),
/// meaning that you are not expected to implement this trait
/// yourself, but you should use the provided ones from Slint.
pub trait Renderer: RendererSealed {}
impl<T: RendererSealed> Renderer for T {}

/// Implementation details behind [`Renderer`], but since this
/// trait is not exported in the public API, it is not possible for the
/// users to re-implement these functions.
pub trait RendererSealed {
    /// The cache used by the default implementations of the text related trait functions,
    /// which lay text out through [`crate::textlayout::sharedparley`]. Renderers using
    /// those default implementations return their cache here; with the default `None`,
    /// text layouts are computed without caching.
    #[cfg(feature = "shared-parley")]
    fn text_layout_cache(&self) -> Option<&crate::textlayout::sharedparley::TextLayoutCache> {
        None
    }

    /// Returns the size of the given text in logical pixels.
    /// When set, `max_width` means that one need to wrap the text, so it does not go further than that,
    /// using the wrapping type passed by `text_wrap`.
    #[cfg(not(feature = "shared-parley"))]
    fn text_size(
        &self,
        text_item: Pin<&dyn crate::item_rendering::RenderString>,
        item_rc: &crate::item_tree::ItemRc,
        max_width: Option<LogicalLength>,
        text_wrap: TextWrap,
    ) -> LogicalSize;

    /// Returns the size of the given text in logical pixels.
    /// When set, `max_width` means that one need to wrap the text, so it does not go further than that,
    /// using the wrapping type passed by `text_wrap`.
    ///
    /// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
    #[cfg(feature = "shared-parley")]
    fn text_size(
        &self,
        text_item: Pin<&dyn crate::item_rendering::RenderString>,
        item_rc: &crate::item_tree::ItemRc,
        max_width: Option<LogicalLength>,
        text_wrap: TextWrap,
    ) -> LogicalSize {
        crate::textlayout::sharedparley::text_size(
            self,
            text_item,
            item_rc,
            max_width,
            text_wrap,
            self.text_layout_cache(),
        )
        .unwrap_or_default()
    }

    /// Returns the content widths of the text, or None if the renderer can't measure them,
    /// in which case the caller falls back to `text_size` without a lower bound.
    ///
    /// These are intrinsic to the text and don't depend on its `wrap` mode: `min` is the
    /// longest word, `max` is the unwrapped width.
    fn text_content_widths(
        &self,
        text_item: Pin<&dyn crate::item_rendering::RenderString>,
        item_rc: &crate::item_tree::ItemRc,
    ) -> Option<ContentWidths> {
        #[cfg(feature = "shared-parley")]
        {
            crate::textlayout::sharedparley::text_content_widths(self, text_item, item_rc)
        }
        #[cfg(not(feature = "shared-parley"))]
        {
            let _ = (text_item, item_rc);
            None
        }
    }

    /// Returns the size of the individual character in logical pixels.
    #[cfg(not(feature = "shared-parley"))]
    fn char_size(
        &self,
        text_item: Pin<&dyn crate::item_rendering::HasFont>,
        item_rc: &crate::item_tree::ItemRc,
        ch: char,
    ) -> LogicalSize;

    /// Returns the size of the individual character in logical pixels.
    ///
    /// The default implementation measures the character with the shared parley layout.
    #[cfg(feature = "shared-parley")]
    fn char_size(
        &self,
        text_item: Pin<&dyn crate::item_rendering::HasFont>,
        item_rc: &crate::item_tree::ItemRc,
        ch: char,
    ) -> LogicalSize {
        self.slint_context()
            .and_then(|ctx| {
                let mut font_ctx = ctx.font_context().borrow_mut();
                crate::textlayout::sharedparley::char_size(&mut font_ctx, text_item, item_rc, ch)
            })
            .unwrap_or_default()
    }

    /// Returns the metrics of the given font.
    #[cfg(not(feature = "shared-parley"))]
    fn font_metrics(&self, font_request: crate::graphics::FontRequest)
    -> crate::items::FontMetrics;

    /// Returns the metrics of the given font.
    ///
    /// The default implementation queries the font through the shared fontique collection.
    #[cfg(feature = "shared-parley")]
    fn font_metrics(
        &self,
        font_request: crate::graphics::FontRequest,
    ) -> crate::items::FontMetrics {
        self.slint_context()
            .map(|ctx| {
                let mut font_ctx = ctx.font_context().borrow_mut();
                crate::textlayout::sharedparley::font_metrics(&mut font_ctx, font_request)
            })
            .unwrap_or_default()
    }

    /// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
    /// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
    /// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
    /// position.
    #[cfg(not(feature = "shared-parley"))]
    fn text_input_byte_offset_for_position(
        &self,
        text_input: Pin<&crate::items::TextInput>,
        item_rc: &ItemRc,
        pos: LogicalPoint,
    ) -> usize;

    /// Returns the (UTF-8) byte offset in the text property that refers to the character that contributed to
    /// the glyph cluster that's visually nearest to the given coordinate. This is used for hit-testing,
    /// for example when receiving a mouse click into a text field. Then this function returns the "cursor"
    /// position.
    ///
    /// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
    #[cfg(feature = "shared-parley")]
    fn text_input_byte_offset_for_position(
        &self,
        text_input: Pin<&crate::items::TextInput>,
        item_rc: &ItemRc,
        pos: LogicalPoint,
    ) -> usize {
        crate::textlayout::sharedparley::text_input_byte_offset_for_position(
            self,
            text_input,
            item_rc,
            pos,
            self.text_layout_cache(),
        )
    }

    /// That's the opposite of [`Self::text_input_byte_offset_for_position`]
    /// It takes a (UTF-8) byte offset in the text property, and returns a Rectangle
    /// left to the char. It is one logical pixel wide and ends at the baseline.
    #[cfg(not(feature = "shared-parley"))]
    fn text_input_cursor_rect_for_byte_offset(
        &self,
        text_input: Pin<&crate::items::TextInput>,
        item_rc: &ItemRc,
        byte_offset: usize,
    ) -> LogicalRect;

    /// That's the opposite of [`Self::text_input_byte_offset_for_position`]
    /// It takes a (UTF-8) byte offset in the text property, and returns a Rectangle
    /// left to the char. It is one logical pixel wide and ends at the baseline.
    ///
    /// The default implementation uses the shared parley text layout with [`Self::text_layout_cache`].
    #[cfg(feature = "shared-parley")]
    fn text_input_cursor_rect_for_byte_offset(
        &self,
        text_input: Pin<&crate::items::TextInput>,
        item_rc: &ItemRc,
        byte_offset: usize,
    ) -> LogicalRect {
        crate::textlayout::sharedparley::text_input_cursor_rect_for_byte_offset(
            self,
            text_input,
            item_rc,
            byte_offset,
            self.text_layout_cache(),
        )
    }

    /// Whether this renderer lays `text_input`'s text out through parley.
    #[cfg(feature = "shared-parley")]
    fn text_input_has_parley_layout(
        &self,
        _text_input: Pin<&crate::items::TextInput>,
        _item_rc: &ItemRc,
    ) -> bool {
        true
    }

    /// Clear the caches for the items that are being removed
    fn free_graphics_resources(
        &self,
        _component: ItemTreeRef,
        _items: &mut dyn Iterator<Item = Pin<crate::items::ItemRef<'_>>>,
    ) -> Result<(), crate::platform::PlatformError> {
        Ok(())
    }

    /// Mark a given region as dirty regardless whether the items actually are dirty.
    ///
    /// Example: when a PopupWindow disappears, the region under the popup needs to be redrawn
    fn mark_dirty_region(&self, _region: crate::partial_renderer::DirtyRegion) {}

    #[cfg(all(feature = "std", not(feature = "shared-parley")))] // FIXME: just because of the Error
    /// This function can be used to register a custom TrueType font with Slint,
    /// for use with the `font-family` property. The provided slice must be a valid TrueType
    /// font.
    fn register_font_from_memory(
        &self,
        _data: &'static [u8],
    ) -> Result<(), Box<dyn std::error::Error>> {
        Err("This renderer does not support registering custom fonts.".into())
    }

    #[cfg(all(feature = "std", feature = "shared-parley"))]
    /// This function can be used to register a custom TrueType font with Slint,
    /// for use with the `font-family` property. The provided slice must be a valid TrueType
    /// font.
    ///
    /// The default implementation registers the font with the shared fontique collection.
    fn register_font_from_memory(
        &self,
        data: &'static [u8],
    ) -> Result<(), Box<dyn std::error::Error>> {
        let ctx = self.slint_context().ok_or("slint platform not initialized")?;
        ctx.font_context().borrow_mut().register_static_font(data);
        Ok(())
    }

    #[cfg(all(feature = "std", not(feature = "shared-parley")))]
    /// This function can be used to register a custom TrueType font with Slint,
    /// for use with the `font-family` property. The provided path must refer to a valid TrueType
    /// font.
    fn register_font_from_path(
        &self,
        _path: &std::path::Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        Err("This renderer does not support registering custom fonts.".into())
    }

    #[cfg(all(feature = "std", feature = "shared-parley"))]
    /// This function can be used to register a custom TrueType font with Slint,
    /// for use with the `font-family` property. The provided path must refer to a valid TrueType
    /// font.
    ///
    /// The default implementation registers the font with the shared fontique collection.
    fn register_font_from_path(
        &self,
        path: &std::path::Path,
    ) -> Result<(), Box<dyn std::error::Error>> {
        let requested_path = path.canonicalize().unwrap_or_else(|_| path.into());
        let contents = std::fs::read(requested_path)?;
        let ctx = self.slint_context().ok_or("slint platform not initialized")?;
        ctx.font_context().borrow_mut().collection.register_fonts(contents.into(), None);
        Ok(())
    }

    fn register_bitmap_font(&self, _font_data: &'static crate::graphics::BitmapFont) {
        crate::debug_log!(
            "Internal error: The current renderer cannot load fonts build with the `EmbedForSoftwareRenderer` option. Please use the software Renderer, or disable that option when building your slint files"
        );
    }

    /// This function is called through the public API to register a callback that the backend needs to invoke during
    /// different phases of rendering.
    fn set_rendering_notifier(
        &self,
        _callback: Box<dyn crate::api::RenderingNotifier>,
    ) -> Result<(), crate::api::SetRenderingNotifierError> {
        Err(crate::api::SetRenderingNotifierError::Unsupported)
    }

    fn set_window_adapter(&self, _window_adapter: &Rc<dyn WindowAdapter>);

    fn window_adapter(&self) -> Option<Rc<dyn WindowAdapter>>;

    fn scale_factor(&self) -> Option<ScaleFactor> {
        self.window_adapter()
            .map(|window_adapter| ScaleFactor::new(window_adapter.window().scale_factor()))
    }

    #[cfg(feature = "shared-parley")]
    fn slint_context(&self) -> Option<crate::SlintContext> {
        self.window_adapter()
            .map(|wa| crate::window::WindowInner::from_pub(wa.window()).context().clone())
    }

    fn resize(&self, _size: crate::api::PhysicalSize) -> Result<(), PlatformError> {
        Ok(())
    }

    /// Re-implement this function to support Window::take_snapshot(), i.e. return
    /// the contents of the window in an image buffer.
    fn take_snapshot(&self) -> Result<SharedPixelBuffer<Rgba8Pixel>, PlatformError> {
        Err("WindowAdapter::take_snapshot is not implemented by the platform".into())
    }

    /// Whether the renderer supports transformations such as rotations and scaling or not.
    fn supports_transformations(&self) -> bool;
}
