// 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 Apache.Arrow.Memory;
using Apache.Arrow.Types;
using System;
using System.Collections;
using System.Collections.Generic;

namespace Apache.Arrow
{
    public class BooleanArray: Array, IReadOnlyList<bool?>, ICollection<bool?>
    {
        public class Builder : IArrowArrayBuilder<bool, BooleanArray, Builder>
        {
            private ArrowBuffer.BitmapBuilder ValueBuffer { get; }
            private ArrowBuffer.BitmapBuilder ValidityBuffer { get; }

            public int Length => ValueBuffer.Length;
            public int Capacity => ValueBuffer.Capacity;
            public int NullCount => ValidityBuffer.UnsetBitCount;

            public Builder()
            {
                ValueBuffer = new ArrowBuffer.BitmapBuilder();
                ValidityBuffer = new ArrowBuffer.BitmapBuilder();
            }

            public Builder Append(bool value)
            {
                return NullableAppend(value);
            }

            public Builder NullableAppend(bool? value)
            {
                // Note that we rely on the fact that null values are false in the value buffer.
                ValueBuffer.Append(value ?? false);
                ValidityBuffer.Append(value.HasValue);
                return this;
            }

            public Builder Append(ReadOnlySpan<bool> span)
            {
                foreach (bool value in span)
                {
                    Append(value);
                }
                return this;
            }

            public Builder AppendRange(IEnumerable<bool> values)
            {
                foreach (bool value in values)
                {
                    Append(value);
                }
                return this;
            }

            public Builder AppendNull()
            {
                return NullableAppend(null);
            }

            public BooleanArray Build(MemoryAllocator allocator = default)
            {
                ArrowBuffer validityBuffer = NullCount > 0
                                        ? ValidityBuffer.Build(allocator)
                                        : ArrowBuffer.Empty;

                return new BooleanArray(
                    ValueBuffer.Build(allocator), validityBuffer,
                    Length, NullCount, 0);
            }

            public Builder Clear()
            {
                ValueBuffer.Clear();
                ValidityBuffer.Clear();
                return this;
            }

            public Builder Reserve(int capacity)
            {
                if (capacity < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(capacity));
                }

                ValueBuffer.Reserve(capacity);
                ValidityBuffer.Reserve(capacity);
                return this;
            }

            public Builder Resize(int length)
            {
                if (length < 0)
                {
                    throw new ArgumentOutOfRangeException(nameof(length));
                }

                ValueBuffer.Resize(length);
                ValidityBuffer.Resize(length);
                return this;
            }

            public Builder Toggle(int index)
            {
                CheckIndex(index);

                // If there is a null at this index, assume it was set to false in the value buffer, and so becomes
                // true/non-null after toggling.
                ValueBuffer.Toggle(index);
                ValidityBuffer.Set(index);
                return this;
            }

            public Builder Set(int index)
            {
                CheckIndex(index);
                ValueBuffer.Set(index);
                ValidityBuffer.Set(index);
                return this;
            }

            public Builder Set(int index, bool value)
            {
                CheckIndex(index);
                ValueBuffer.Set(index, value);
                ValidityBuffer.Set(index);
                return this;
            }

            public Builder Swap(int i, int j)
            {
                CheckIndex(i);
                CheckIndex(j);
                ValueBuffer.Swap(i, j);
                ValidityBuffer.Swap(i, j);
                return this;
            }

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

        public ArrowBuffer ValueBuffer => Data.Buffers[1];
        public ReadOnlySpan<byte> Values => ValueBuffer.Span.Slice(0, (int) Math.Ceiling(Length / 8.0));

        public BooleanArray(
            ArrowBuffer valueBuffer, ArrowBuffer nullBitmapBuffer,
            int length, int nullCount, int offset)
            : this(new ArrayData(BooleanType.Default, length, nullCount, offset,
                new[] { nullBitmapBuffer, valueBuffer }))
        { }

        public BooleanArray(ArrayData data)
            : base(data)
        {
            data.EnsureDataType(ArrowTypeId.Boolean);
        }

        public override void Accept(IArrowArrayVisitor visitor) => Accept(this, visitor);

        [Obsolete("GetBoolean does not support null values. Use GetValue instead (which this method invokes internally).")]
        public bool GetBoolean(int index)
        {
            return GetValue(index).GetValueOrDefault();
        }

        public bool? GetValue(int index)
        {
            return IsNull(index)
                ? null
                : BitUtility.GetBit(ValueBuffer.Span, index + Offset);
        }

        int IReadOnlyCollection<bool?>.Count => Length;

        bool? IReadOnlyList<bool?>.this[int index] => GetValue(index);

        IEnumerator<bool?> IEnumerable<bool?>.GetEnumerator()
        {
            for (int index = 0; index < Length; index++)
            {
                yield return GetValue(index);
            }
        }

        IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable<bool?>)this).GetEnumerator();

        int ICollection<bool?>.Count => Length;
        bool ICollection<bool?>.IsReadOnly => true;
        void ICollection<bool?>.Add(bool? item) => throw new NotSupportedException("Collection is read-only.");
        bool ICollection<bool?>.Remove(bool? item) => throw new NotSupportedException("Collection is read-only.");
        void ICollection<bool?>.Clear() => throw new NotSupportedException("Collection is read-only.");

        bool ICollection<bool?>.Contains(bool? item)
        {
            for (int index = 0; index < Length; index++)
            {
                if (GetValue(index).Equals(item))
                    return true;
            }

            return false;
        }

        void ICollection<bool?>.CopyTo(bool?[] array, int arrayIndex)
        {
            for (int srcIndex = 0, destIndex = arrayIndex; srcIndex < Length; srcIndex++, destIndex++)
            {
                array[destIndex] = GetValue(srcIndex);
            }
        }
    }
}
