// SPDX-License-Identifier: LGPL-2.1-only
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>

use smithay_client_toolkit::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge;

use super::app_data::AppData;
use super::surface::{ SurfaceFocus, SurfaceKind };
use crate::app::App;
use crate::types::{ CursorShape, Point };

/// Classify a pointer position against a surface of size `w × h` (in
/// physical pixels). Returns the resize edge if the pointer is inside
/// the `border`-thick grab band along any edge or corner.
pub( crate ) fn resize_edge_for_pos( pos: Point, w: f32, h: f32, border: f32 ) -> Option<ResizeEdge>
{
	let on_left   = pos.x < border;
	let on_right  = pos.x > w - border;
	let on_top    = pos.y < border;
	let on_bottom = pos.y > h - border;
	Some( match ( on_top, on_bottom, on_left, on_right )
	{
		( true,  _,    true,  _    ) => ResizeEdge::TopLeft,
		( true,  _,    _,     true ) => ResizeEdge::TopRight,
		( _,     true, true,  _    ) => ResizeEdge::BottomLeft,
		( _,     true, _,     true ) => ResizeEdge::BottomRight,
		( true,  _,    _,     _    ) => ResizeEdge::Top,
		( _,     true, _,     _    ) => ResizeEdge::Bottom,
		( _,     _,    true,  _    ) => ResizeEdge::Left,
		( _,     _,    _,     true ) => ResizeEdge::Right,
		_                            => return None,
	} )
}

/// Map an [`ResizeEdge`] to the [`CursorShape`](crate::types::CursorShape)
/// to display while hovering / dragging it.
pub( crate ) fn resize_edge_to_cursor( edge: ResizeEdge ) -> CursorShape
{
	use CursorShape::*;
	match edge
	{
		ResizeEdge::Top         => NResize,
		ResizeEdge::Bottom      => SResize,
		ResizeEdge::Left        => WResize,
		ResizeEdge::Right       => EResize,
		ResizeEdge::TopLeft     => NwResize,
		ResizeEdge::TopRight    => NeResize,
		ResizeEdge::BottomLeft  => SwResize,
		ResizeEdge::BottomRight => SeResize,
		_                       => Default,
	}
}

impl<A: App> AppData<A>
{
	/// Convert a public [`CursorShape`](crate::types::CursorShape) into
	/// the wire-protocol [`Shape`] enum the compositor expects.
	fn cursor_shape_to_wp( shape: CursorShape )
		-> smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape
	{
		use CursorShape as C;
		use smithay_client_toolkit::reexports::protocols::wp::cursor_shape::v1::client::wp_cursor_shape_device_v1::Shape as S;
		match shape
		{
			C::Default      => S::Default,
			C::ContextMenu  => S::ContextMenu,
			C::Help         => S::Help,
			C::Pointer      => S::Pointer,
			C::Progress     => S::Progress,
			C::Wait         => S::Wait,
			C::Cell         => S::Cell,
			C::Crosshair    => S::Crosshair,
			C::Text         => S::Text,
			C::VerticalText => S::VerticalText,
			C::Alias        => S::Alias,
			C::Copy         => S::Copy,
			C::Move         => S::Move,
			C::NoDrop       => S::NoDrop,
			C::NotAllowed   => S::NotAllowed,
			C::Grab         => S::Grab,
			C::Grabbing     => S::Grabbing,
			C::EResize      => S::EResize,
			C::NResize      => S::NResize,
			C::NeResize     => S::NeResize,
			C::NwResize     => S::NwResize,
			C::SResize      => S::SResize,
			C::SeResize     => S::SeResize,
			C::SwResize     => S::SwResize,
			C::WResize      => S::WResize,
			C::EwResize     => S::EwResize,
			C::NsResize     => S::NsResize,
			C::NeswResize   => S::NeswResize,
			C::NwseResize   => S::NwseResize,
			C::ColResize    => S::ColResize,
			C::RowResize    => S::RowResize,
			C::AllScroll    => S::AllScroll,
			C::ZoomIn       => S::ZoomIn,
			C::ZoomOut      => S::ZoomOut,
		}
	}

