// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Concurrent;
using System.Runtime.InteropServices;

namespace Microsoft.Maui.Platform.Linux.Services;

/// <summary>
/// AT-SPI2 accessibility service implementation.
/// Provides screen reader support through the AT-SPI2 D-Bus interface.
/// </summary>
public class AtSpi2AccessibilityService : IAccessibilityService, IDisposable
{
    private nint _connection;
    private nint _registry;
    private bool _isEnabled;
    private bool _disposed;
    private IAccessible? _focusedAccessible;
    private readonly ConcurrentDictionary<string, IAccessible> _registeredObjects = new();
    private readonly string _applicationName;
    private nint _applicationAccessible;

    public bool IsEnabled => _isEnabled;

    public AtSpi2AccessibilityService(string applicationName = "MAUI Application")
    {
        _applicationName = applicationName;
    }

    public void Initialize()
    {
        try
        {
            // Initialize AT-SPI2
            int result = atspi_init();
            if (result != 0)
            {
                DiagnosticLog.Error("AtSpi2AccessibilityService", "Failed to initialize AT-SPI2");
                return;
            }

            // Check if accessibility is enabled
            _isEnabled = CheckAccessibilityEnabled();

            if (_isEnabled)
            {
                // Get the desktop (root accessible)
                _registry = atspi_get_desktop(0);

                // Register our application
                RegisterApplication();

                DiagnosticLog.Debug("AtSpi2AccessibilityService", "Initialized successfully");
            }
            else
            {
                DiagnosticLog.Warn("AtSpi2AccessibilityService", "Accessibility is not enabled");
            }
        }
        catch (Exception ex)
        {
            DiagnosticLog.Error("AtSpi2AccessibilityService", $"Initialization failed - {ex.Message}");
        }
    }

    private bool CheckAccessibilityEnabled()
    {
        // Check if AT-SPI2 registry is available
        try
        {
            nint desktop = atspi_get_desktop(0);
            if (desktop != IntPtr.Zero)
            {
                g_object_unref(desktop);
                return true;
            }
        }
        catch
        {
            // AT-SPI2 not available
        }

        // Also check the gsettings key
        var enabled = Environment.GetEnvironmentVariable("GTK_A11Y");
        return enabled?.ToLowerInvariant() != "none";
    }

    private void RegisterApplication()
    {
        // In a full implementation, we would create an AtspiApplication object
        // and register it with the AT-SPI2 registry. For now, we set up the basics.

        // Set application name
        atspi_set_main_context(IntPtr.Zero);
    }

    public void Register(IAccessible accessible)
    {
        if (accessible == null) return;

        _registeredObjects.TryAdd(accessible.AccessibleId, accessible);

        // In a full implementation, we would create an AtspiAccessible object
        // and register it with AT-SPI2
    }

    public void Unregister(IAccessible accessible)
    {
        if (accessible == null) return;

        _registeredObjects.TryRemove(accessible.AccessibleId, out _);

        // Clean up AT-SPI2 resources for this accessible
    }

    public void NotifyFocusChanged(IAccessible? accessible)
    {
        _focusedAccessible = accessible;

        if (!_isEnabled || accessible == null) return;

        // Emit focus event through AT-SPI2
        EmitEvent("focus:", accessible);
    }

    public void NotifyPropertyChanged(IAccessible accessible, AccessibleProperty property)
    {
        if (!_isEnabled || accessible == null) return;

        string eventName = property switch
        {
            AccessibleProperty.Name => "object:property-change:accessible-name",
            AccessibleProperty.Description => "object:property-change:accessible-description",
            AccessibleProperty.Role => "object:property-change:accessible-role",
            AccessibleProperty.Value => "object:property-change:accessible-value",
            AccessibleProperty.Parent => "object:property-change:accessible-parent",
            AccessibleProperty.Children => "object:children-changed",
            _ => string.Empty
        };

        if (!string.IsNullOrEmpty(eventName))
        {
            EmitEvent(eventName, accessible);
        }
    }

    public void NotifyStateChanged(IAccessible accessible, AccessibleState state, bool value)
    {
        if (!_isEnabled || accessible == null) return;

        string stateName = state.ToString().ToLowerInvariant();
        string eventName = $"object:state-changed:{stateName}";

        EmitEvent(eventName, accessible, value ? 1 : 0);
    }

