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

//! AT-SPI2 accessibility via AccessKit.
//!
//! ltk does not implement the D-Bus AT-SPI2 protocol itself — that is
//! delegated to [`accesskit_unix`], which translates a backend-neutral
//! [`accesskit::TreeUpdate`] into the corresponding D-Bus calls,
//! registers the application against `org.a11y.Bus`, and emits the
//! property / state / focus change signals Orca and friends listen
//! for. Our job is two-way:
//!
//! 1. **Outbound**: every frame, after the layout pass has populated
//!    `widget_rects`, build a fresh [`accesskit::TreeUpdate`] from
//!    those rects and push it into the adapter through
//!    [`A11yState::update`]. The runtime calls this from the same
//!    place that runs `draw_frame`, so the accessible tree is always
//!    in sync with what the user can see.
//!
//! 2. **Inbound**: an [`accesskit::ActionRequest`] from an assistive
//!    technology (Orca pressing the "Default Action" on a button, a
//!    switch-control device focusing the next node, …) lands on the
//!    [`ActionForwarder`] running inside the adapter's worker
//!    thread; the forwarder pushes it through an `mpsc::Sender` into
//!    the run loop, which drains the channel each iteration and
//!    translates the request into a synthetic press / focus on the
//!    target widget.
//!
//! The adapter is constructed unconditionally — `accesskit_unix`
//! never refuses creation, it just stays inactive when no AT client
//! attaches to the session bus. Nothing else in the pipeline reads
//! the field except the per-frame tree update and the per-iteration
//! action inbox drain, so the rest of the runtime is unaware of
//! whether an AT is listening or not.

pub( crate ) mod tree;

use std::sync::mpsc;

use accesskit::{ ActionRequest, Rect as A11yRect, TreeUpdate };

/// Channel carrying inbound action requests from the AccessKit
/// adapter thread to the run loop.
pub( crate ) type ActionInbox = mpsc::Receiver<ActionRequest>;

/// Top-level accessibility state owned by `AppData`. Holds the
/// platform adapter alongside the receiver half of the action
/// channel.
pub( crate ) struct A11yState
{
	adapter:                accesskit_unix::Adapter,
	pub( crate ) action_rx: ActionInbox,
}

impl A11yState
{
	/// Bring up the AccessKit adapter. Returns `None` only when we
	/// reserve the right to refuse creation in the future (currently
	/// always `Some` — `accesskit_unix::Adapter::new` is infallible
	/// and stays inactive when no AT client is attached). Keeping
	/// the `Option` lets us toggle accessibility off at runtime via
	/// an env var or feature flag without re-plumbing every call
	/// site.
	pub( crate ) fn try_new( _app_name: &str, _app_id: &str ) -> Option<Self>
	{
		let ( action_tx, action_rx ) = mpsc::channel();
		let adapter = accesskit_unix::Adapter::new(
			ActivationStub,
			ActionForwarder { tx: action_tx },
			DeactivationStub,
		);
		Some( Self { adapter, action_rx } )
	}

	/// Push a freshly-built tree into the adapter. The closure is
	/// invoked synchronously **only** when AT-SPI2 is currently
	/// observing the application (an AT client connected to the
	/// daemon and queried us at least once), so the cost of
	/// constructing the tree is paid only when something is actually
	/// listening.
	pub( crate ) fn update<F>( &mut self, factory: F )
	where
		F: FnOnce() -> TreeUpdate,
	{
		self.adapter.update_if_active( factory );
	}

	/// AT-SPI clients filter by window focus; without it Orca skips
	/// the surface entirely.
	pub( crate ) fn set_window_focus( &mut self, focused: bool )
	{
		self.adapter.update_window_focus_state( focused );
	}

	pub( crate ) fn set_window_bounds( &mut self, width: f64, height: f64 )
	{
		let r = A11yRect { x0: 0.0, y0: 0.0, x1: width, y1: height };
		self.adapter.set_root_window_bounds( r, r );
	}

	#[ allow( dead_code ) ]
	pub( crate ) fn notify_focus( &mut self, new_focus: accesskit::NodeId )
	{
		self.adapter.update_if_active( ||
		{
			TreeUpdate
			{
				nodes: vec![],
				tree:  None,
				focus: new_focus,
			}
		} );
	}
}

/// Activation handler the adapter calls when an assistive technology
/// first attaches and asks for the initial tree. We hand back an
/// empty root — the first proper update comes from the run loop on
/// the very next frame, which is the natural moment to materialise a
/// real tree (the widget_rects are populated by then).
struct ActivationStub;

impl accesskit::ActivationHandler for ActivationStub
{
	fn request_initial_tree( &mut self ) -> Option<TreeUpdate>
	{
		Some( tree::empty_root() )
	}
}

/// Action handler that simply forwards every incoming request to the
/// main event loop. The translation from `ActionRequest` to a
/// synthetic press / focus happens on the main thread so it can
/// touch the rest of `AppData` without locking.
struct ActionForwarder
{
	tx: mpsc::Sender<ActionRequest>,
}

impl accesskit::ActionHandler for ActionForwarder
{
	fn do_action( &mut self, request: ActionRequest )
	{
		let _ = self.tx.send( request );
	}
}

/// Deactivation handler. AccessKit invokes this when every AT client
/// detaches; we have no per-client cleanup so it's a no-op.
struct DeactivationStub;

impl accesskit::DeactivationHandler for DeactivationStub
{
	fn deactivate_accessibility( &mut self ) {}
}
