using J2N.IO;
using Lucene.Net.Diagnostics;
using Lucene.Net.Util.Fst;
using System;
using System.IO;
using System.Runtime.CompilerServices;

namespace Lucene.Net.Store
{
    /*
     * 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.
     */

    /// <summary>
    /// Base <see cref="IndexInput"/> implementation that uses an array
    /// of <see cref="ByteBuffer"/>s to represent a file.
    /// <para/>
    /// Because Java's <see cref="ByteBuffer"/> uses an <see cref="int"/> to address the
    /// values, it's necessary to access a file greater
    /// <see cref="int.MaxValue"/> in size using multiple byte buffers.
    /// <para/>
    /// For efficiency, this class requires that the buffers
    /// are a power-of-two (<c>chunkSizePower</c>).
    /// </summary>
    public abstract class ByteBufferIndexInput : IndexInput
    {
        private ByteBuffer[] buffers;

        private readonly long chunkSizeMask;
        private readonly int chunkSizePower;

        private int offset;
        private long length;
        private string sliceDescription;

        private int curBufIndex;

        private ByteBuffer curBuf; // redundant for speed: buffers[curBufIndex]
        private bool isClone = false;
        // LUCENENET: Using ConditionalWeakTable rather than WeakIdenityMap. ConditionalWeakTable
        // uses RuntimeHelpers.GetHashCode() to find the item, so technically, it IS an identity collection.
        private ConditionalWeakTable<ByteBufferIndexInput, BoolRefWrapper> clones;

        private class BoolRefWrapper
        {
            private readonly bool value;

            // .NET port: this is needed as bool is not a reference type
            public BoolRefWrapper(bool value)
            {
                this.value = value;
            }

            public static implicit operator bool(BoolRefWrapper value)
            {
                return value.value;
            }

            public static implicit operator BoolRefWrapper(bool value)
            {
                return new BoolRefWrapper(value);
            }
        }

        internal ByteBufferIndexInput(string resourceDescription, ByteBuffer[] buffers, long length, int chunkSizePower, bool trackClones)
            : base(resourceDescription)
        {
            //this.buffers = buffers; // LUCENENET: this is set in SetBuffers()
            this.length = length;
            this.chunkSizePower = chunkSizePower;
            this.chunkSizeMask = (1L << chunkSizePower) - 1L;
            // LUCENENET: Using ConditionalWeakTable rather than WeakIdenityMap. ConditionalWeakTable
            // uses RuntimeHelpers.GetHashCode() to find the item, so technically, it IS an identity collection.
            this.clones = trackClones ? new ConditionalWeakTable<ByteBufferIndexInput, BoolRefWrapper>() : null;

            if (Debugging.AssertsEnabled)
            {
                Debugging.Assert(chunkSizePower >= 0 && chunkSizePower <= 30);
                Debugging.Assert(((long)((ulong)length >> chunkSizePower)) < int.MaxValue);
            }

            // LUCENENET specific: MMapIndexInput calls SetBuffers() to populate
            // the buffers, so we need to skip that call if it is null here, and
            // do the seek inside SetBuffers()
            if (buffers != null)
            {
                SetBuffers(buffers);
            }
        }

        // LUCENENET specific for encapsulating buffers field.
        internal void SetBuffers(ByteBuffer[] buffers) // necessary for MMapIndexInput
        {
            this.buffers = buffers;
            Seek(0L);
        }

        public override sealed byte ReadByte()
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            if (curBuf.HasRemaining)
            {
                return curBuf.Get();
            }

            do
            {
                curBufIndex++;
                if (curBufIndex >= buffers.Length)
                {
                    throw new EndOfStreamException("read past EOF: " + this);
                }
                curBuf = buffers[curBufIndex];
                curBuf.Position = 0;
            } while (!curBuf.HasRemaining);
            return curBuf.Get();
        }