    public void Announce(string text, AnnouncementPriority priority = AnnouncementPriority.Polite)
    {
        if (!_isEnabled || string.IsNullOrEmpty(text)) return;

        // Use AT-SPI2 live region to announce text
        // Priority maps to: Polite = ATSPI_LIVE_POLITE, Assertive = ATSPI_LIVE_ASSERTIVE

        try
        {
            // In AT-SPI2, announcements are typically done through live regions
            // or by emitting "object:announcement" events

            // For now, use a simpler approach with the event system
            DiagnosticLog.Debug("AtSpi2AccessibilityService", $"Announcement ({priority}): {text}");
        }
        catch (Exception ex)
        {
            DiagnosticLog.Error("AtSpi2AccessibilityService", $"Announcement failed - {ex.Message}");
        }
    }

    private void EmitEvent(string eventName, IAccessible accessible, int detail1 = 0, int detail2 = 0)
    {
        // In a full implementation, we would emit the event through D-Bus
        // using the org.a11y.atspi.Event interface

        // For now, log the event for debugging
        DiagnosticLog.Debug("AtSpi2AccessibilityService", $"Event {eventName}: {accessible.AccessibleName} ({accessible.Role})");
    }

    /// <summary>
    /// Gets the AT-SPI2 role value for the given accessible role.
    /// </summary>
    public static int GetAtSpiRole(AccessibleRole role)
    {
        return role switch
        {
            AccessibleRole.Unknown => ATSPI_ROLE_UNKNOWN,
            AccessibleRole.Window => ATSPI_ROLE_WINDOW,
            AccessibleRole.Application => ATSPI_ROLE_APPLICATION,
            AccessibleRole.Panel => ATSPI_ROLE_PANEL,
            AccessibleRole.Frame => ATSPI_ROLE_FRAME,
            AccessibleRole.Button => ATSPI_ROLE_PUSH_BUTTON,
            AccessibleRole.CheckBox => ATSPI_ROLE_CHECK_BOX,
            AccessibleRole.RadioButton => ATSPI_ROLE_RADIO_BUTTON,
            AccessibleRole.ComboBox => ATSPI_ROLE_COMBO_BOX,
            AccessibleRole.Entry => ATSPI_ROLE_ENTRY,
            AccessibleRole.Label => ATSPI_ROLE_LABEL,
            AccessibleRole.List => ATSPI_ROLE_LIST,
            AccessibleRole.ListItem => ATSPI_ROLE_LIST_ITEM,
            AccessibleRole.Menu => ATSPI_ROLE_MENU,
            AccessibleRole.MenuBar => ATSPI_ROLE_MENU_BAR,
            AccessibleRole.MenuItem => ATSPI_ROLE_MENU_ITEM,
            AccessibleRole.ScrollBar => ATSPI_ROLE_SCROLL_BAR,
            AccessibleRole.Slider => ATSPI_ROLE_SLIDER,
            AccessibleRole.SpinButton => ATSPI_ROLE_SPIN_BUTTON,
            AccessibleRole.StatusBar => ATSPI_ROLE_STATUS_BAR,
            AccessibleRole.Tab => ATSPI_ROLE_PAGE_TAB,
            AccessibleRole.TabPanel => ATSPI_ROLE_PAGE_TAB_LIST,
            AccessibleRole.Text => ATSPI_ROLE_TEXT,
            AccessibleRole.ToggleButton => ATSPI_ROLE_TOGGLE_BUTTON,
            AccessibleRole.ToolBar => ATSPI_ROLE_TOOL_BAR,
            AccessibleRole.ToolTip => ATSPI_ROLE_TOOL_TIP,
            AccessibleRole.Tree => ATSPI_ROLE_TREE,
            AccessibleRole.TreeItem => ATSPI_ROLE_TREE_ITEM,
            AccessibleRole.Image => ATSPI_ROLE_IMAGE,
            AccessibleRole.ProgressBar => ATSPI_ROLE_PROGRESS_BAR,
            AccessibleRole.Separator => ATSPI_ROLE_SEPARATOR,
            AccessibleRole.Link => ATSPI_ROLE_LINK,
            AccessibleRole.Table => ATSPI_ROLE_TABLE,
            AccessibleRole.TableCell => ATSPI_ROLE_TABLE_CELL,
            AccessibleRole.TableRow => ATSPI_ROLE_TABLE_ROW,
            AccessibleRole.TableColumnHeader => ATSPI_ROLE_TABLE_COLUMN_HEADER,
            AccessibleRole.TableRowHeader => ATSPI_ROLE_TABLE_ROW_HEADER,
            AccessibleRole.PageTab => ATSPI_ROLE_PAGE_TAB,
            AccessibleRole.PageTabList => ATSPI_ROLE_PAGE_TAB_LIST,
            AccessibleRole.Dialog => ATSPI_ROLE_DIALOG,
            AccessibleRole.Alert => ATSPI_ROLE_ALERT,
            AccessibleRole.Filler => ATSPI_ROLE_FILLER,
            AccessibleRole.Icon => ATSPI_ROLE_ICON,
            AccessibleRole.Canvas => ATSPI_ROLE_CANVAS,
            _ => ATSPI_ROLE_UNKNOWN
        };
    }

