using System;
using System.Collections.Generic;
using System.Linq;

namespace ZB.MOM.WW.MxGateway.Worker.MxAccess;

/// <summary>
///     Tracks the server, item, and advice handles owned by one MXAccess session.
/// </summary>
/// <remarks>
///     The registry is STA-confined by contract: every mutation and every query
///     runs on the session's dedicated STA thread (see
///     <see cref="MxAccessStaSession"/>, which marshals all registry access
///     through that thread), so there is no locking here and the memoized views
///     and secondary indexes below need none either.
/// </remarks>
public sealed class MxAccessHandleRegistry
{
    private readonly Dictionary<int, RegisteredServerHandle> serverHandles = new();
    private readonly Dictionary<long, RegisteredItemHandle> itemHandles = new();
    private readonly Dictionary<AdviceHandleKey, RegisteredAdviceHandle> adviceHandles = new();

    // Secondary indexes. Every register/remove method below maintains these so
    // lookups by tag and removals stay proportional to what is actually removed
    // instead of scanning a whole primary table.
    private readonly Dictionary<ItemDefinitionKey, List<int>> itemHandlesByDefinition = new();
    private readonly Dictionary<int, HashSet<long>> itemKeysByServer = new();
    private readonly Dictionary<long, List<AdviceHandleKey>> adviceKeysByItem = new();
    private readonly Dictionary<int, HashSet<long>> advisedItemKeysByServer = new();

    // Memoized materializations of the three sorted views. Each is dropped by
    // the mutation that invalidates it, so repeated reads between mutations
    // hand back the same array instead of re-sorting and re-copying the table.
    private RegisteredServerHandle[]? serverHandlesView;
    private RegisteredItemHandle[]? itemHandlesView;
    private RegisteredAdviceHandle[]? adviceHandlesView;

    /// <summary>Gets a read-only list of registered server handles ordered by handle value.</summary>
    public IReadOnlyList<RegisteredServerHandle> ServerHandles => serverHandlesView ??= serverHandles
        .Values
        .OrderBy(handle => handle.ServerHandle)
        .ToArray();

    /// <summary>Gets a read-only list of registered item handles ordered by server handle then item handle.</summary>
    public IReadOnlyList<RegisteredItemHandle> ItemHandles => itemHandlesView ??= itemHandles
        .Values
        .OrderBy(handle => handle.ServerHandle)
        .ThenBy(handle => handle.ItemHandle)
        .ToArray();

    /// <summary>Gets a read-only list of registered advice handles ordered by server handle, item handle, and advice kind.</summary>
    public IReadOnlyList<RegisteredAdviceHandle> AdviceHandles => adviceHandlesView ??= adviceHandles
        .Values
        .OrderBy(handle => handle.ServerHandle)
        .ThenBy(handle => handle.ItemHandle)
        .ThenBy(handle => handle.AdviceKind)
        .ToArray();

    /// <summary>Registers a server handle with the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="clientName">Display name of the client that owns the server handle.</param>
    public void RegisterServerHandle(
        int serverHandle,
        string clientName)
    {
        serverHandles[serverHandle] = new RegisteredServerHandle(serverHandle, clientName);
        serverHandlesView = null;
    }

