//! Tools for inspecting and manipulating windows.

use {
    crate::{
        Axis, Direction, Workspace,
        client::{Client, ClientCriterion},
        input::Seat,
    },
    serde::{Deserialize, Serialize},
    std::ops::Deref,
};

/// A toplevel window.
///
/// A toplevel window is anything that can be stored within a container tile or within a
/// floating window.
///
/// There are currently four types of windows:
///
/// - Containers
/// - Placeholders that take the place of a window when it goes fullscreen
/// - XDG toplevels
/// - X windows
///
/// You can find out the type of a window by using the [`Window::type_`] function.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct Window(pub u64);

bitflags! {
    /// The type of a window.
    #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)]
    pub struct WindowType(pub u64) {
        /// A container.
        pub const CONTAINER = 1 << 0,
        /// A placeholder.
        pub const PLACEHOLDER = 1 << 1,
        /// An XDG toplevel.
        pub const XDG_TOPLEVEL = 1 << 2,
        /// An X window.
        pub const X_WINDOW = 1 << 3,
    }
}

bitflags! {
    /// The content type of a window.
    #[derive(Serialize, Deserialize, Copy, Clone, Hash, Eq, PartialEq)]
    pub struct ContentType(pub u64) {
        /// No content type.
        pub const NO_CONTENT_TYPE = 1 << 0,
        /// Photo content type.
        pub const PHOTO_CONTENT = 1 << 1,
        /// Video content type.
        pub const VIDEO_CONTENT = 1 << 2,
        /// Game content type.
        pub const GAME_CONTENT = 1 << 3,
    }
}

/// The tile state of a window.
#[non_exhaustive]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub enum TileState {
    /// The window is tiled.
    Tiled,
    /// The window is floating.
    Floating,
}

/// A window created by a client.
///
/// This is the same as `XDG_TOPLEVEL | X_WINDOW`.
pub const CLIENT_WINDOW: WindowType = WindowType(XDG_TOPLEVEL.0 | X_WINDOW.0);

impl Window {
    /// Returns whether the window exists.
    pub fn exists(self) -> bool {
        self.0 != 0 && get!(false).window_exists(self)
    }

    /// Returns whether the window does not exist.
    ///
    /// This is a shorthand for `!self.exists()`.
    pub fn does_not_exist(self) -> bool {
        !self.exists()
    }

    /// Returns the client of the window.
    ///
    /// If the window does not have a client, [`Client::exists`] return false.
    pub fn client(self) -> Client {
        get!(Client(0)).window_client(self)
    }

    /// Returns the title of the window.
    pub fn title(self) -> String {
        get!().window_title(self)
    }

    /// Returns the type of the window.
    pub fn type_(self) -> WindowType {
        get!(WindowType(0)).window_type(self)
    }

    /// Returns the content type of the window.
    pub fn content_type(self) -> ContentType {
        get!(ContentType(0)).content_type(self)
    }

    /// Returns the identifier of the window.
    ///
    /// This is the identifier used in the `ext-foreign-toplevel-list-v1` protocol.
    pub fn id(self) -> String {
        get!().window_id(self)
    }

    /// Returns whether this window is visible.
    pub fn is_visible(self) -> bool {
        get!().window_is_visible(self)
    }

    /// Returns the parent of this window.
    ///
    /// If this window has no parent, [`Window::exists`] returns false.
    pub fn parent(self) -> Window {
        get!(Window(0)).window_parent(self)
    }

    /// Returns the children of this window.
    ///
    /// Only containers have children.
    pub fn children(self) -> Vec<Window> {
        get!().window_children(self)
    }

    /// Moves the window in the specified direction.
    pub fn move_(self, direction: Direction) {
        get!().window_move(self, direction)
    }

    /// Returns whether the parent-container of the window is in mono-mode.
    pub fn mono(self) -> bool {
        get!(false).window_mono(self)
    }

    /// Sets whether the parent-container of the window is in mono-mode.
    pub fn set_mono(self, mono: bool) {
        get!().set_window_mono(self, mono)
    }

    /// Toggles whether the parent-container of the window is in mono-mode.
    pub fn toggle_mono(self) {
        self.set_mono(!self.mono());
    }

