// xfwl4 -- Wayland compositor for the Xfce Desktop Environment
//
// Copyright (C) 2026 Brian Tarricone <brian@tarricone.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.

use std::collections::HashMap;

use smithay::{
    backend::input::ButtonState,
    desktop::space::SpaceElement,
    input::{
        Seat,
        pointer::{ButtonEvent, MotionEvent},
    },
    utils::{Logical, Point, SERIAL_COUNTER, Serial},
};

use crate::{
    backend::Backend,
    core::{
        config::adjacent_monitor_in_direction,
        cycle::CyclingPhase,
        focus::PointerFocusTarget,
        placement::FillMode,
        shell::{GrabTrigger, ResizeEdge, WindowCapabilities, WindowElement},
        state::Xfwl4State,
        util::{BTN_RIGHT, Direction, OutputExt},
    },
    protocols::xfwl4_compositor_ui::{
        CompositorUiHandler, CompositorUiState, DialogId, WindowMenuAction, WindowMenuState as UiWindowMenuState,
        proto::{
            xfwl4_ui_tabwin_v1::CloseReason,
            xfwl4_ui_window_menu_v1::{ActionType, Direction as WindowMenuDirection, StackingState},
        },
    },
};

pub struct WindowMenuState<BackendData: Backend + 'static> {
    anchor: Option<WindowElement>,
    target: Option<WindowElement>,
    pending_state: Option<PendingWindowMenuState<BackendData>>,
}

struct PendingWindowMenuState<BackendData: Backend + 'static> {
    focus: PointerFocusTarget,
    location: Point<i32, Logical>,
    seat: Seat<Xfwl4State<BackendData>>,
    serial: Serial,
}

pub enum ActionLocation {
    WindowRelative(Point<i32, Logical>),
    Absolute(Point<i32, Logical>),
}

impl<BackendData: Backend + 'static> WindowMenuState<BackendData> {
    pub(in crate::core) fn reset(&mut self) {
        self.anchor = None;
        self.target = None;
        self.pending_state = None;
    }

    pub(in crate::core) fn reset_pending(&mut self) {
        self.pending_state = None;
    }

    pub(in crate::core) fn window_menu_anchor(&self) -> Option<&WindowElement> {
        self.anchor.as_ref()
    }

    pub(in crate::core) fn update_window_menu_anchor(&mut self, anchor: WindowElement) {
        self.anchor = Some(anchor);
    }
}

impl<BackendData: Backend + 'static> Default for WindowMenuState<BackendData> {
    fn default() -> Self {
        Self {
            anchor: None,
            target: None,
            pending_state: None,
        }
    }
}

