use std::sync::Arc;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};

use indexmap::IndexSet;

use crate::utils::alive_tracker::{AliveTracker, IsAlive};
use crate::wayland::shell::xdg::{XdgPopupSurfaceData, XdgToplevelSurfaceData};
use crate::{
    utils::{Rectangle, Serial},
    wayland::{
        Dispatch2, compositor,
        shell::xdg::{PopupState, XDG_POPUP_ROLE, XDG_TOPLEVEL_ROLE},
    },
};

use wayland_protocols::{
    xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1,
    xdg::dialog::v1::server::xdg_dialog_v1,
    xdg::shell::server::{
        xdg_popup::XdgPopup, xdg_surface, xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel, xdg_wm_base,
    },
};

use wayland_server::{DataInit, Dispatch, DisplayHandle, Resource, Weak, protocol::wl_surface};

use super::{
    PopupConfigure, SurfaceCachedState, ToplevelConfigure, XdgPopupSurfaceRoleAttributes,
    XdgPositionerUserData, XdgShellHandler, XdgToplevelSurfaceRoleAttributes,
};

mod toplevel;
use toplevel::make_toplevel_handle;
pub use toplevel::{get_parent, send_toplevel_configure};

mod popup;
pub use popup::{make_popup_handle, send_popup_configure};

/// User data of XdgSurface
#[derive(Debug)]
pub struct XdgSurfaceUserData {
    pub(crate) known_surfaces: Arc<Mutex<IndexSet<Weak<xdg_surface::XdgSurface>>>>,
    pub(crate) wl_surface: wl_surface::WlSurface,
    pub(crate) wm_base: xdg_wm_base::XdgWmBase,
    pub(crate) has_active_role: AtomicBool,
}