        public override sealed void ReadBytes(byte[] b, int offset, int len)
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            int curAvail = curBuf.Remaining;
            if (len <= curAvail)
            {
                curBuf.Get(b, offset, len);
            }
            else
            {
                while (len > curAvail)
                {
                    curBuf.Get(b, offset, curAvail);
                    len -= curAvail;
                    offset += curAvail;
                    curBufIndex++;
                    if (curBufIndex >= buffers.Length)
                    {
                        throw new EndOfStreamException("read past EOF: " + this);
                    }
                    curBuf = buffers[curBufIndex];
                    curBuf.Position = 0;
                    curAvail = curBuf.Remaining;
                }
                curBuf.Get(b, offset, len);
            }
        }

        /// <summary>
        /// NOTE: this was readShort() in Lucene
        /// </summary>
        public override sealed short ReadInt16()
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            if (curBuf.Remaining >= 2)
            {
                return curBuf.GetInt16();
            }
            return base.ReadInt16();
        }

        /// <summary>
        /// NOTE: this was readInt() in Lucene
        /// </summary>
        public override sealed int ReadInt32()
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            if (curBuf.Remaining >= 4)
            {
                return curBuf.GetInt32();
            }
            return base.ReadInt32();
        }

        /// <summary>
        /// NOTE: this was readLong() in Lucene
        /// </summary>
        public override sealed long ReadInt64()
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            if (curBuf.Remaining >= 8)
            {
                return curBuf.GetInt64();
            }
            return base.ReadInt64();
        }

        public override sealed long GetFilePointer()
        {
            // LUCENENET: Refactored to avoid calls on invalid conditions instead of
            // catching and re-throwing exceptions in the normal workflow.
            EnsureOpen();
            return (((long)curBufIndex) << chunkSizePower) + curBuf.Position - offset;
        }

        public override sealed void Seek(long pos)
        {
            // necessary in case offset != 0 and pos < 0, but pos >= -offset
            if (pos < 0L)
            {
                throw new ArgumentException("Seeking to negative position: " + this);
            }
            pos += offset;
            // we use >> here to preserve negative, so we will catch AIOOBE,
            // in case pos + offset overflows.
            int bi = (int)(pos >> chunkSizePower);
            try
            {
                ByteBuffer b = buffers[bi];
                b.Position = ((int)(pos & chunkSizeMask));
                // write values, on exception all is unchanged
                this.curBufIndex = bi;
                this.curBuf = b;
            }
            catch (IndexOutOfRangeException)
            {
                throw new EndOfStreamException("seek past EOF: " + this);
            }
            catch (ArgumentException)
            {
                throw new EndOfStreamException("seek past EOF: " + this);
            }
            catch (NullReferenceException)
            {
                throw new ObjectDisposedException(this.GetType().FullName, "Already closed: " + this);
            }
        }

        public override sealed long Length => length;

        public override sealed object Clone()
        {
            ByteBufferIndexInput clone = BuildSlice(0L, this.length);
            try
            {
                clone.Seek(GetFilePointer());
            }
            catch (IOException ioe)
            {
                throw new Exception("Should never happen: " + this, ioe);
            }

            return clone;
        }

        /// <summary>
        /// Creates a slice of this index input, with the given description, offset, and length. The slice is seeked to the beginning.
        /// </summary>
        public ByteBufferIndexInput Slice(string sliceDescription, long offset, long length)
        {
            if (isClone) // well we could, but this is stupid
            {
                throw new InvalidOperationException("cannot slice() " + sliceDescription + " from a cloned IndexInput: " + this);
            }
            ByteBufferIndexInput clone = BuildSlice(offset, length);
            clone.sliceDescription = sliceDescription;
            try
            {
                clone.Seek(0L);
            }
            catch (IOException ioe)
            {
                throw new Exception("Should never happen: " + this, ioe);
            }

            return clone;
        }

        private ByteBufferIndexInput BuildSlice(long offset, long length)
        {
            if (buffers == null)
            {
                throw new ObjectDisposedException(this.GetType().FullName, "Already closed: " + this);
            }
            if (offset < 0 || length < 0 || offset + length > this.length)
            {
                throw new ArgumentException("slice() " + sliceDescription + " out of bounds: offset=" + offset + ",length=" + length + ",fileLength=" + this.length + ": " + this);
            }

            // include our own offset into the final offset:
            offset += this.offset;

            ByteBufferIndexInput clone = (ByteBufferIndexInput)base.Clone();
            clone.isClone = true;
            // we keep clone.clones, so it shares the same map with original and we have no additional cost on clones
            if (Debugging.AssertsEnabled) Debugging.Assert(clone.clones == this.clones);
            clone.buffers = BuildSlice(buffers, offset, length);
            clone.offset = (int)(offset & chunkSizeMask);
            clone.length = length;

            // register the new clone in our clone list to clean it up on closing:
            if (clones != null)
            {
                this.clones.Add(clone, true);
            }

            return clone;
        }

        /// <summary>
        /// Returns a sliced view from a set of already-existing buffers:
        /// the last buffer's <see cref="J2N.IO.Buffer.Limit"/> will be correct, but
        /// you must deal with <paramref name="offset"/> separately (the first buffer will not be adjusted)
        /// </summary>
        private ByteBuffer[] BuildSlice(ByteBuffer[] buffers, long offset, long length)
        {
            long sliceEnd = offset + length;

            int startIndex = (int)((long)((ulong)offset >> chunkSizePower));
            int endIndex = (int)((long)((ulong)sliceEnd >> chunkSizePower));

            // we always allocate one more slice, the last one may be a 0 byte one
            ByteBuffer[] slices = new ByteBuffer[endIndex - startIndex + 1];

            for (int i = 0; i < slices.Length; i++)
            {
                slices[i] = buffers[startIndex + i].Duplicate();
            }

            // set the last buffer's limit for the sliced view.
            slices[slices.Length - 1].Limit = ((int)(sliceEnd & chunkSizeMask));

            return slices;
        }

        private void UnsetBuffers()
        {
            buffers = null;
            curBuf = null;
            curBufIndex = 0;
        }

        // LUCENENET specific - rather than using all of this exception catching nonsense 
        // for control flow, we check whether we are disposed first.
        private void EnsureOpen()
        {
            if (buffers == null || curBuf == null)
            {
                throw new ObjectDisposedException(this.GetType().FullName, "Already closed: " + this);
            }
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing)
            {
                try
                {
                    if (buffers == null)
                    {
                        return;
                    }

                    // make local copy, then un-set early
                    ByteBuffer[] bufs = buffers;
                    UnsetBuffers();
                    if (clones != null)
                    {
                        clones.Remove(this);
                    }

                    if (isClone)
                    {
                        return;
                    }

                    // for extra safety unset also all clones' buffers:
                    if (clones != null)
                    {
                        // LUCENENET: Since .NET will GC types that go out of scope automatically,
                        // this isn't strictly necessary. However, we are doing it anyway when
                        // the enumerator is available (.NET Standard 2.1+)
#if FEATURE_CONDITIONALWEAKTABLE_ENUMERATOR
                        foreach (var pair in clones)
                        {
                            if (Debugging.AssertsEnabled) Debugging.Assert(pair.Key.isClone);
                            pair.Key.UnsetBuffers();
                        }
                        this.clones.Clear();
#endif
                        this.clones = null; // LUCENENET: de-reference the table so it can be GC'd
                    }

                    foreach (ByteBuffer b in bufs)
                    {
                        FreeBuffer(b); // LUCENENET: This calls Dispose() when necessary
                    }
                }
                finally
                {
                    UnsetBuffers();
                }
            }
        }

        /// <summary>
        /// Called when the contents of a buffer will be no longer needed.
        /// </summary>
        protected abstract void FreeBuffer(ByteBuffer b);

        public override sealed string ToString()
        {
            if (sliceDescription != null)
            {
                return base.ToString() + " [slice=" + sliceDescription + "]";
            }
            else
            {
                return base.ToString();
            }
        }
    }
}