// MIT License - Copyright (c) 2025 wallstop // Full license text: https://github.com/wallstop/unity-helpers/blob/main/LICENSE namespace WallstopStudios.UnityHelpers.Core.DataStructure { using System; using System.Collections; using System.Collections.Generic; using System.Runtime.CompilerServices; using ProtoBuf; using WallstopStudios.UnityHelpers.Core.Helper; /// /// Immutable value-type snapshot of a that exposes read-only bit access without allocating. /// Ideal for safely sharing flag data across threads, event callbacks, or history buffers without accidental mutations. /// /// /// /// [Serializable] [ProtoContract(IgnoreListHandling = true)] public readonly struct ImmutableBitSet : IEquatable { private const int BitsPerLongShift = 6; // log2(64) private const int BitsPerLongMask = 63; // 64 - 1 [ProtoMember(1)] private readonly ulong[] _bits; [ProtoMember(2)] private readonly int _capacity; /// /// Gets the current capacity (number of bits stored). /// public int Capacity => _capacity; /// /// Gets the number of bits in this bit set. /// public int Count => _capacity; /// /// Gets the bit at the specified index. /// Returns false for indices beyond capacity. /// public bool this[int index] => TryGet(index, out bool value) && value; /// /// Constructs an immutable bit set with the specified capacity and bit data. /// internal ImmutableBitSet(ulong[] bits, int capacity) { _bits = bits ?? Array.Empty(); _capacity = capacity; } /// /// Gets a copy of the internal bits array for serialization purposes. /// internal ulong[] GetBitsArrayCopy() { if (_bits == null) { return Array.Empty(); } ulong[] copy = new ulong[_bits.Length]; Array.Copy(_bits, copy, _bits.Length); return copy; } /// /// Attempts to get the bit at the specified index. /// Returns false if the index is out of range. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public bool TryGet(int index, out bool value) { if (index < 0 || index >= _capacity || _bits == null) { value = false; return false; } int arrayIndex = index >> BitsPerLongShift; int bitIndex = index & BitsPerLongMask; value = (_bits[arrayIndex] & (1UL << bitIndex)) != 0; return true; } /// /// Counts the number of bits set to 1. /// public int CountSetBits() { if (_bits == null) { return 0; } int count = 0; foreach (ulong bit in _bits) { count += PopCount(bit); } return count; } /// /// Checks if any bits are set to 1. /// public bool Any() { if (_bits == null) { return false; } return Array.Exists(_bits, bit => bit != 0); } public List ToList() { List result = new(); foreach (bool bit in this) { result.Add(bit); } return result; } public List ToList(List result) { if (result == null) { throw new ArgumentNullException(nameof(result)); } result.Clear(); foreach (bool bit in this) { result.Add(bit); } return result; } public IEnumerable AsEnumerable() { foreach (bool value in this) { yield return value; } } public bool[] ToArray() { bool[] result = null; _ = ToArray(ref result); return result; } public int ToArray(ref bool[] result) { int count = Count; if (result == null || result.Length < count) { result = new bool[count]; } int index = 0; foreach (bool value in this) { result[index++] = value; } return count; } /// /// Checks if all bits are set to 0. /// public bool None() { return !Any(); } /// /// Checks if all bits (within capacity) are set to 1. /// public bool All() { if (_capacity <= 0 || _bits == null) { return false; } int fullSegments = _capacity >> BitsPerLongShift; for (int i = 0; i < fullSegments; i++) { if (_bits[i] != ulong.MaxValue) { return false; } } int remainingBits = _capacity & BitsPerLongMask; if (remainingBits == 0) { return true; } if (_bits.Length <= fullSegments) { return false; } ulong mask = (1UL << remainingBits) - 1; return (_bits[fullSegments] & mask) == mask; } /// /// Populates the provided list with indices of all set bits. /// Clears the list before adding. Returns the same list for chaining. /// public List GetSetBits(List results) { if (results == null) { throw new ArgumentNullException(nameof(results)); } results.Clear(); for (int i = 0; i < _capacity; i++) { if (TryGet(i, out bool value) && value) { results.Add(i); } } return results; } /// /// Enumerates indices of all set bits (where value is 1). /// Use this for efficient iteration over sparse bitsets. /// public IEnumerable EnumerateSetIndices() { for (int i = 0; i < _capacity; i++) { if (TryGet(i, out bool value) && value) { yield return i; } } } /// /// Converts this immutable bit set to a mutable BitSet. /// Creates a new BitSet with a copy of the bit data. /// public BitSet ToBitSet() { BitSet result = new(_capacity > 0 ? _capacity : 64); if (_bits == null || _capacity <= 0) { return result; } // Copy bits from this immutable set to the new mutable set for (int i = 0; i < _capacity; i++) { if (TryGet(i, out bool value) && value) { result.TrySet(i); } } return result; } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static int PopCount(ulong value) { // Brian Kernighan's algorithm int count = 0; while (value != 0) { value &= value - 1; count++; } return count; } /// /// Returns an enumerator that iterates through all bit values (true/false) in order. /// public BitEnumerator GetEnumerator() { return new BitEnumerator(this); } // IEnumerator IEnumerable.GetEnumerator() // { // return GetEnumerator(); // } // // IEnumerator IEnumerable.GetEnumerator() // { // return GetEnumerator(); // } /// /// Checks if this ImmutableBitSet is equal to another. /// public bool Equals(ImmutableBitSet other) { if (_capacity != other._capacity) { return false; } if (_bits == null && other._bits == null) { return true; } if (_bits == null || other._bits == null) { return false; } if (_bits.Length != other._bits.Length) { return false; } return _bits.AsSpan().SequenceEqual(other._bits); } public override bool Equals(object obj) { return obj is ImmutableBitSet other && Equals(other); } public override int GetHashCode() { int hash = Objects.SpanHashCode(_bits.AsSpan()); return Objects.HashCode(_capacity, hash); } public static bool operator ==(ImmutableBitSet left, ImmutableBitSet right) { return left.Equals(right); } public static bool operator !=(ImmutableBitSet left, ImmutableBitSet right) { return !(left == right); } public struct BitEnumerator : IEnumerator { private readonly ImmutableBitSet _bitSet; private int _index; private bool _current; internal BitEnumerator(ImmutableBitSet bitSet) { _bitSet = bitSet; _index = -1; _current = false; } public bool MoveNext() { if (++_index < _bitSet._capacity) { _bitSet.TryGet(_index, out _current); return true; } _current = false; return false; } public bool Current => _current; object IEnumerator.Current => Current; public void Reset() { _index = -1; _current = false; } public void Dispose() { } } } }