//! Communicate with the iced runtime from widgets.
use crate::clipboard;
use crate::event;
use crate::window;
use crate::{Clipboard, InputMethod, Window};

use std::sync::Arc;

/// A connection to the state of a shell.
///
/// A [`Widget`] can leverage a [`Shell`] to trigger changes in an application,
/// like publishing messages or invalidating the current layout.
///
/// [`Widget`]: crate::Widget
#[derive(Debug)]
pub struct Shell<'a, Message> {
    window: &'a dyn Window,
    messages: &'a mut Vec<Message>,
    waker: Waker,
    event_status: event::Status,
    redraw_request: window::RedrawRequest,
    input_method: InputMethod,
    is_layout_invalid: Option<Diff>,
    are_widgets_invalid: bool,
    clipboard: Clipboard,
}

impl<'a, Message> Shell<'a, Message> {
    /// Creates a new [`Shell`] with the provided buffer of messages.
    pub fn new(window: &'a dyn Window, waker: Waker, messages: &'a mut Vec<Message>) -> Self {
        Self {
            window,
            messages,
            waker,
            event_status: event::Status::Ignored,
            redraw_request: window::RedrawRequest::Wait,
            is_layout_invalid: None,
            are_widgets_invalid: false,
            input_method: InputMethod::Disabled,
            clipboard: Clipboard {
                reads: Vec::new(),
                write: None,
            },
        }
    }