impl<BackendData: Backend + 'static> CompositorUiHandler for Xfwl4State<BackendData> {
    fn compositor_ui_state(&mut self) -> &mut CompositorUiState {
        &mut self.core.compositor_ui_state
    }

    fn theme_colors(&mut self, theme_colors: HashMap<String, gtk::gdk::RGBA>) {
        self.core.config.update_color_names(theme_colors);
        if let Err(err) = self.load_decoration_theme() {
            tracing::warn!("Failed to reload decoration theme after theme color change: {err}");
        }
    }

    fn tabwin_image_sizes(&mut self, preview_icon_size: Option<u32>, window_icon_size: u32) {
        self.core
            .cycling_state
            .set_tabwin_image_sizes(preview_icon_size, Some(window_icon_size));
        self.send_window_images_to_tabwin();
    }

    fn tabwin_selected(&mut self, selected_window_id: u32) {
        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|elem: &WindowElement| elem.window_id() == selected_window_id)
        {
            if self.core.config.cycle_raise() {
                self.raise_window(&window, SERIAL_COUNTER.next_serial(), false);
            }

            if self.core.config.cycle_draw_frame() {
                self.show_tabwin_window_wireframe(&window);
            }
        }
    }

    fn tabwin_activated(&mut self, activated_window_id: u32) {
        if self.core.cycling_state.cycling_phase() == CyclingPhase::Active {
            // This is a little bit of a hack: if the cycling phase is 'active', then that means
            // this is an "unsolicited" 'activated'; that is, the user clicked on an item in the
            // tabwin, so it's pro-actively sending activated, and that isn't in response to a
            // 'close' event we've sent it.  So, to ensure the UI process cleans up, send 'close'
            // here, but with 'cancelled' as the reason so the tabwin doesn't send us another
            // 'activated'.
            self.core.compositor_ui_state.tabwin_close(CloseReason::Cancelled);
        }

        self.clear_window_cycling_state();
        self.clear_tabwin_grabs();

        if let Some(window) = self
            .core
            .workspace_manager
            .find_window(|elem: &WindowElement| elem.window_id() == activated_window_id)
        {
            self.cycle_activate_window(&window);
        }
    }

    fn tabwin_destroyed(&mut self) {
        let is_incomplete = self.core.cycling_state.cycling_phase() != CyclingPhase::None;
        self.clear_window_cycling_state();
        if is_incomplete {
            self.clear_tabwin_grabs();
        }
    }

    fn window_menu_ready(&mut self) {
        if let Some(state) = self.core.window_menu_state.pending_state.take()
            && let Some(window_menu_anchor) = self.core.window_menu_state.anchor.as_ref()
            && let Some(pointer) = state.seat.get_pointer()
        {
            // Map the anchor window so rendering and hit-testing will work
            // without hacks.
            self.new_window(window_menu_anchor.clone(), state.location, false, None);

            // Release any active grab (e.g. ClickGrab from the button press
            // that triggered show_window_menu).  ClickGrab ignores the focus
            // parameter in motion events, so we must release it before
            // synthesizing events to the anchor window.
            pointer.unset_grab(self, state.serial, self.core.now().as_millis());

            // Next send motion to the anchor window to give it pointer focus.
            let pointer_loc = pointer.current_location();
            let motion_event = MotionEvent {
                location: pointer_loc,
                serial: SERIAL_COUNTER.next_serial(),
                time: self.core.now().as_millis(),
            };
            pointer.motion(self, Some((state.focus, pointer_loc)), &motion_event);
            pointer.frame(self);

            // Then synthesize a right-click so GTK will pop up the menu.
            let button_event = ButtonEvent {
                state: ButtonState::Pressed,
                serial: SERIAL_COUNTER.next_serial(),
                time: self.core.now().as_millis(),
                button: BTN_RIGHT,
            };
            pointer.button(self, &button_event);
            pointer.frame(self);
        }
    }

    fn window_menu_action(&mut self, window_id: u32, action: WindowMenuAction) {
        if let Some(window) = self.core.workspace_manager.find_window(|elem| elem.window_id() == window_id) {
            match action {
                WindowMenuAction::Action(action) => self.handle_window_menu_action(window, action),
                WindowMenuAction::MoveToWorkspace(index) => self.handle_window_menu_move_to_workspace(window, index),
                WindowMenuAction::MoveToOutput(direction) => self.handle_window_menu_move_to_output_in_direction(window, direction),
            }
        }
    }

    fn window_menu_dismissed(&mut self) {
        if let Some(target) = self.core.window_menu_state.target.take()
            && self.core.workspace_manager.active_workspace().window_location(&target).is_some()
        {
            self.focus_window(&target, SERIAL_COUNTER.next_serial(), None);
        }

        if let Some(window_menu_anchor) = self.core.window_menu_state.anchor.clone() {
            self.remove_window(&window_menu_anchor);

            let pointer = self.core.pointer.clone();

            // Synthesize a button release on the anchor window.  If the original trigger
            // for the menu popping up was indeed the right mouse button, this will be a
            // spurious release (which hopefully any app/toolkit should ignore), but if the
            // trigger was a different mouse button, or a touch event, not synthesizing the
            // release will cause the anchor window to think that our synthesized right
            // Then synthesize a right-click so GTK will pop up the menu.
            let button_event = ButtonEvent {
                state: ButtonState::Released,
                serial: SERIAL_COUNTER.next_serial(),
                time: self.core.now().as_millis(),
                button: BTN_RIGHT,
            };
            pointer.button(self, &button_event);
            pointer.frame(self);

            // Pointer focus will still be on the anchor window at this point, so let's
            // move it back to whatever surface is under the pointer.
            let pointer_loc = pointer.current_location();
            let focus_surface = self.surface_under(pointer_loc);
            pointer.motion(
                self,
                focus_surface,
                &MotionEvent {
                    location: pointer_loc,
                    serial: SERIAL_COUNTER.next_serial(),
                    time: self.core.now().as_millis(),
                },
            );
            pointer.frame(self);
        }
    }

    fn dialog_action(&mut self, dialog_id: DialogId, action_id: String) {
        if action_id == "accept" {
            self.core.kill_client_for_dialog(dialog_id);
        }
    }

    fn dialog_destroyed(&mut self, dialog_id: DialogId) {
        self.core.shell_state.dialog_destroyed(dialog_id);
    }
}

