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

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

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();

    /// <summary>Gets a read-only list of registered server handles ordered by handle value.</summary>
    public IReadOnlyList<RegisteredServerHandle> ServerHandles => 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 => 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 => 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);
    }

    /// <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)
    {
        serverHandles.Remove(serverHandle);

        foreach (long key in itemHandles
            .Where(pair => pair.Value.ServerHandle == serverHandle)
            .Select(pair => pair.Key)
            .ToArray())
        {
            itemHandles.Remove(key);
        }

        foreach (AdviceHandleKey key in adviceHandles
            .Where(pair => pair.Value.ServerHandle == serverHandle)
            .Select(pair => pair.Key)
            .ToArray())
        {
            adviceHandles.Remove(key);
        }
    }

    /// <summary>Checks if the registry contains the specified server handle.</summary>
    /// <param name="serverHandle">Handle returned by the worker.</param>
    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)
    {
        itemHandles[CreateItemKey(serverHandle, itemHandle)] = new RegisteredItemHandle(
            serverHandle,
            itemHandle,
            itemDefinition,
            itemContext,
            hasItemContext);
    }

    /// <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)
    {
        itemHandles.Remove(CreateItemKey(serverHandle, itemHandle));
        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>
    public bool ContainsItemHandle(
        int serverHandle,
        int itemHandle)
    {
        return itemHandles.ContainsKey(CreateItemKey(serverHandle, itemHandle));
    }

    /// <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);
        adviceHandles[key] = new RegisteredAdviceHandle(
            serverHandle,
            itemHandle,
            adviceKind);
    }

    /// <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)
    {
        foreach (AdviceHandleKey key in adviceHandles
            .Where(pair => pair.Value.ServerHandle == serverHandle && pair.Value.ItemHandle == itemHandle)
            .Select(pair => pair.Key)
            .ToArray())
        {
            adviceHandles.Remove(key);
        }
    }

    /// <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>
    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;
    }

    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;
            }
        }
    }
}