    /// Creates a new [`Shell`] from the current one with the given list of local messages.
    pub fn local<'b, A>(&self, messages: &'b mut Vec<A>) -> Shell<'b, A>
    where
        'a: 'b,
    {
        Shell::new(self.window, self.waker.clone(), messages)
    }

    /// Returns the [`Window`] of the [`Shell`].
    pub fn window(&self) -> &'a dyn Window {
        self.window
    }

    /// Returns the [`Waker`] of the [`Shell`].
    pub fn waker(&self) -> &Waker {
        &self.waker
    }

    /// Returns true if the [`Shell`] contains no published messages
    #[must_use]
    pub fn is_empty(&self) -> bool {
        self.messages.is_empty()
    }

    /// Publish the given `Message` for an application to process it.
    pub fn publish(&mut self, message: Message) {
        self.messages.push(message);
    }

    /// Marks the current event as captured. Prevents "event bubbling".
    ///
    /// A widget should capture an event when no ancestor should
    /// handle it.
    pub fn capture_event(&mut self) {
        self.event_status = event::Status::Captured;
    }

    /// Returns the current [`event::Status`] of the [`Shell`].
    #[must_use]
    pub fn event_status(&self) -> event::Status {
        self.event_status
    }

    /// Returns whether the current event has been captured.
    #[must_use]
    pub fn is_event_captured(&self) -> bool {
        self.event_status == event::Status::Captured
    }

    /// Requests a new frame to be drawn as soon as possible.
    pub fn request_redraw(&mut self) {
        self.redraw_request = window::RedrawRequest::NextFrame;
    }

    /// Requests a new frame to be drawn at the given [`window::RedrawRequest`].
    pub fn request_redraw_at(&mut self, redraw_request: impl Into<window::RedrawRequest>) {
        self.redraw_request = self.redraw_request.min(redraw_request.into());
    }

    /// Returns the request a redraw should happen, if any.
    #[must_use]
    pub fn redraw_request(&self) -> window::RedrawRequest {
        self.redraw_request
    }

    /// Replaces the redraw request of the [`Shell`]; without conflict resolution.
    ///
    /// This is useful if you want to overwrite the redraw request to a previous value.
    /// Since it's a fairly advanced use case and should rarely be used, it is a static
    /// method.
    pub fn replace_redraw_request(shell: &mut Self, redraw_request: window::RedrawRequest) {
        shell.redraw_request = redraw_request;
    }

    /// Requests the runtime to read the clipboard contents expecting the given [`clipboard::Kind`].
    ///
    /// The runtime will produce a [`clipboard::Event::Read`] when the contents have been read.
    pub fn read_clipboard(&mut self, kind: clipboard::Kind) {
        self.clipboard
            .reads
            .push((clipboard::ClipboardKind::Standard, kind));
    }

    /// Requests the runtime to read the primary clipboard contents expecting the given [`clipboard::Kind`].
    ///
    /// The runtime will produce a [`clipboard::Event::Read`] when the contents have been read.
    pub fn read_clipboard_primary(&mut self, kind: clipboard::Kind) {
        self.clipboard
            .reads
            .push((clipboard::ClipboardKind::Primary, kind));
    }

    /// Requests the runtime to write the given [`clipboard::Content`] to the clipboard.
    ///
    /// The runtime will produce a [`clipboard::Event::Written`] when the contents have been written.
    pub fn write_clipboard(&mut self, content: clipboard::Content) {
        self.clipboard.write = Some((clipboard::ClipboardKind::Standard, content));
    }

    /// Requests the runtime to write the given [`clipboard::Content`] to the primary clipboard.
    ///
    /// The runtime will produce a [`clipboard::Event::Written`] when the contents have been written.
    pub fn write_clipboard_primary(&mut self, content: clipboard::Content) {
        self.clipboard.write = Some((clipboard::ClipboardKind::Primary, content));
    }

    /// Returns the [`Clipboard`] requests of the [`Shell`], mutably.
    pub fn clipboard_mut(&mut self) -> &mut Clipboard {
        &mut self.clipboard
    }

    /// Requests the current [`InputMethod`] strategy.
    ///
    /// __Important__: This request will only be honored by the
    /// [`Shell`] only during a [`window::Event::RedrawRequested`].
    pub fn request_input_method<T: AsRef<str>>(&mut self, ime: &InputMethod<T>) {
        self.input_method.merge(ime);
    }

    /// Returns the current [`InputMethod`] strategy.
    #[must_use]
    pub fn input_method(&self) -> &InputMethod {
        &self.input_method
    }

    /// Returns the current [`InputMethod`] strategy.
    #[must_use]
    pub fn input_method_mut(&mut self) -> &mut InputMethod {
        &mut self.input_method
    }

    /// Returns whether the current layout is invalid or not.
    #[must_use]
    pub fn is_layout_invalid(&self) -> Option<Diff> {
        self.is_layout_invalid
    }

    /// Invalidates the current application layout.
    ///
    /// The shell will relayout the application widgets.
    pub fn invalidate_layout(&mut self) {
        self.invalidate_layout_with(Diff::Skip);
    }

    /// Invalidates the current application layout with the following [`Diff`] strategy.
    pub fn invalidate_layout_with(&mut self, diff: Diff) {
        self.is_layout_invalid = Some(diff);
    }

    /// Triggers the given function if the layout is invalid, cleaning it in the
    /// process.
    pub fn revalidate_layout(&mut self, f: impl FnOnce(Diff)) {
        if let Some(diff) = self.is_layout_invalid.take() {
            f(diff);
        }
    }

    /// Returns whether the widgets of the current application have been
    /// invalidated.
    #[must_use]
    pub fn are_widgets_invalid(&self) -> bool {
        self.are_widgets_invalid
    }

    /// Invalidates the current application widgets.
    ///
    /// The shell will rebuild and relayout the widget tree.
    pub fn invalidate_widgets(&mut self) {
        self.are_widgets_invalid = true;
    }

    /// Merges the current [`Shell`] with another one by applying the given
    /// function to the messages of the latter.
    ///
    /// This method is useful for composition.
    pub fn merge<B>(&mut self, mut other: Shell<'_, B>, f: impl Fn(B) -> Message) {
        self.messages.extend(other.messages.drain(..).map(f));

        self.is_layout_invalid = match (self.is_layout_invalid, other.is_layout_invalid) {
            (Some(a), Some(b)) => Some(a.max(b)),
            _ => self.is_layout_invalid.or(other.is_layout_invalid),
        };

        self.are_widgets_invalid = self.are_widgets_invalid || other.are_widgets_invalid;
        self.redraw_request = self.redraw_request.min(other.redraw_request);
        self.event_status = self.event_status.merge(other.event_status);

        self.input_method.merge(&other.input_method);
        self.clipboard.merge(&mut other.clipboard);
    }
}

/// A waker can be used to wake up the iced runtime and, consequently, trigger
/// wake events concurrently from widget logic.
#[derive(Clone)]
pub struct Waker {
    wake: Arc<dyn Fn() + Send + Sync + 'static>,
}

impl Waker {
    /// Creates a new [`Waker`] with the given `wake` function.
    pub fn new(wake: impl Fn() + Send + Sync + 'static) -> Self {
        Self {
            wake: Arc::new(wake),
        }
    }

    /// Creates a new [`Waker`] that does nothing.
    pub fn noop() -> Self {
        Self::new(|| {})
    }

    /// Wakes up the iced runtime as soon as possible.
    ///
    /// You normally want to call this concurrently (e.g. from a different thread).
    pub fn wake(&self) {
        (self.wake)();
    }
}

impl std::fmt::Debug for Waker {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Waker").finish()
    }
}

/// The diffing strategy to follow when invalidating some layout.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum Diff {
    /// Skips the diffing step.
    Skip,
    /// Performs diffing again before layouting.
    Perform,
}