    /// <summary>Unregisters a server handle and all associated item and advice handles from the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    public void UnregisterServerHandle(int serverHandle)
    {
        if (serverHandles.Remove(serverHandle))
        {
            serverHandlesView = null;
        }

        // Per-server indexes: the removal cost is proportional to the handles
        // this server actually owns, not to the size of the whole table.
        if (itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
        {
            itemKeysByServer.Remove(serverHandle);

            foreach (long itemKey in ownedItemKeys)
            {
                RemoveItemEntry(itemKey);
            }
        }

        if (advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
        {
            advisedItemKeysByServer.Remove(serverHandle);

            foreach (long itemKey in advisedItemKeys)
            {
                RemoveAdviceEntries(itemKey);
            }
        }
    }

    /// <summary>Checks if the registry contains the specified server handle.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <returns><see langword="true"/> if the server handle is registered; otherwise, <see langword="false"/>.</returns>
    public bool ContainsServerHandle(int serverHandle)
    {
        return serverHandles.ContainsKey(serverHandle);
    }

    /// <summary>Registers an item handle with the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="itemDefinition">Item definition name from MXAccess.</param>
    /// <param name="itemContext">Item context from MXAccess, or empty string if none.</param>
    /// <param name="hasItemContext">True if the item has a context; false otherwise.</param>
    public void RegisterItemHandle(
        int serverHandle,
        int itemHandle,
        string itemDefinition,
        string itemContext,
        bool hasItemContext)
    {
        long itemKey = CreateItemKey(serverHandle, itemHandle);

        if (itemHandles.TryGetValue(itemKey, out RegisteredItemHandle? existing))
        {
            // Re-registering the same handle can carry a different item
            // definition; drop the stale reverse-index entry first.
            RemoveDefinitionIndexEntry(existing);
        }

        itemHandles[itemKey] = new RegisteredItemHandle(
            serverHandle,
            itemHandle,
            itemDefinition,
            itemContext,
            hasItemContext);

        AddDefinitionIndexEntry(serverHandle, itemHandle, itemDefinition);

        if (!itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
        {
            ownedItemKeys = new HashSet<long>();
            itemKeysByServer[serverHandle] = ownedItemKeys;
        }

        ownedItemKeys.Add(itemKey);
        itemHandlesView = null;
    }

    /// <summary>Removes an item handle and all associated advice handles from the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    public void RemoveItemHandle(
        int serverHandle,
        int itemHandle)
    {
        long itemKey = CreateItemKey(serverHandle, itemHandle);

        if (RemoveItemEntry(itemKey)
            && itemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? ownedItemKeys))
        {
            ownedItemKeys.Remove(itemKey);

            if (ownedItemKeys.Count == 0)
            {
                itemKeysByServer.Remove(serverHandle);
            }
        }

        RemoveAdviceHandles(serverHandle, itemHandle);
    }

    /// <summary>Checks if the registry contains the specified item handle.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <returns><see langword="true"/> if the item handle is registered; otherwise, <see langword="false"/>.</returns>
    public bool ContainsItemHandle(
        int serverHandle,
        int itemHandle)
    {
        return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle));
    }

    /// <summary>
    ///     Gets the item handles registered under the specified server handle for
    ///     the specified item definition, ordered by item handle.
    /// </summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemDefinition">Item definition name, compared ordinally.</param>
    /// <returns>
    ///     The matching item handles in ascending order — the same order a scan of
    ///     <see cref="ItemHandles"/> would visit them — or an empty list when the
    ///     definition is not registered for that server. MXAccess allows the same
    ///     tag to be added more than once under one server handle, so this is a
    ///     list rather than a single handle. The returned list is a live view of
    ///     the index and is only valid until the next registry mutation.
    /// </returns>
    public IReadOnlyList<int> GetItemHandlesForDefinition(
        int serverHandle,
        string itemDefinition)
    {
        if (itemHandlesByDefinition.TryGetValue(
            new ItemDefinitionKey(serverHandle, itemDefinition),
            out List<int>? handles))
        {
            return handles;
        }

        return Array.Empty<int>();
    }

    /// <summary>Registers an advice handle with the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="adviceKind">Type of advice to register.</param>
    public void RegisterAdviceHandle(
        int serverHandle,
        int itemHandle,
        MxAccessAdviceKind adviceKind)
    {
        AdviceHandleKey key = new(serverHandle, itemHandle, adviceKind);

        if (!adviceHandles.ContainsKey(key))
        {
            // Only index a key the primary table does not already hold, so the
            // per-item list never accumulates duplicates.
            long itemKey = CreateItemKey(serverHandle, itemHandle);

            if (!adviceKeysByItem.TryGetValue(itemKey, out List<AdviceHandleKey>? itemAdviceKeys))
            {
                itemAdviceKeys = new List<AdviceHandleKey>(2);
                adviceKeysByItem[itemKey] = itemAdviceKeys;
            }

            itemAdviceKeys.Add(key);

            if (!advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
            {
                advisedItemKeys = new HashSet<long>();
                advisedItemKeysByServer[serverHandle] = advisedItemKeys;
            }

            advisedItemKeys.Add(itemKey);
        }

        adviceHandles[key] = new RegisteredAdviceHandle(
            serverHandle,
            itemHandle,
            adviceKind);

        adviceHandlesView = null;
    }

    /// <summary>Removes all advice handles for the specified server and item handles from the registry.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    public void RemoveAdviceHandles(
        int serverHandle,
        int itemHandle)
    {
        long itemKey = CreateItemKey(serverHandle, itemHandle);
        RemoveAdviceEntries(itemKey);

        if (advisedItemKeysByServer.TryGetValue(serverHandle, out HashSet<long>? advisedItemKeys))
        {
            advisedItemKeys.Remove(itemKey);

            if (advisedItemKeys.Count == 0)
            {
                advisedItemKeysByServer.Remove(serverHandle);
            }
        }
    }

    /// <summary>Checks if the registry contains the specified advice handle.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    /// <param name="itemHandle">Handle returned by the worker.</param>
    /// <param name="adviceKind">Type of advice to check.</param>
    /// <returns><see langword="true"/> if the advice handle is registered; otherwise, <see langword="false"/>.</returns>
    public bool ContainsAdviceHandle(
        int serverHandle,
        int itemHandle,
        MxAccessAdviceKind adviceKind)
    {
        return adviceHandles.ContainsKey(new AdviceHandleKey(serverHandle, itemHandle, adviceKind));
    }

    private static long CreateItemKey(
        int serverHandle,
        int itemHandle)
    {
        return ((long)serverHandle << 32) | (uint)itemHandle;
    }

    /// <summary>Removes one item from the primary table and the definition index.</summary>
    /// <param name="itemKey">Packed server/item key produced by <see cref="CreateItemKey"/>.</param>
    /// <returns><see langword="true"/> if an item was removed; otherwise, <see langword="false"/>.</returns>
    private bool RemoveItemEntry(long itemKey)
    {
        if (!itemHandles.TryGetValue(itemKey, out RegisteredItemHandle? registered))
        {
            return false;
        }

        itemHandles.Remove(itemKey);
        RemoveDefinitionIndexEntry(registered);
        itemHandlesView = null;

        return true;
    }

    /// <summary>Removes every advice handle recorded for one item from the primary table and the per-item index.</summary>
    /// <param name="itemKey">Packed server/item key produced by <see cref="CreateItemKey"/>.</param>
    private void RemoveAdviceEntries(long itemKey)
    {
        if (!adviceKeysByItem.TryGetValue(itemKey, out List<AdviceHandleKey>? itemAdviceKeys))
        {
            return;
        }

        adviceKeysByItem.Remove(itemKey);

        foreach (AdviceHandleKey key in itemAdviceKeys)
        {
            adviceHandles.Remove(key);
        }

        adviceHandlesView = null;
    }

    private void AddDefinitionIndexEntry(
        int serverHandle,
        int itemHandle,
        string itemDefinition)
    {
        ItemDefinitionKey definitionKey = new(serverHandle, itemDefinition);

        if (!itemHandlesByDefinition.TryGetValue(definitionKey, out List<int>? handles))
        {
            handles = new List<int>(1);
            itemHandlesByDefinition[definitionKey] = handles;
        }

        // Kept sorted so callers see the same order a scan of ItemHandles would.
        int index = handles.BinarySearch(itemHandle);

        if (index < 0)
        {
            handles.Insert(~index, itemHandle);
        }
    }

    private void RemoveDefinitionIndexEntry(RegisteredItemHandle registered)
    {
        ItemDefinitionKey definitionKey = new(registered.ServerHandle, registered.ItemDefinition);

        if (!itemHandlesByDefinition.TryGetValue(definitionKey, out List<int>? handles))
        {
            return;
        }

        handles.Remove(registered.ItemHandle);

        if (handles.Count == 0)
        {
            itemHandlesByDefinition.Remove(definitionKey);
        }
    }

    private readonly struct AdviceHandleKey : IEquatable<AdviceHandleKey>
    {
        private readonly int serverHandle;
        private readonly int itemHandle;
        private readonly MxAccessAdviceKind adviceKind;

        /// <summary>Initializes a new instance of the <see cref="AdviceHandleKey"/> struct.</summary>
        /// <param name="serverHandle">Handle returned by the worker.</param>
        /// <param name="itemHandle">Handle returned by the worker.</param>
        /// <param name="adviceKind">Type of advice.</param>
        public AdviceHandleKey(
            int serverHandle,
            int itemHandle,
            MxAccessAdviceKind adviceKind)
        {
            this.serverHandle = serverHandle;
            this.itemHandle = itemHandle;
            this.adviceKind = adviceKind;
        }

        /// <inheritdoc />
        public bool Equals(AdviceHandleKey other)
        {
            return serverHandle == other.serverHandle
                && itemHandle == other.itemHandle
                && adviceKind == other.adviceKind;
        }

        /// <inheritdoc />
        public override bool Equals(object? obj)
        {
            return obj is AdviceHandleKey other && Equals(other);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = serverHandle;
                hashCode = (hashCode * 397) ^ itemHandle;
                hashCode = (hashCode * 397) ^ (int)adviceKind;

                return hashCode;
            }
        }
    }

    /// <summary>
    ///     Reverse-index key: a server handle plus an item definition, compared
    ///     ordinally to match the tag comparison the read path performs.
    /// </summary>
    private readonly struct ItemDefinitionKey : IEquatable<ItemDefinitionKey>
    {
        private readonly int serverHandle;
        private readonly string itemDefinition;

        /// <summary>Initializes a new instance of the <see cref="ItemDefinitionKey"/> struct.</summary>
        /// <param name="serverHandle">Handle returned by the worker.</param>
        /// <param name="itemDefinition">Item definition name from MXAccess.</param>
        public ItemDefinitionKey(
            int serverHandle,
            string itemDefinition)
        {
            this.serverHandle = serverHandle;
            this.itemDefinition = itemDefinition;
        }

        /// <inheritdoc />
        public bool Equals(ItemDefinitionKey other)
        {
            return serverHandle == other.serverHandle
                && string.Equals(itemDefinition, other.itemDefinition, StringComparison.Ordinal);
        }

        /// <inheritdoc />
        public override bool Equals(object? obj)
        {
            return obj is ItemDefinitionKey other && Equals(other);
        }

        /// <inheritdoc />
        public override int GetHashCode()
        {
            unchecked
            {
                int hashCode = serverHandle;
                hashCode = (hashCode * 397) ^ (itemDefinition ?? string.Empty).GetHashCode();

                return hashCode;
            }
        }
    }
}
