using ZB.MOM.WW.CBDD.Bson;
using System;
using System.Linq;

namespace ZB.MOM.WW.CBDD.Core.Indexing;

/// <summary>
/// Represents a key in an index.
/// Implemented as struct for efficient index operations.
/// Note: Contains byte array so cannot be readonly struct.
/// </summary>
public struct IndexKey : IEquatable<IndexKey>, IComparable<IndexKey>
{
    private readonly byte[] _data;
    private readonly int _hashCode;

    /// <summary>
    /// Gets the minimum possible index key.
    /// </summary>
    public static IndexKey MinKey => new IndexKey(Array.Empty<byte>());

    /// <summary>
    /// Gets the maximum possible index key.
    /// </summary>
    public static IndexKey MaxKey => new IndexKey(Enumerable.Repeat((byte)0xFF, 32).ToArray());

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from raw key bytes.
    /// </summary>
    /// <param name="data">The key bytes.</param>
    public IndexKey(ReadOnlySpan<byte> data)
    {
        _data = data.ToArray();
        _hashCode = ComputeHashCode(data);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from an object identifier.
    /// </summary>
    /// <param name="objectId">The object identifier value.</param>
    public IndexKey(ObjectId objectId)
    {
        _data = new byte[12];
        objectId.WriteTo(_data);
        _hashCode = ComputeHashCode(_data);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from a 32-bit integer.
    /// </summary>
    /// <param name="value">The integer value.</param>
    public IndexKey(int value)
    {
        _data = BitConverter.GetBytes(value);
        _hashCode = ComputeHashCode(_data);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from a 64-bit integer.
    /// </summary>
    /// <param name="value">The integer value.</param>
    public IndexKey(long value)
    {
        _data = BitConverter.GetBytes(value);
        _hashCode = ComputeHashCode(_data);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from a string.
    /// </summary>
    /// <param name="value">The string value.</param>
    public IndexKey(string value)
    {
        _data = System.Text.Encoding.UTF8.GetBytes(value);
        _hashCode = ComputeHashCode(_data);
    }

    /// <summary>
    /// Initializes a new instance of the <see cref="IndexKey"/> struct from a GUID.
    /// </summary>
    /// <param name="value">The GUID value.</param>
    public IndexKey(Guid value)
    {
        _data = value.ToByteArray();
        _hashCode = ComputeHashCode(_data);
    }

    /// <summary>
    /// Gets the raw byte data for this key.
    /// </summary>
    public readonly ReadOnlySpan<byte> Data => _data;

    /// <summary>
    /// Compares this key to another key.
    /// </summary>
    /// <param name="other">The key to compare with.</param>
    /// <returns>
    /// A value less than zero if this key is less than <paramref name="other"/>, zero if equal, or greater than zero if greater.
    /// </returns>
    public readonly int CompareTo(IndexKey other)
    {
        if (_data == null) return other._data == null ? 0 : -1;
        if (other._data == null) return 1;

        var minLength = Math.Min(_data.Length, other._data.Length);

        for (int i = 0; i < minLength; i++)
        {
            var cmp = _data[i].CompareTo(other._data[i]);
            if (cmp != 0)
                return cmp;
        }

        return _data.Length.CompareTo(other._data.Length);
    }

    /// <summary>
    /// Determines whether this key equals another key.
    /// </summary>
    /// <param name="other">The key to compare with.</param>
    /// <returns><see langword="true"/> if the keys are equal; otherwise, <see langword="false"/>.</returns>
    public readonly bool Equals(IndexKey other)
    {
        if (_hashCode != other._hashCode)
            return false;

        if (_data == null) return other._data == null;
        if (other._data == null) return false;

        return _data.AsSpan().SequenceEqual(other._data);
    }

    /// <inheritdoc />
    public override readonly bool Equals(object? obj) => obj is IndexKey other && Equals(other);

    /// <inheritdoc />
    public override readonly int GetHashCode() => _hashCode;

    public static bool operator ==(IndexKey left, IndexKey right) => left.Equals(right);
    public static bool operator !=(IndexKey left, IndexKey right) => !left.Equals(right);
    public static bool operator <(IndexKey left, IndexKey right) => left.CompareTo(right) < 0;
    public static bool operator >(IndexKey left, IndexKey right) => left.CompareTo(right) > 0;
    public static bool operator <=(IndexKey left, IndexKey right) => left.CompareTo(right) <= 0;
    public static bool operator >=(IndexKey left, IndexKey right) => left.CompareTo(right) >= 0;

    private static int ComputeHashCode(ReadOnlySpan<byte> data)
    {
        var hash = new HashCode();
        hash.AddBytes(data);
        return hash.ToHashCode();
    }

    /// <summary>
    /// Creates an <see cref="IndexKey"/> from a supported CLR value.
    /// </summary>
    /// <typeparam name="T">The CLR type of the value.</typeparam>
    /// <param name="value">The value to convert.</param>
    /// <returns>The created index key.</returns>
    public static IndexKey Create<T>(T value)
    {
        if (value == null) return default;

        if (typeof(T) == typeof(ObjectId)) return new IndexKey((ObjectId)(object)value);
        if (typeof(T) == typeof(int)) return new IndexKey((int)(object)value);
        if (typeof(T) == typeof(long)) return new IndexKey((long)(object)value);
        if (typeof(T) == typeof(string)) return new IndexKey((string)(object)value);
        if (typeof(T) == typeof(Guid)) return new IndexKey((Guid)(object)value);
        if (typeof(T) == typeof(byte[])) return new IndexKey((byte[])(object)value);

        throw new NotSupportedException($"Type {typeof(T).Name} is not supported as an IndexKey. Provide a custom mapping.");
    }

    /// <summary>
    /// Converts this key to a CLR value of type <typeparamref name="T"/>.
    /// </summary>
    /// <typeparam name="T">The CLR type to read from this key.</typeparam>
    /// <returns>The converted value.</returns>
    public readonly T As<T>()
    {
        if (_data == null) return default!;

        if (typeof(T) == typeof(ObjectId)) return (T)(object)new ObjectId(_data);
        if (typeof(T) == typeof(int)) return (T)(object)BitConverter.ToInt32(_data);
        if (typeof(T) == typeof(long)) return (T)(object)BitConverter.ToInt64(_data);
        if (typeof(T) == typeof(string)) return (T)(object)System.Text.Encoding.UTF8.GetString(_data);
        if (typeof(T) == typeof(Guid)) return (T)(object)new Guid(_data);
        if (typeof(T) == typeof(byte[])) return (T)(object)_data;

        throw new NotSupportedException($"Type {typeof(T).Name} cannot be extracted from IndexKey. Provide a custom mapping.");
    }
}