impl<D> Dispatch2<XdgSurface, D> for XdgSurfaceUserData
where
    D: Dispatch<XdgToplevel, XdgShellSurfaceUserData>,
    D: Dispatch<XdgPopup, XdgShellSurfaceUserData>,
    D: XdgShellHandler,
    D: 'static,
{
    fn request(
        &self,
        state: &mut D,
        _client: &wayland_server::Client,
        xdg_surface: &XdgSurface,
        request: xdg_surface::Request,
        _dh: &DisplayHandle,
        data_init: &mut DataInit<'_, D>,
    ) {
        match request {
            xdg_surface::Request::Destroy => {
                self.known_surfaces
                    .lock()
                    .unwrap()
                    .swap_remove(&xdg_surface.downgrade());

                if !self.wl_surface.alive() {
                    // the wl_surface is destroyed, this means the client is not
                    // trying to change the role but it's a cleanup (possibly a
                    // disconnecting client), ignore the protocol check.
                    return;
                }

                if compositor::get_role(&self.wl_surface).is_none() {
                    // No role assigned to the surface, we can exit early.
                    return;
                }

                if self.has_active_role.load(Ordering::Acquire) {
                    self.wm_base.post_error(
                        xdg_wm_base::Error::Role,
                        "xdg_surface was destroyed before its role object",
                    );
                }
            }
            xdg_surface::Request::GetToplevel { id } => {
                // We now can assign a role to the surface
                let surface = &self.wl_surface;
                let shell = &self.wm_base;

                if compositor::give_role(surface, XDG_TOPLEVEL_ROLE).is_err() {
                    shell.post_error(xdg_wm_base::Error::Role, "Surface already has a role.");
                    return;
                }

                self.has_active_role.store(true, Ordering::Release);

                let initial = compositor::with_states(surface, |states| {
                    let initial = states.data_map.insert_if_missing_threadsafe(|| {
                        Mutex::new(XdgToplevelSurfaceRoleAttributes::default())
                    });

                    // Initialize the toplevel capabilities from the default capabilities
                    let default_capabilities = &state.xdg_shell_state().default_capabilities;
                    let mut attributes = states
                        .data_map
                        .get::<Mutex<XdgToplevelSurfaceRoleAttributes>>()
                        .unwrap()
                        .lock()
                        .unwrap();
                    if attributes.server_pending.is_none() {
                        let current = attributes.current_server_state();
                        attributes.server_pending = Some(current);
                    }
                    let current_capabilities = &mut attributes.server_pending.as_mut().unwrap().capabilities;
                    current_capabilities.replace(default_capabilities.capabilities.iter().copied());

                    initial
                });

                if initial {
                    compositor::add_pre_commit_hook::<D, _>(
                        surface,
                        super::super::ToplevelSurface::pre_commit_hook,
                    );
                }

                let toplevel = data_init.init(
                    id,
                    XdgShellSurfaceUserData {
                        wl_surface: self.wl_surface.clone(),
                        xdg_surface: xdg_surface.clone(),
                        wm_base: self.wm_base.clone(),
                        decoration: Default::default(),
                        dialog: Default::default(),
                        alive_tracker: Default::default(),
                    },
                );

                state
                    .xdg_shell_state()
                    .known_toplevels
                    .push(make_toplevel_handle(&toplevel));

                let handle = make_toplevel_handle(&toplevel);

                XdgShellHandler::new_toplevel(state, handle);
            }
            xdg_surface::Request::GetPopup {
                id,
                parent,
                positioner,
            } => {
                let positioner_data = *positioner
                    .data::<XdgPositionerUserData>()
                    .unwrap()
                    .inner
                    .lock()
                    .unwrap();

                let parent_surface = parent.map(|parent| {
                    let parent_data = parent.data::<XdgSurfaceUserData>().unwrap();
                    parent_data.wl_surface.clone()
                });

                // We now can assign a role to the surface
                let surface = &self.wl_surface;
                let shell = &self.wm_base;

                let attributes = XdgPopupSurfaceRoleAttributes {
                    parent: parent_surface,
                    server_pending: Some(PopupState {
                        // Set the positioner data as the popup geometry
                        geometry: positioner_data.get_geometry(),
                        positioner: positioner_data,
                    }),
                    ..Default::default()
                };
                if compositor::give_role(surface, XDG_POPUP_ROLE).is_err() {
                    shell.post_error(xdg_wm_base::Error::Role, "Surface already has a role.");
                    return;
                }

                self.has_active_role.store(true, Ordering::Release);

                let initial = compositor::with_states(surface, |states| {
                    let inserted = states.data_map.insert_if_missing_threadsafe(|| {
                        Mutex::new(XdgPopupSurfaceRoleAttributes::default())
                    });
                    *states
                        .data_map
                        .get::<XdgPopupSurfaceData>()
                        .unwrap()
                        .lock()
                        .unwrap() = attributes;
                    inserted
                });

                if initial {
                    compositor::add_pre_commit_hook::<D, _>(
                        surface,
                        super::super::PopupSurface::pre_commit_hook,
                    );
                }

                let popup = data_init.init(
                    id,
                    XdgShellSurfaceUserData {
                        wl_surface: self.wl_surface.clone(),
                        xdg_surface: xdg_surface.clone(),
                        wm_base: self.wm_base.clone(),
                        decoration: Default::default(),
                        dialog: Default::default(),
                        alive_tracker: Default::default(),
                    },
                );

                state
                    .xdg_shell_state()
                    .known_popups
                    .push(make_popup_handle(&popup));

                let handle = make_popup_handle(&popup);

                XdgShellHandler::new_popup(state, handle, positioner_data);
            }
            xdg_surface::Request::SetWindowGeometry { x, y, width, height } => {
                // Check the role of the surface, this can be either xdg_toplevel
                // or xdg_popup. If none of the role matches the xdg_surface has no role set
                // which is a protocol error.
                let surface = &self.wl_surface;

                let role = compositor::get_role(surface);

                if role.is_none() {
                    xdg_surface.post_error(
                        xdg_surface::Error::NotConstructed,
                        "xdg_surface must have a role.",
                    );
                    return;
                }

                if role != Some(XDG_TOPLEVEL_ROLE) && role != Some(XDG_POPUP_ROLE) {
                    self.wm_base.post_error(
                        xdg_wm_base::Error::Role,
                        "xdg_surface must have a role of xdg_toplevel or xdg_popup.",
                    );
                }

                if width <= 0 || height <= 0 {
                    xdg_surface.post_error(
                        xdg_surface::Error::InvalidSize,
                        "width and height of the window geometry must be greater than zero.",
                    );
                    return;
                }

                compositor::with_states(surface, |states| {
                    states.cached_state.get::<SurfaceCachedState>().pending().geometry =
                        Some(Rectangle::new((x, y).into(), (width, height).into()));
                });
            }
            xdg_surface::Request::AckConfigure { serial } => {
                let serial = Serial::from(serial);
                let surface = &self.wl_surface;

                // Check the role of the surface, this can be either xdg_toplevel
                // or xdg_popup. If none of the role matches the xdg_surface has no role set
                // which is a protocol error.
                if compositor::get_role(surface).is_none() {
                    xdg_surface.post_error(
                        xdg_surface::Error::NotConstructed,
                        "xdg_surface must have a role.",
                    );
                    return;
                }

                // Find the correct configure state for the provided serial
                // discard all configure states that are older than the provided
                // serial.
                // If no matching serial can be found raise a protocol error
                //
                // Invoke the user impl with the found configuration
                // This has to include the serial and the role specific data.
                // - For xdg_popup there is no data.
                // - For xdg_toplevel send the state data including
                //   width, height, min/max size, maximized, fullscreen, resizing, activated
                //
                // This can be used to integrate custom protocol extensions
                let found_configure = compositor::with_states(surface, |states| {
                    if states.role == Some(XDG_TOPLEVEL_ROLE) {
                        Ok(states
                            .data_map
                            .get::<XdgToplevelSurfaceData>()
                            .unwrap()
                            .lock()
                            .unwrap()
                            .ack_configure(serial))
                    } else if states.role == Some(XDG_POPUP_ROLE) {
                        Ok(states
                            .data_map
                            .get::<XdgPopupSurfaceData>()
                            .unwrap()
                            .lock()
                            .unwrap()
                            .ack_configure(serial))
                    } else {
                        Err(())
                    }
                });

                let configure = match found_configure {
                    Ok(Some(configure)) => configure,
                    Ok(None) => {
                        self.wm_base.post_error(
                            xdg_wm_base::Error::InvalidSurfaceState,
                            format!("wrong configure serial: {}", <u32>::from(serial)),
                        );
                        return;
                    }
                    Err(()) => {
                        self.wm_base.post_error(
                            xdg_wm_base::Error::Role,
                            "xdg_surface must have a role of xdg_toplevel or xdg_popup.",
                        );
                        return;
                    }
                };

                XdgShellHandler::ack_configure(state, surface.clone(), configure);
            }
            _ => unreachable!(),
        }
    }
}

/// User data of xdg toplevel surface
#[derive(Debug)]
pub struct XdgShellSurfaceUserData {
    pub(crate) wl_surface: wl_surface::WlSurface,
    pub(crate) wm_base: xdg_wm_base::XdgWmBase,
    pub(crate) xdg_surface: xdg_surface::XdgSurface,
    pub(crate) decoration: Mutex<Option<zxdg_toplevel_decoration_v1::ZxdgToplevelDecorationV1>>,
    pub(crate) dialog: Mutex<Option<xdg_dialog_v1::XdgDialogV1>>,

    pub(crate) alive_tracker: AliveTracker,
}

impl XdgShellSurfaceUserData {
    /// Associated xdg_surface
    pub fn xdg_surface(&self) -> &xdg_surface::XdgSurface {
        &self.xdg_surface
    }
}

impl IsAlive for XdgToplevel {
    #[inline]
    fn alive(&self) -> bool {
        let data: &XdgShellSurfaceUserData = self.data().unwrap();
        data.alive_tracker.alive()
    }
}

impl IsAlive for XdgPopup {
    #[inline]
    fn alive(&self) -> bool {
        let data: &XdgShellSurfaceUserData = self.data().unwrap();
        data.alive_tracker.alive()
    }
}