    /// Returns the split axis of the parent-container of the window.
    pub fn split(self) -> Axis {
        get!(Axis::Horizontal).window_split(self)
    }

    /// Sets the split axis of the parent-container of the window.
    pub fn set_split(self, axis: Axis) {
        get!().set_window_split(self, axis)
    }

    /// Toggles the split axis of the parent-container of the window.
    pub fn toggle_split(self) {
        self.set_split(self.split().other());
    }

    /// Creates a new container with the specified split in place of the window.
    pub fn create_split(self, axis: Axis) {
        get!().create_window_split(self, axis);
    }

    /// Requests the window to be closed.
    pub fn close(self) {
        get!().close_window(self);
    }

    /// Returns whether the window is floating.
    pub fn floating(self) -> bool {
        get!().get_window_floating(self)
    }
    /// Sets whether the window is floating.
    pub fn set_floating(self, floating: bool) {
        get!().set_window_floating(self, floating);
    }

    /// Toggles whether the window is floating.
    ///
    /// You can do the same by double-clicking on the header.
    pub fn toggle_floating(self) {
        self.set_floating(!self.floating());
    }

    /// Returns the workspace that this window belongs to.
    ///
    /// If no such workspace exists, `exists` returns `false` for the returned workspace.
    pub fn workspace(self) -> Workspace {
        get!(Workspace(0)).get_window_workspace(self)
    }

    /// Moves the window to the workspace.
    pub fn set_workspace(self, workspace: Workspace) {
        get!().set_window_workspace(self, workspace)
    }

    /// Toggles whether the currently focused window is fullscreen.
    pub fn toggle_fullscreen(self) {
        self.set_fullscreen(!self.fullscreen())
    }
    /// Returns whether the window is fullscreen.
    pub fn fullscreen(self) -> bool {
        get!(false).get_window_fullscreen(self)
    }

    /// Sets whether the window is fullscreen.
    pub fn set_fullscreen(self, fullscreen: bool) {
        get!().set_window_fullscreen(self, fullscreen)
    }

    /// Gets whether the window is pinned.
    ///
    /// If a floating window is pinned, it will stay visible even when switching to a
    /// different workspace.
    pub fn float_pinned(self) -> bool {
        get!().get_window_pinned(self)
    }

    /// Sets whether the window is pinned.
    pub fn set_float_pinned(self, pinned: bool) {
        get!().set_window_pinned(self, pinned);
    }

    /// Toggles whether the window is pinned.
    pub fn toggle_float_pinned(self) {
        self.set_float_pinned(!self.float_pinned());
    }

    /// Resizes the window.
    pub fn resize(self, dx1: i32, dy1: i32, dx2: i32, dy2: i32) {
        get!().resize_window(self, dx1, dy1, dx2, dy2);
    }
}

/// A window matcher.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct WindowMatcher(pub u64);

/// A matched window.
#[derive(Serialize, Deserialize, Copy, Clone, Debug, Hash, Eq, PartialEq)]
pub struct MatchedWindow {
    pub(crate) matcher: WindowMatcher,
    pub(crate) window: Window,
}

