using ZB.MOM.WW.CBDD.Bson.IO;

namespace ZB.MOM.WW.CBDD.Bson.Schema;

public class BsonField
{
    /// <summary>
    ///     Gets the field name.
    /// </summary>
    public required string Name { get; init; }

    /// <summary>
    ///     Gets the field BSON type.
    /// </summary>
    public BsonType Type { get; init; }

    /// <summary>
    ///     Gets a value indicating whether the field is nullable.
    /// </summary>
    public bool IsNullable { get; init; }

    /// <summary>
    ///     Gets the nested schema when this field is a document.
    /// </summary>
    public BsonSchema? NestedSchema { get; init; }

    /// <summary>
    ///     Gets the array item type when this field is an array.
    /// </summary>
    public BsonType? ArrayItemType { get; init; }

    /// <summary>
    ///     Writes this field definition to BSON.
    /// </summary>
    /// <param name="writer">The BSON writer.</param>
    public void ToBson(ref BsonSpanWriter writer)
    {
        int size = writer.BeginDocument();
        writer.WriteString("n", Name);
        writer.WriteInt32("t", (int)Type);
        writer.WriteBoolean("b", IsNullable);

        if (NestedSchema != null)
        {
            writer.WriteElementHeader(BsonType.Document, "s");
            NestedSchema.ToBson(ref writer);
        }

        if (ArrayItemType != null) writer.WriteInt32("a", (int)ArrayItemType.Value);

        writer.EndDocument(size);
    }

    /// <summary>
    ///     Reads a field definition from BSON.
    /// </summary>
    /// <param name="reader">The BSON reader.</param>
    /// <returns>The deserialized field.</returns>
    public static BsonField FromBson(ref BsonSpanReader reader)
    {
        reader.ReadInt32(); // Read doc size

        var name = "";
        var type = BsonType.Null;
        var isNullable = false;
        BsonSchema? nestedSchema = null;
        BsonType? arrayItemType = null;

        while (reader.Remaining > 1)
        {
            var btype = reader.ReadBsonType();
            if (btype == BsonType.EndOfDocument) break;

            string key = reader.ReadElementHeader();
            switch (key)
            {
                case "n": name = reader.ReadString(); break;
                case "t": type = (BsonType)reader.ReadInt32(); break;
                case "b": isNullable = reader.ReadBoolean(); break;
                case "s": nestedSchema = BsonSchema.FromBson(ref reader); break;
                case "a": arrayItemType = (BsonType)reader.ReadInt32(); break;
                default: reader.SkipValue(btype); break;
            }
        }

        return new BsonField
        {
            Name = name,
            Type = type,
            IsNullable = isNullable,
            NestedSchema = nestedSchema,
            ArrayItemType = arrayItemType
        };
    }

    /// <summary>
    ///     Computes a hash representing the field definition.
    /// </summary>
    /// <returns>The computed hash value.</returns>
    public long GetHash()
    {
        var hash = new HashCode();
        hash.Add(Name);
        hash.Add((int)Type);
        hash.Add(IsNullable);
        hash.Add(ArrayItemType);
        if (NestedSchema != null) hash.Add(NestedSchema.GetHash());
        return hash.ToHashCode();
    }

    /// <summary>
    ///     Determines whether this field is equal to another field.
    /// </summary>
    /// <param name="other">The other field.</param>
    /// <returns><see langword="true" /> if the fields are equal; otherwise, <see langword="false" />.</returns>
    public bool Equals(BsonField? other)
    {
        if (other == null) return false;
        return GetHash() == other.GetHash();
    }

    /// <inheritdoc />
    public override bool Equals(object? obj)
    {
        return Equals(obj as BsonField);
    }

    /// <inheritdoc />
    public override int GetHashCode()
    {
        return (int)GetHash();
    }
}