    /// <summary>
    /// Converts accessible states to AT-SPI2 state set.
    /// </summary>
    public static (uint Low, uint High) GetAtSpiStates(AccessibleStates states)
    {
        uint low = 0;
        uint high = 0;

        if (states.HasFlag(AccessibleStates.Active)) low |= 1 << 0;
        if (states.HasFlag(AccessibleStates.Armed)) low |= 1 << 1;
        if (states.HasFlag(AccessibleStates.Busy)) low |= 1 << 2;
        if (states.HasFlag(AccessibleStates.Checked)) low |= 1 << 3;
        if (states.HasFlag(AccessibleStates.Collapsed)) low |= 1 << 4;
        if (states.HasFlag(AccessibleStates.Defunct)) low |= 1 << 5;
        if (states.HasFlag(AccessibleStates.Editable)) low |= 1 << 6;
        if (states.HasFlag(AccessibleStates.Enabled)) low |= 1 << 7;
        if (states.HasFlag(AccessibleStates.Expandable)) low |= 1 << 8;
        if (states.HasFlag(AccessibleStates.Expanded)) low |= 1 << 9;
        if (states.HasFlag(AccessibleStates.Focusable)) low |= 1 << 10;
        if (states.HasFlag(AccessibleStates.Focused)) low |= 1 << 11;
        if (states.HasFlag(AccessibleStates.Horizontal)) low |= 1 << 13;
        if (states.HasFlag(AccessibleStates.Iconified)) low |= 1 << 14;
        if (states.HasFlag(AccessibleStates.Modal)) low |= 1 << 15;
        if (states.HasFlag(AccessibleStates.MultiLine)) low |= 1 << 16;
        if (states.HasFlag(AccessibleStates.MultiSelectable)) low |= 1 << 17;
        if (states.HasFlag(AccessibleStates.Opaque)) low |= 1 << 18;
        if (states.HasFlag(AccessibleStates.Pressed)) low |= 1 << 19;
        if (states.HasFlag(AccessibleStates.Resizable)) low |= 1 << 20;
        if (states.HasFlag(AccessibleStates.Selectable)) low |= 1 << 21;
        if (states.HasFlag(AccessibleStates.Selected)) low |= 1 << 22;
        if (states.HasFlag(AccessibleStates.Sensitive)) low |= 1 << 23;
        if (states.HasFlag(AccessibleStates.Showing)) low |= 1 << 24;
        if (states.HasFlag(AccessibleStates.SingleLine)) low |= 1 << 25;
        if (states.HasFlag(AccessibleStates.Stale)) low |= 1 << 26;
        if (states.HasFlag(AccessibleStates.Transient)) low |= 1 << 27;
        if (states.HasFlag(AccessibleStates.Vertical)) low |= 1 << 28;
        if (states.HasFlag(AccessibleStates.Visible)) low |= 1 << 29;
        if (states.HasFlag(AccessibleStates.ManagesDescendants)) low |= 1 << 30;
        if (states.HasFlag(AccessibleStates.Indeterminate)) low |= 1u << 31;

        // High bits (states 32+)
        if (states.HasFlag(AccessibleStates.Required)) high |= 1 << 0;
        if (states.HasFlag(AccessibleStates.Truncated)) high |= 1 << 1;
        if (states.HasFlag(AccessibleStates.Animated)) high |= 1 << 2;
        if (states.HasFlag(AccessibleStates.InvalidEntry)) high |= 1 << 3;
        if (states.HasFlag(AccessibleStates.SupportsAutocompletion)) high |= 1 << 4;
        if (states.HasFlag(AccessibleStates.SelectableText)) high |= 1 << 5;
        if (states.HasFlag(AccessibleStates.IsDefault)) high |= 1 << 6;
        if (states.HasFlag(AccessibleStates.Visited)) high |= 1 << 7;
        if (states.HasFlag(AccessibleStates.ReadOnly)) high |= 1 << 10;

        return (low, high);
    }