impl<BackendData: Backend + 'static> Xfwl4State<BackendData> {
    pub(in crate::core) fn pop_up_window_menu(
        &mut self,
        window: &WindowElement,
        seat: &Seat<Self>,
        serial: Serial,
        location: ActionLocation,
    ) {
        if let Some(window_location) = self.core.workspace_manager.active_workspace().window_location(window)
            && let Some(window_menu_anchor) = self.core.window_menu_state.anchor.as_ref()
            && let Some(window_menu_anchor_focus_target) = window_menu_anchor
                .wl_surface()
                .map(|surf| PointerFocusTarget::WlSurface(surf.into_owned()))
        {
            let location = match location {
                ActionLocation::Absolute(location) => location,
                ActionLocation::WindowRelative(location) => {
                    if let Some(window_decorations) = window.decoration_state().window_decorations() {
                        window_location + location + window_decorations.decorations_offset()
                    } else {
                        window_location + location - SpaceElement::geometry(&window.0).loc
                    }
                }
            };

            let workspace_names = if !window.sticky() {
                self.core
                    .workspace_manager
                    .workspaces()
                    .iter()
                    .map(|workspace| workspace.name().to_owned())
                    .collect()
            } else {
                vec![]
            };

            if let Some(current_output_and_rect) = self.output_and_rect_for_window(window) {
                let outputs_and_rects = self.outputs_and_rects();
                let adjacent_outputs = [
                    adjacent_monitor_in_direction(&outputs_and_rects, &current_output_and_rect, Direction::Up)
                        .map(|_| WindowMenuDirection::Up),
                    adjacent_monitor_in_direction(&outputs_and_rects, &current_output_and_rect, Direction::Down)
                        .map(|_| WindowMenuDirection::Down),
                    adjacent_monitor_in_direction(&outputs_and_rects, &current_output_and_rect, Direction::Left)
                        .map(|_| WindowMenuDirection::Left),
                    adjacent_monitor_in_direction(&outputs_and_rects, &current_output_and_rect, Direction::Right)
                        .map(|_| WindowMenuDirection::Right),
                ]
                .into_iter()
                .flatten()
                .collect::<Vec<_>>();

                let state = PendingWindowMenuState {
                    focus: window_menu_anchor_focus_target,
                    location,
                    seat: seat.clone(),
                    serial,
                };
                if let Err(err) = self.core.compositor_ui_state.create_window_menu::<Self>(UiWindowMenuState {
                    window_id: window.window_id(),
                    maximize_state: Some(window.maximized()),
                    can_minimize: window.capabilities().contains(WindowCapabilities::MINIMIZE),
                    can_move: true,
                    can_resize: !window.maximized(),
                    stacking_state: if window.normal_stacking() {
                        StackingState::Normal
                    } else if window.always_on_bottom() {
                        StackingState::AlwaysBelow
                    } else {
                        StackingState::AlwaysOnTop
                    },
                    shaded_state: window.capabilities().contains(WindowCapabilities::SHADE).then(|| window.shaded()),
                    fullscreen_state: Some(window.fullscreened()),
                    sticky: window.sticky(),
                    workspace_names,
                    current_workspace: self.core.workspace_manager.active_workspace_index(),
                    adjacent_outputs,
                    can_close: true,
                }) {
                    tracing::warn!("Failed to create window menu: {err}");
                } else {
                    self.core.window_menu_state.pending_state = Some(state);
                    self.core.window_menu_state.target = Some(window.clone());
                }
            }
        }
    }

    fn handle_window_menu_action(&mut self, window: WindowElement, action: ActionType) {
        match action {
            ActionType::ToggleMaximize => {
                if !window.maximized() {
                    self.set_window_maximized(&window, FillMode::Both, None);
                } else {
                    self.set_window_unmaximized(&window, None);
                }
            }
            ActionType::Minimize => self.set_window_minimized(&window),
            ActionType::MinimizeOtherWindows => {
                let other_windows = self
                    .core
                    .workspace_manager
                    .active_workspace()
                    .visible_windows()
                    .filter(|elem| **elem != window)
                    .cloned()
                    .collect::<Vec<_>>();
                for other_window in other_windows {
                    self.set_window_minimized(&other_window);
                }
            }
            ActionType::Move => {
                let serial = SERIAL_COUNTER.next_serial();
                // Set focus back to the window, because it may still be on the menu anchor
                // window.
                self.focus_window(&window, serial, None);
                // Use a keyboard trigger because we don't have a pointer button pressed
                self.start_window_move(window, self.core.seat.clone(), serial, GrabTrigger::Keyboard);
            }
            ActionType::Resize => {
                let serial = SERIAL_COUNTER.next_serial();
                // Set focus back to the window, because it may still be on the menu anchor
                // window.
                self.focus_window(&window, serial, None);
                self.start_window_resize(
                    window,
                    self.core.seat.clone(),
                    serial,
                    ResizeEdge::BOTTOM_RIGHT,
                    // Use a keyboard trigger because we don't have a pointer button pressed
                    GrabTrigger::Keyboard,
                );
            }
            ActionType::StackOnTop => self.set_window_always_on_top(&window),
            ActionType::StackNormal => self.set_window_normal_stacking(&window),
            ActionType::StackBelow => self.set_window_always_on_bottom(&window),
            ActionType::ToggleShade => {
                self.set_window_shaded(&window, !window.shaded());
            }
            ActionType::ToggleFullscreen => {
                let pointer_loc = self.core.pointer.current_location();
                let pointer_output = self
                    .core
                    .workspace_manager
                    .outputs()
                    .find(|output| {
                        output
                            .geometry()
                            .filter(|output_rect| output_rect.contains(pointer_loc.to_i32_round()))
                            .is_some()
                    })
                    .cloned();
                self.set_window_fullscreen(&window, pointer_output);
            }
            ActionType::ToggleSticky => {
                self.set_window_sticky(&window, !window.sticky());
            }
            ActionType::Close => {
                self.close_window(&window);
            }
        }
    }

    fn handle_window_menu_move_to_workspace(&mut self, window: WindowElement, index: u32) {
        self.move_window_to_workspace_index(&window, index);
    }

    fn handle_window_menu_move_to_output_in_direction(&mut self, window: WindowElement, direction: WindowMenuDirection) {
        self.move_window_to_output_in_direction(&window, direction.into());
    }
}

impl From<WindowMenuDirection> for Direction {
    fn from(value: WindowMenuDirection) -> Self {
        match value {
            WindowMenuDirection::Up => Self::Up,
            WindowMenuDirection::Down => Self::Down,
            WindowMenuDirection::Left => Self::Left,
            WindowMenuDirection::Right => Self::Right,
        }
    }
}
