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

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

public class BsonSchema
{
    /// <summary>
    ///     Gets or sets the schema title.
    /// </summary>
    public string? Title { get; set; }

    /// <summary>
    ///     Gets or sets the schema version.
    /// </summary>
    public int? Version { get; set; }

    /// <summary>
    ///     Gets the schema fields.
    /// </summary>
    public List<BsonField> Fields { get; } = new();

    /// <summary>
    ///     Serializes this schema instance to BSON.
    /// </summary>
    /// <param name="writer">The BSON writer to write into.</param>
    public void ToBson(ref BsonSpanWriter writer)
    {
        int size = writer.BeginDocument();
        if (Title != null) writer.WriteString("t", Title);
        if (Version != null) writer.WriteInt32("_v", Version.Value);

        int fieldsSize = writer.BeginArray("f");
        for (var i = 0; i < Fields.Count; i++)
        {
            writer.WriteElementHeader(BsonType.Document, i.ToString());
            Fields[i].ToBson(ref writer);
        }

        writer.EndArray(fieldsSize);

        writer.EndDocument(size);
    }

    /// <summary>
    ///     Deserializes a schema instance from BSON.
    /// </summary>
    /// <param name="reader">The BSON reader to read from.</param>
    /// <returns>The deserialized schema.</returns>
    public static BsonSchema FromBson(ref BsonSpanReader reader)
    {
        reader.ReadInt32(); // Read doc size

        var schema = new BsonSchema();

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

            string key = reader.ReadElementHeader();
            switch (key)
            {
                case "t": schema.Title = reader.ReadString(); break;
                case "_v": schema.Version = reader.ReadInt32(); break;
                case "f":
                    reader.ReadInt32(); // array size
                    while (reader.Remaining > 1)
                    {
                        var itemType = reader.ReadBsonType();
                        if (itemType == BsonType.EndOfDocument) break;
                        reader.ReadElementHeader(); // index
                        schema.Fields.Add(BsonField.FromBson(ref reader));
                    }

                    break;
                default: reader.SkipValue(btype); break;
            }
        }

        return schema;
    }

    /// <summary>
    ///     Computes a hash value for this schema based on its contents.
    /// </summary>
    /// <returns>The computed hash value.</returns>
    public long GetHash()
    {
        var hash = new HashCode();
        hash.Add(Title);
        foreach (var field in Fields) hash.Add(field.GetHash());
        return hash.ToHashCode();
    }

    /// <summary>
    ///     Determines whether this schema is equal to another schema.
    /// </summary>
    /// <param name="other">The schema to compare with.</param>
    /// <returns><see langword="true" /> when schemas are equal; otherwise, <see langword="false" />.</returns>
    public bool Equals(BsonSchema? other)
    {
        if (other == null) return false;
        return GetHash() == other.GetHash();
    }

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

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

    /// <summary>
    ///     Enumerates all field keys in this schema, including nested schema keys.
    /// </summary>
    /// <returns>An enumerable of field keys.</returns>
    public IEnumerable<string> GetAllKeys()
    {
        foreach (var field in Fields)
        {
            yield return field.Name;
            if (field.NestedSchema != null)
                foreach (string nestedKey in field.NestedSchema.GetAllKeys())
                    yield return nestedKey;
        }
    }
}