	/// Compute the cursor shape that should be active right now and
	/// push it to the compositor if it changed since the last
	/// dispatch. Precedence:
	///
	/// 1. [`crate::App::cursor_override`] — application-driven busy
	///    state, wins over everything.
	/// 2. Active slider drag — `Grabbing` while the gesture machine
	///    holds a `dragging_slider`.
	/// 3. Cursor declared by the widget under the pointer
	///    ([`crate::widget::LaidOutWidget::cursor`]).
	/// 4. `Default` — pointer is over surface chrome / empty space.
	pub( crate ) fn dispatch_cursor_shape( &mut self, focus: SurfaceFocus )
	{
		let target = {
			let app_override   = self.app.cursor_override();
			let dragging       = self.surface( focus ).gesture.dragging_slider.is_some();
			let hover_cursor   = self.surface( focus ).hovered_idx
				.and_then( |idx| crate::tree::find_widget( &self.surface( focus ).frame.widget_rects, idx ) )
				.map( |w| w.cursor )
				.unwrap_or( CursorShape::Default );
			let resize_cursor  = self.resize_edge_under_pointer( focus ).map( resize_edge_to_cursor );

			// Resize-edge hover wins over the widget cursor and over a
			// `Default` app override, but loses to a non-default app
			// override (so a "busy" / "wait" state still trumps chrome).
			let allow_chrome = app_override.is_none()
				|| app_override == Some( CursorShape::Default );
			if let Some( c ) = resize_cursor.filter( |_| allow_chrome )
			{
				c
			} else if let Some( o ) = app_override {
				o
			} else if dragging {
				CursorShape::Grabbing
			} else {
				hover_cursor
			}
		};
		// Skip when the compositor already shows the right shape.
		// `current_cursor_shape == None` means "we have not pushed
		// anything since the last `Enter`" — e.g. the pointer just
		// arrived from another client's surface, where the prior
		// client may have asked for a text I-beam. We MUST push
		// unconditionally in that case so the user does not see the
		// stranger cursor lingering until they hit a widget.
		if self.current_cursor_shape == Some( target ) { return; }

		if let Some( device ) = self.cursor_shape_device.as_ref()
		{
			device.set_shape( self.last_pointer_enter_serial, Self::cursor_shape_to_wp( target ) );
			self.current_cursor_shape = Some( target );
		}
	}

	/// Width (logical pixels) of the resize-grab band along each
	/// edge of an xdg-toplevel surface. 6 px is the GTK / KWin
	/// convention.
	pub( crate ) const RESIZE_BORDER_LOGICAL: f32 = 6.0;

	/// Return the [`ResizeEdge`] the pointer currently sits on, or
	/// `None` if it is not in a resize-grab band. Only fires for the
	/// main surface when it is an xdg-toplevel — layer-shell surfaces
	/// and overlays cannot be resized by the user.
	pub( crate ) fn resize_edge_under_pointer( &self, focus: SurfaceFocus ) -> Option<ResizeEdge>
	{
		if !matches!( focus, SurfaceFocus::Main ) { return None; }
		if !matches!( self.main.surface, SurfaceKind::Window( _ ) ) { return None; }
		let sf     = self.surface( focus ).scale_factor.max( 1 ) as f32;
		let border = Self::RESIZE_BORDER_LOGICAL * sf;
		let w      = self.surface( focus ).physical_width()  as f32;
		let h      = self.surface( focus ).physical_height() as f32;
		resize_edge_for_pos( self.pointer_pos, w, h, border )
	}
}

#[ cfg( test ) ]
mod resize_edge_tests
{
	use super::{ resize_edge_for_pos, ResizeEdge };
	use crate::types::Point;

	const W: f32 = 800.0;
	const H: f32 = 600.0;
	const B: f32 = 6.0;

	fn at( x: f32, y: f32 ) -> Option<ResizeEdge>
	{
		resize_edge_for_pos( Point { x, y }, W, H, B )
	}

	#[ test ]
	fn middle_is_none()
	{
		assert!( at( 400.0, 300.0 ).is_none() );
	}

	#[ test ]
	fn just_inside_border_is_none()
	{
		// 6 px border; (6, 6) is one pixel inside on both axes.
		assert!( at( 6.0, 6.0 ).is_none() );
	}

	#[ test ]
	fn corners_take_precedence_over_single_edges()
	{
		assert_eq!( at( 1.0,    1.0    ), Some( ResizeEdge::TopLeft     ) );
		assert_eq!( at( W - 1.0, 1.0    ), Some( ResizeEdge::TopRight    ) );
		assert_eq!( at( 1.0,    H - 1.0 ), Some( ResizeEdge::BottomLeft  ) );
		assert_eq!( at( W - 1.0, H - 1.0 ), Some( ResizeEdge::BottomRight ) );
	}

	#[ test ]
	fn straight_edges()
	{
		assert_eq!( at( 400.0, 1.0     ), Some( ResizeEdge::Top    ) );
		assert_eq!( at( 400.0, H - 1.0 ), Some( ResizeEdge::Bottom ) );
		assert_eq!( at( 1.0,   300.0   ), Some( ResizeEdge::Left   ) );
		assert_eq!( at( W - 1.0, 300.0 ), Some( ResizeEdge::Right  ) );
	}
}