/// A criterion for matching a window.
#[derive(Copy, Clone, Debug, Hash, Eq, PartialEq)]
#[non_exhaustive]
pub enum WindowCriterion<'a> {
    /// Matches if the contained matcher matches.
    Matcher(WindowMatcher),
    /// Matches if the contained criterion does not match.
    Not(&'a WindowCriterion<'a>),
    /// Matches if the window has one of the types.
    Types(WindowType),
    /// Matches if all of the contained criteria match.
    All(&'a [WindowCriterion<'a>]),
    /// Matches if any of the contained criteria match.
    Any(&'a [WindowCriterion<'a>]),
    /// Matches if an exact number of the contained criteria match.
    Exactly(usize, &'a [WindowCriterion<'a>]),
    /// Matches if the window's client matches the client criterion.
    Client(&'a ClientCriterion<'a>),
    /// Matches the title of the window verbatim.
    Title(&'a str),
    /// Matches the title of the window with a regular expression.
    TitleRegex(&'a str),
    /// Matches the app-id of the window verbatim.
    AppId(&'a str),
    /// Matches the app-id of the window with a regular expression.
    AppIdRegex(&'a str),
    /// Matches if the window is floating.
    Floating,
    /// Matches if the window is visible.
    Visible,
    /// Matches if the window has the urgency flag set.
    Urgent,
    /// Matches if the window has the keyboard focus of the seat.
    Focus(Seat),
    /// Matches if the window is fullscreen.
    Fullscreen,
    /// Matches if the window has/hasn't just been mapped.
    ///
    /// This is true for one iteration of the compositor's main loop immediately after the
    /// window has been mapped.
    JustMapped,
    /// Matches the toplevel-tag of the window verbatim.
    Tag(&'a str),
    /// Matches the toplevel-tag of the window with a regular expression.
    TagRegex(&'a str),
    /// Matches the X class of the window verbatim.
    XClass(&'a str),
    /// Matches the X class of the window with a regular expression.
    XClassRegex(&'a str),
    /// Matches the X instance of the window verbatim.
    XInstance(&'a str),
    /// Matches the X instance of the window with a regular expression.
    XInstanceRegex(&'a str),
    /// Matches the X role of the window verbatim.
    XRole(&'a str),
    /// Matches the X role of the window with a regular expression.
    XRoleRegex(&'a str),
    /// Matches the workspace the window.
    Workspace(Workspace),
    /// Matches the workspace name of the window verbatim.
    WorkspaceName(&'a str),
    /// Matches the workspace name of the window with a regular expression.
    WorkspaceNameRegex(&'a str),
    /// Matches if the window has one of the content types.
    ContentTypes(ContentType),
}

impl WindowCriterion<'_> {
    /// Converts the criterion to a matcher.
    pub fn to_matcher(self) -> WindowMatcher {
        get!(WindowMatcher(0)).create_window_matcher(self)
    }

    /// Binds a function to execute when the criterion matches a window.
    ///
    /// This leaks the matcher.
    pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
        self.to_matcher().bind(cb);
    }

    /// Sets whether newly mapped windows that match this criterion get the keyboard focus.
    ///
    /// If a window matches any criterion for which this is false, the window will not be
    /// automatically focused.
    ///
    /// This leaks the matcher.
    pub fn set_auto_focus(self, auto_focus: bool) {
        self.to_matcher().set_auto_focus(auto_focus);
    }

    /// Sets whether newly mapped windows that match this matcher are mapped tiling or
    /// floating.
    ///
    /// If multiple such window matchers match a window, the used tile state is
    /// unspecified.
    ///
    /// This leaks the matcher.
    pub fn set_initial_tile_state(self, tile_state: TileState) {
        self.to_matcher().set_initial_tile_state(tile_state);
    }
}

impl WindowMatcher {
    /// Destroys the matcher.
    ///
    /// Any bound callback will no longer be executed.
    pub fn destroy(self) {
        get!().destroy_window_matcher(self);
    }

    /// Sets a function to execute when the criterion matches a window.
    ///
    /// Replaces any already bound callback.
    pub fn bind<F: FnMut(MatchedWindow) + 'static>(self, cb: F) {
        get!().set_window_matcher_handler(self, cb);
    }

    /// Sets whether newly mapped windows that match this matcher get the keyboard focus.
    ///
    /// If a window matches any matcher for which this is false, the window will not be
    /// automatically focused.
    pub fn set_auto_focus(self, auto_focus: bool) {
        get!().set_window_matcher_auto_focus(self, auto_focus);
    }

    /// Sets whether newly mapped windows that match this matcher are mapped tiling or
    /// floating.
    ///
    /// If multiple such window matchers match a window, the used tile state is
    /// unspecified.
    pub fn set_initial_tile_state(self, tile_state: TileState) {
        get!().set_window_matcher_initial_tile_state(self, tile_state);
    }
}

impl MatchedWindow {
    /// Returns the window that matched.
    pub fn window(self) -> Window {
        self.window
    }

    /// Returns the matcher.
    pub fn matcher(self) -> WindowMatcher {
        self.matcher
    }

    /// Latches a function to be executed when the window no longer matches the criteria.
    pub fn latch<F: FnOnce() + 'static>(self, cb: F) {
        get!().set_window_matcher_latch_handler(self.matcher, self.window, cb);
    }
}

impl Deref for MatchedWindow {
    type Target = Window;

    fn deref(&self) -> &Self::Target {
        &self.window
    }
}
