using System;
using UnityEngine;

namespace pdinklag
{
    /// <summary>
    /// Represents a selection from a set of items.
    /// </summary>
    [Serializable]
    public class ItemSelection
    {
        private static readonly int BITS_PER_INT = 32;
        private static readonly int BLOCK_SHIFT = 5; // LOG2(BITS_PER_INT)
        private static readonly int BLOCK_AND = BITS_PER_INT - 1;

        /// <summary>
        /// The selection stored as an encoded bit array.
        /// </summary>
        [HideInInspector]
        public int[] Data;

        /// <summary>
        /// The number of available items.
        /// </summary>
        [HideInInspector]
        public int Size;

        /// <summary>
        /// The number of selected items.
        /// </summary>
        [HideInInspector]
        public int NumSelected;

        /// <summary>
        /// Tests whether the selection is valid.
        /// </summary>
        public bool IsValid => Size > 0;

        /// <summary>
        /// Tests whether the selection is empty.
        /// </summary>
        public bool IsNone => NumSelected == 0;

        /// <summary>
        /// Tests whether all items are selected.
        /// </summary>
        public bool IsAll => NumSelected == Size;

        /// <summary>
        /// Sets the size of the selection, resetting it in the process.
        /// </summary>
        /// <param name="size">the size (number of available items)</param>
        /// <param name="allSelected">whether all items are selected by default</param>
        public void SetSize(int size, bool allSelected = false)
        {
            Size = size;
            NumSelected = allSelected ? Size : 0;

            Data = new int[Mathf.CeilToInt((float)Size / (float)BITS_PER_INT)];
            if (allSelected) SetAll(true);
        }

        /// <summary>
        /// Tests whether the i-th item is selected.
        /// 
        /// If the selection is not initialized, the default value will be returned.
        /// </summary>
        /// <param name="i">the item index</param>
        /// <param name="defaultValue">the default value</param>
        /// <returns></returns>
        public bool IsSelected(int i, bool defaultValue = false) => IsValid ? this[i] : defaultValue;

        /// <summary>
        /// Gets or sets whether the i-th item is selected.
        /// 
        /// This does NOT include any validity checks and fails if the selection is not properly initialized.
        /// </summary>
        public bool this[int i]
        {
            get => new BitArray32((uint)Data[i >> BLOCK_SHIFT])[i & BLOCK_AND];
            set
            {
                var block = i >> BLOCK_SHIFT;
                var bits = new BitArray32((uint)Data[block]);

                var local = i & BLOCK_AND;
                if (bits[local] != value)
                {
                    NumSelected = value ? (NumSelected + 1) : (NumSelected - 1);
                }

                bits[local] = value;
                Data[block] = (int)bits.Bits;
            }
        }

        /// <summary>
        /// Sets the selection status for all items.
        /// </summary>
        /// <param name="b">whether to select or unselect all items</param>
        public void SetAll(bool b)
        {
            var x = (b ? ~0 : 0);
            for (var j = 0; j < Data.Length; j++) Data[j] = x;
            NumSelected = b ? Size : 0;
        }

        /// <summary>
        /// Computes the union of this and another selection and stores it in this selection.
        /// </summary>
        /// <param name="other">the other selection</param>
        public void Union(ItemSelection other)
        {
            if (Size != other.Size) throw new ArgumentException("selections must have the same size");

            NumSelected = 0;
            for (var j = 0; j < Data.Length; j++)
            {
                Data[j] |= other.Data[j];
                NumSelected += ((uint)Data[j]).CountSetBits();
            }
        }

        public override string ToString()
        {
            if (Size == 0)
            {
                return "No selection";
            }
            else if (IsAll)
            {
                return string.Format("All {0}", Size);
            }
            else if (IsNone)
            {
                return string.Format("None of {0}", Size);
            }
            else
            {
                return string.Format("{0} of {1}", NumSelected, Size);
            }
        }
    }
}
