#nullable enable

using System;
using System.Diagnostics;
using System.Reactive.Disposables;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Logging;
using Avalonia.Platform;
using Tmds.DBus;

[assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]

[assembly:
    InternalsVisibleTo(
        "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]

namespace Avalonia.FreeDesktop
{
    internal class DBusTrayIconImpl : ITrayIconImpl
    {
        private static int s_trayIconInstanceId;

        private readonly ObjectPath _dbusMenuPath;
        private readonly Connection? _connection;
        private IDisposable? _serviceWatchDisposable;

        private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
        private IStatusNotifierWatcher? _statusNotifierWatcher;
        private DbusPixmap _icon;

        private string? _sysTrayServiceName;
        private string? _tooltipText;
        private bool _isDisposed;
        private bool _serviceConnected;
        private bool _isVisible = true;

        public bool IsActive { get; private set; }
        public INativeMenuExporter? MenuExporter { get; }
        public Action? OnClicked { get; set; }
        public Func<IWindowIconImpl?, uint[]>? IconConverterDelegate { get; set; }

        public DBusTrayIconImpl()
        {
            _connection = DBusHelper.TryCreateNewConnection();

            if (_connection is null)
            {
                Logger.TryGet(LogEventLevel.Error, "DBUS")
                    ?.Log(this, "Unable to get a dbus connection for system tray icons.");

                return;
            }

            IsActive = true;

            _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;

            MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);

            WatchAsync();
        }

        private void InitializeSNWService()
        {
            if (_connection is null || _isDisposed) return;

            try
            {
                _statusNotifierWatcher = _connection.CreateProxy<IStatusNotifierWatcher>(
                    "org.kde.StatusNotifierWatcher",
                    "/StatusNotifierWatcher");
            }
            catch
            {
                Logger.TryGet(LogEventLevel.Error, "DBUS")
                    ?.Log(this,
                        "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it.");

                return;
            }

            _serviceConnected = true;
        }

        private async void WatchAsync()
        {
            try
            {
                _serviceWatchDisposable =
                    await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!;
            }
            catch (Exception e)
            {
                Logger.TryGet(LogEventLevel.Error, "DBUS")
                    ?.Log(this,
                        $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}");
            }
        }

        private void OnNameChange(ServiceOwnerChangedEventArgs obj)
        {
            if (_isDisposed)
                return;

            if (!_serviceConnected & obj.NewOwner != null)
            {
                _serviceConnected = true;
                InitializeSNWService();

                DestroyTrayIcon();

                if (_isVisible)
                {
                    CreateTrayIcon();
                }
            }
            else if (_serviceConnected & obj.NewOwner is null)
            {
                DestroyTrayIcon();
                _serviceConnected = false;
            }
        }

        private void CreateTrayIcon()
        {
            if (_connection is null || !_serviceConnected || _isDisposed)
                return;

            var pid = Process.GetCurrentProcess().Id;
            var tid = s_trayIconInstanceId++;

            _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
            _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath);

            try
            {
                _connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
                _connection.RegisterServiceAsync(_sysTrayServiceName);
                _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
            }
            catch (Exception e)
            {
                Logger.TryGet(LogEventLevel.Error, "DBUS")
                    ?.Log(this, $"Error creating a DBus tray icon: {e}.");

                _serviceConnected = false;
            }

            _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
            _statusNotifierItemDbusObj.SetIcon(_icon);

            _statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
        }

        private void DestroyTrayIcon()
        {
            if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
                return;

            _connection.UnregisterObject(_statusNotifierItemDbusObj);
            _connection.UnregisterServiceAsync(_sysTrayServiceName);
        }

        public void Dispose()
        {
            IsActive = false;
            _isDisposed = true;
            DestroyTrayIcon();
            _connection?.Dispose();
            _serviceWatchDisposable?.Dispose();
        }

        public void SetIcon(IWindowIconImpl? icon)
        {
            if (_isDisposed || IconConverterDelegate is null)
                return;

            if (icon is null)
            {
                _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
                return;
            }

            var x11iconData = IconConverterDelegate(icon);

            if (x11iconData.Length == 0) return;

            var w = (int)x11iconData[0];
            var h = (int)x11iconData[1];

            var pixLength = w * h;
            var pixByteArrayCounter = 0;
            var pixByteArray = new byte[w * h * 4];

            for (var i = 0; i < pixLength; i++)
            {
                var rawPixel = x11iconData[i + 2];
                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24);
                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16);
                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8);
                pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF);
            }

            _icon = new DbusPixmap(w, h, pixByteArray);
            _statusNotifierItemDbusObj?.SetIcon(_icon);
        }

        public void SetIsVisible(bool visible)
        {
            if (_isDisposed)
                return;

            switch (visible)
            {
                case true when !_isVisible:
                    DestroyTrayIcon();
                    CreateTrayIcon();
                    break;
                case false when _isVisible:
                    DestroyTrayIcon();
                    break;
            }

            _isVisible = visible;
        }

        public void SetToolTipText(string? text)
        {
            if (_isDisposed || text is null)
                return;
            _tooltipText = text;
            _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText);
        }
    }

    /// <summary>
    /// DBus Object used for setting system tray icons.
    /// </summary>
    /// <remarks>
    /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
    /// </remarks>
    internal class StatusNotifierItemDbusObj : IStatusNotifierItem
    {
        private readonly StatusNotifierItemProperties _backingProperties;
        public event Action? OnTitleChanged;
        public event Action? OnIconChanged;
        public event Action? OnAttentionIconChanged;
        public event Action? OnOverlayIconChanged;
        public event Action? OnTooltipChanged;
        public Action<string>? NewStatusAsync { get; set; }
        public Action? ActivationDelegate { get; set; }
        public ObjectPath ObjectPath { get; }

        public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath)
        {
            ObjectPath = new ObjectPath($"/StatusNotifierItem");

            _backingProperties = new StatusNotifierItemProperties
            {
                Menu = dbusmenuPath, // Needs a dbus menu somehow
                ToolTip = new ToolTip("")
            };

            InvalidateAll();
        }

        public Task ContextMenuAsync(int x, int y) => Task.CompletedTask;

        public Task ActivateAsync(int x, int y)
        {
            ActivationDelegate?.Invoke();
            return Task.CompletedTask;
        }

        public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask;

        public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask;

        public void InvalidateAll()
        {
            OnTitleChanged?.Invoke();
            OnIconChanged?.Invoke();
            OnOverlayIconChanged?.Invoke();
            OnAttentionIconChanged?.Invoke();
            OnTooltipChanged?.Invoke();
        }

        public Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
        {
            OnTitleChanged += handler;
            return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
        }

        public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
        {
            OnIconChanged += handler;
            return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
        }

        public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError)
        {
            OnAttentionIconChanged += handler;
            return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler));
        }

        public Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
        {
            OnOverlayIconChanged += handler;
            return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
        }

        public Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError)
        {
            OnTooltipChanged += handler;
            return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler));
        }

        public Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError)
        {
            NewStatusAsync += handler;
            return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
        }

        public Task<object?> GetAsync(string prop)
        {
            return Task.FromResult<object?>(prop switch
            {
                nameof(_backingProperties.Category) => _backingProperties.Category,
                nameof(_backingProperties.Id) => _backingProperties.Id,
                nameof(_backingProperties.Menu) => _backingProperties.Menu,
                nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap,
                nameof(_backingProperties.Status) => _backingProperties.Status,
                nameof(_backingProperties.Title) => _backingProperties.Title,
                nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip,
                _ => null
            });
        }

        public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);

        public Task SetAsync(string prop, object val) => Task.CompletedTask;

        public Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler) =>
            Task.FromResult(Disposable.Empty);

        public void SetIcon(DbusPixmap dbusPixmap)
        {
            _backingProperties.IconPixmap = new[] { dbusPixmap };
            InvalidateAll();
        }

        public void SetTitleAndTooltip(string? text)
        {
            if (text is null)
                return;

            _backingProperties.Id = text;
            _backingProperties.Category = "ApplicationStatus";
            _backingProperties.Status = text;
            _backingProperties.Title = text;
            _backingProperties.ToolTip = new ToolTip(text);

            InvalidateAll();
        }
    }

    [DBusInterface("org.kde.StatusNotifierWatcher")]
    internal interface IStatusNotifierWatcher : IDBusObject
    {
        Task RegisterStatusNotifierItemAsync(string Service);
        Task RegisterStatusNotifierHostAsync(string Service);
    }

    [DBusInterface("org.kde.StatusNotifierItem")]
    internal interface IStatusNotifierItem : IDBusObject
    {
        Task ContextMenuAsync(int x, int y);
        Task ActivateAsync(int x, int y);
        Task SecondaryActivateAsync(int x, int y);
        Task ScrollAsync(int delta, string orientation);
        Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError);
        Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError);
        Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError);
        Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
        Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
        Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
        Task<object?> GetAsync(string prop);
        Task<StatusNotifierItemProperties> GetAllAsync();
        Task SetAsync(string prop, object val);
        Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
    }

    // This class is used by Tmds.Dbus to ferry properties
    // from the SNI spec.
    // Don't change this to actual C# properties since
    // Tmds.Dbus will get confused.
    [Dictionary]
    internal class StatusNotifierItemProperties
    {
        public string? Category;

        public string? Id;

        public string? Title;

        public string? Status;

        public ObjectPath Menu;

        public DbusPixmap[]? IconPixmap;

        public ToolTip ToolTip;
    }

    internal struct ToolTip
    {
        public readonly string First;
        public readonly DbusPixmap[] Second;
        public readonly string Third;
        public readonly string Fourth;

        private static readonly DbusPixmap[] s_blank =
        {
            new DbusPixmap(0, 0, Array.Empty<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>())
        };

        public ToolTip(string message) : this("", s_blank, message, "")
        {
        }

        public ToolTip(string first, DbusPixmap[] second, string third, string fourth)
        {
            First = first;
            Second = second;
            Third = third;
            Fourth = fourth;
        }
    }

    internal readonly struct DbusPixmap
    {
        public readonly int Width;
        public readonly int Height;
        public readonly byte[] Data;

        public DbusPixmap(int width, int height, byte[] data)
        {
            Width = width;
            Height = height;
            Data = data;
        }

        public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 });
    }
}
