// Licensed to the Apache Software Foundation (ASF) under one or more
// contributor license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright ownership.
// The ASF licenses this file to You under the Apache License, Version 2.0
// (the "License"); you may not use this file except in compliance with
// the License.  You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Apache.Arrow.Memory;

namespace Apache.Arrow
{
    public partial struct ArrowBuffer
    {
        /// <summary>
        /// The <see cref="BitmapBuilder"/> class is a complement to <see cref="ArrowBuffer.Builder{T}"/>
        /// and is designed for boolean fields, which are efficiently bit-packed into byte-aligned memory.
        /// </summary>
        public class BitmapBuilder
        {
            private const int DefaultBitCapacity = 64;

            /// <summary>
            /// Gets the number of bits that can be contained in the memory allocated by the current instance.
            /// </summary>
            public int Capacity { get; private set; }

            /// <summary>
            /// Gets the number of bits currently appended.
            /// </summary>
            public int Length { get; private set; }

            /// <summary>
            /// Gets the raw byte memory underpinning the builder.
            /// </summary>
            public Memory<byte> Memory { get; private set; }

            /// <summary>
            /// Gets the span of (bit-packed byte) memory underpinning the builder.
            /// </summary>
            public Span<byte> Span => Memory.Span;

            /// <summary>
            /// Gets the number of set bits (i.e. set to 1).
            /// </summary>
            public int SetBitCount { get; private set; }

            /// <summary>
            /// Gets the number of unset bits (i.e. set to 0).
            /// </summary>
            public int UnsetBitCount => Length - SetBitCount;

            /// <summary>
            /// Creates an instance of the <see cref="BitmapBuilder"/> class.
            /// </summary>
            /// <param name="capacity">Number of bits of initial capacity to reserve.</param>
            public BitmapBuilder(int capacity = DefaultBitCapacity)
            {
                Memory = new byte[BitUtility.ByteCount(capacity)];
                Capacity = capacity;
            }

            /// <summary>
            /// Append a single bit.
            /// </summary>
            /// <param name="value">Bit to append.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Append(bool value)
            {
                if (Length % 8 == 0)
                {
                    // Append a new byte to the buffer when needed.
                    EnsureAdditionalCapacity(1);
                }

                BitUtility.SetBit(Span, Length, value);
                Length++;
                SetBitCount += value ? 1 : 0;
                return this;
            }

            /// <summary>
            /// Append multiple bits.
            /// </summary>
            /// <param name="values">Bits to append.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder AppendRange(IEnumerable<bool> values)
            {
                if (values != null)
                {
                    foreach (var v in values)
                    {
                        Append(v);
                    }
                }

                return this;
            }

            /// <summary>
            /// Toggle the bit at a particular index.
            /// </summary>
            /// <param name="index">Index of bit to toggle.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Toggle(int index)
            {
                CheckIndex(index);
                bool priorValue = BitUtility.GetBit(Span, index);
                SetBitCount += priorValue ? -1 : 1;
                BitUtility.ToggleBit(Span, index);
                return this;
            }

            /// <summary>
            /// Set the bit at a particular index to 1.
            /// </summary>
            /// <param name="index">Index of bit to set.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Set(int index)
            {
                CheckIndex(index);
                bool priorValue = BitUtility.GetBit(Span, index);
                SetBitCount += priorValue ? 0 : 1;
                BitUtility.SetBit(Span, index);
                return this;
            }

            /// <summary>
            /// Set the bit at a particular index to a given value.
            /// </summary>
            /// <param name="index">Index of bit to set/unset.</param>
            /// <param name="value">Value of bit.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Set(int index, bool value)
            {
                CheckIndex(index);
                bool priorValue = BitUtility.GetBit(Span, index);
                SetBitCount -= priorValue ? 1 : 0;
                SetBitCount += value ? 1 : 0;
                BitUtility.SetBit(Span, index, value);
                return this;
            }

            /// <summary>
            /// Swap the bits at two given indices.
            /// </summary>
            /// <param name="i">First index.</param>
            /// <param name="j">Second index.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Swap(int i, int j)
            {
                CheckIndex(i);
                CheckIndex(j);
                bool bi = BitUtility.GetBit(Span, i);
                bool bj = BitUtility.GetBit(Span, j);
                BitUtility.SetBit(Span, i, bj);
                BitUtility.SetBit(Span, j, bi);
                return this;
            }

            /// <summary>
            /// Reserve a given number of bits' additional capacity.
            /// </summary>
            /// <param name="additionalCapacity">Number of bits of required additional capacity.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Reserve(int additionalCapacity)
            {
                if (additionalCapacity < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(additionalCapacity));
                }

                EnsureAdditionalCapacity(additionalCapacity);
                return this;
            }

            /// <summary>
            /// Resize the buffer to a given size.
            /// </summary>
            /// <remarks>
            /// Note that if the required capacity is larger than the current length of the populated buffer so far,
            /// the buffer's contents in the new, expanded region are undefined.
            /// </remarks>
            /// <remarks>
            /// Note that if the required capacity is smaller than the current length of the populated buffer so far,
            /// the buffer will be truncated and items at the end of the buffer will be lost.
            /// </remarks>
            /// <param name="capacity">Number of bits of required capacity.</param>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Resize(int capacity)
            {
                if (capacity < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(capacity), "Capacity must be non-negative");
                }

                EnsureCapacity(capacity);
                Length = capacity;

                SetBitCount = BitUtility.CountBits(Span, 0, Length);

                return this;
            }

            /// <summary>
            /// Clear all contents appended so far.
            /// </summary>
            /// <returns>Returns the builder (for fluent-style composition).</returns>
            public BitmapBuilder Clear()
            {
                Span.Fill(default);
                Length = 0;
                SetBitCount = 0;
                return this;
            }

            /// <summary>
            /// Build an Arrow buffer from the appended contents so far.
            /// </summary>
            /// <param name="allocator">Optional memory allocator.</param>
            /// <returns>Returns an <see cref="ArrowBuffer"/> object.</returns>
            public ArrowBuffer Build(MemoryAllocator allocator = default)
            {
                int bufferLength = checked((int)BitUtility.RoundUpToMultipleOf64(Memory.Length));
                var memoryAllocator = allocator ?? MemoryAllocator.Default.Value;
                var memoryOwner = memoryAllocator.Allocate(bufferLength);
                Memory.Slice(0, Memory.Length).CopyTo(memoryOwner.Memory);
                return new ArrowBuffer(memoryOwner);
            }

            private void CheckIndex(int index)
            {
                if (index < 0 || index >= Length)
                {
                    throw new ArgumentOutOfRangeException(nameof(index));
                }
            }

            private void EnsureAdditionalCapacity(int additionalCapacity)
            {
                EnsureCapacity(checked(Length + additionalCapacity));
            }

            private void EnsureCapacity(int requiredCapacity)
            {
                if (requiredCapacity > Capacity)
                {
                    // TODO: specifiable growth strategy
                    // Double the length of the in-memory array, or use the byte count of the capacity, whichever is
                    // greater.
                    int byteCount = Math.Max(BitUtility.ByteCount(requiredCapacity), Memory.Length * 2);
                    Reallocate(byteCount);
                    Capacity = byteCount * 8;
                }
            }

            private void Reallocate(int numBytes)
            {
                if (numBytes != 0)
                {
                    Debug.Assert(numBytes > Memory.Length);
                    var memory = new Memory<byte>(new byte[numBytes]);
                    Memory.CopyTo(memory);

                    Memory = memory;
                }
            }
        }
    }
}