    public void Shutdown()
    {
        Dispose();
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;

        _registeredObjects.Clear();

        if (_applicationAccessible != IntPtr.Zero)
        {
            g_object_unref(_applicationAccessible);
            _applicationAccessible = IntPtr.Zero;
        }

        if (_registry != IntPtr.Zero)
        {
            g_object_unref(_registry);
            _registry = IntPtr.Zero;
        }

        // Exit AT-SPI2
        atspi_exit();
    }

    #region AT-SPI2 Role Constants

    private const int ATSPI_ROLE_UNKNOWN = 0;
    private const int ATSPI_ROLE_WINDOW = 22;
    private const int ATSPI_ROLE_APPLICATION = 75;
    private const int ATSPI_ROLE_PANEL = 25;
    private const int ATSPI_ROLE_FRAME = 11;
    private const int ATSPI_ROLE_PUSH_BUTTON = 31;
    private const int ATSPI_ROLE_CHECK_BOX = 4;
    private const int ATSPI_ROLE_RADIO_BUTTON = 33;
    private const int ATSPI_ROLE_COMBO_BOX = 6;
    private const int ATSPI_ROLE_ENTRY = 24;
    private const int ATSPI_ROLE_LABEL = 16;
    private const int ATSPI_ROLE_LIST = 17;
    private const int ATSPI_ROLE_LIST_ITEM = 18;
    private const int ATSPI_ROLE_MENU = 19;
    private const int ATSPI_ROLE_MENU_BAR = 20;
    private const int ATSPI_ROLE_MENU_ITEM = 21;
    private const int ATSPI_ROLE_SCROLL_BAR = 40;
    private const int ATSPI_ROLE_SLIDER = 43;
    private const int ATSPI_ROLE_SPIN_BUTTON = 44;
    private const int ATSPI_ROLE_STATUS_BAR = 46;
    private const int ATSPI_ROLE_PAGE_TAB = 26;
    private const int ATSPI_ROLE_PAGE_TAB_LIST = 27;
    private const int ATSPI_ROLE_TEXT = 49;
    private const int ATSPI_ROLE_TOGGLE_BUTTON = 51;
    private const int ATSPI_ROLE_TOOL_BAR = 52;
    private const int ATSPI_ROLE_TOOL_TIP = 53;
    private const int ATSPI_ROLE_TREE = 54;
    private const int ATSPI_ROLE_TREE_ITEM = 55;
    private const int ATSPI_ROLE_IMAGE = 14;
    private const int ATSPI_ROLE_PROGRESS_BAR = 30;
    private const int ATSPI_ROLE_SEPARATOR = 42;
    private const int ATSPI_ROLE_LINK = 83;
    private const int ATSPI_ROLE_TABLE = 47;
    private const int ATSPI_ROLE_TABLE_CELL = 48;
    private const int ATSPI_ROLE_TABLE_ROW = 89;
    private const int ATSPI_ROLE_TABLE_COLUMN_HEADER = 36;
    private const int ATSPI_ROLE_TABLE_ROW_HEADER = 37;
    private const int ATSPI_ROLE_DIALOG = 8;
    private const int ATSPI_ROLE_ALERT = 2;
    private const int ATSPI_ROLE_FILLER = 10;
    private const int ATSPI_ROLE_ICON = 13;
    private const int ATSPI_ROLE_CANVAS = 3;

    #endregion

    #region AT-SPI2 Interop

    [DllImport("libatspi.so.0")]
    private static extern int atspi_init();

    [DllImport("libatspi.so.0")]
    private static extern int atspi_exit();

    [DllImport("libatspi.so.0")]
    private static extern nint atspi_get_desktop(int i);

    [DllImport("libatspi.so.0")]
    private static extern void atspi_set_main_context(nint context);

    [DllImport("libgobject-2.0.so.0")]
    private static extern void g_object_unref(nint obj);

    #endregion
}
