using Lucene.Net.Support;
using Lucene.Net.Support.IO;
using System;
using System.Diagnostics;
using System.IO;

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>
    /// An <see cref="FSDirectory"/> implementation that uses <see cref="FileStream"/>'s
    /// positional read, which allows multiple threads to read from the same file
    /// without synchronizing.
    /// <para/>
    /// This class only uses <see cref="FileStream"/> when reading; writing is achieved with
    /// <see cref="FSDirectory.FSIndexOutput"/>.
    /// <para>
    /// <b>NOTE</b>: <see cref="NIOFSDirectory"/> is not recommended on Windows because of a bug in
    /// how FileChannel.read is implemented in Sun's JRE. Inside of the
    /// implementation the position is apparently synchronized. See <a
    /// href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6265734">here</a>
    /// for details.
    /// </para>
    /// <para>
    /// <font color="red"><b>NOTE:</b> Accessing this class either directly or
    /// indirectly from a thread while it's interrupted can close the
    /// underlying file descriptor immediately if at the same time the thread is
    /// blocked on IO. The file descriptor will remain closed and subsequent access
    /// to <see cref="NIOFSDirectory"/> will throw a <see cref="ObjectDisposedException"/>. If
    /// your application uses either <see cref="System.Threading.Thread.Interrupt()"/> or
    /// <see cref="System.Threading.Tasks.Task"/> you should use <see cref="SimpleFSDirectory"/> in
    /// favor of <see cref="NIOFSDirectory"/>.</font>
    /// </para>
    /// </summary>
#if FEATURE_SERIALIZABLE
    [Serializable]
#endif
    public class NIOFSDirectory : FSDirectory
    {
        /// <summary>
        /// Create a new <see cref="NIOFSDirectory"/> for the named location.
        /// </summary>
        /// <param name="path"> the path of the directory </param>
        /// <param name="lockFactory"> the lock factory to use, or null for the default
        /// (<see cref="NativeFSLockFactory"/>); </param>
        /// <exception cref="IOException"> if there is a low-level I/O error </exception>
        public NIOFSDirectory(DirectoryInfo path, LockFactory lockFactory)
            : base(path, lockFactory)
        {
        }

        /// <summary>
        /// Create a new <see cref="NIOFSDirectory"/> for the named location and <see cref="NativeFSLockFactory"/>.
        /// </summary>
        /// <param name="path"> the path of the directory </param>
        /// <exception cref="IOException"> if there is a low-level I/O error </exception>
        public NIOFSDirectory(DirectoryInfo path)
            : base(path, null)
        {
        }

        /// <summary>
        /// Creates an <see cref="IndexInput"/> for the file with the given name. </summary>
        public override IndexInput OpenInput(string name, IOContext context)
        {
            EnsureOpen();
            var path = new FileInfo(Path.Combine(Directory.FullName, name));
            var fc = new FileStream(path.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
            return new NIOFSIndexInput("NIOFSIndexInput(path=\"" + path + "\")", fc, context);
        }

        public override IndexInputSlicer CreateSlicer(string name, IOContext context)
        {
            EnsureOpen();
            var path = new FileInfo(Path.Combine(Directory.FullName, name));
            var fc = new FileStream(path.FullName, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
            return new IndexInputSlicerAnonymousInnerClassHelper(this, context, path, fc);
        }

#if FEATURE_SERIALIZABLE
        [Serializable]
#endif
        private class IndexInputSlicerAnonymousInnerClassHelper : IndexInputSlicer
        {
            private readonly IOContext context;
            private readonly FileInfo path;
            private readonly FileStream descriptor;

            public IndexInputSlicerAnonymousInnerClassHelper(NIOFSDirectory outerInstance, IOContext context, FileInfo path, FileStream descriptor)
                : base(outerInstance)
            {
                this.context = context;
                this.path = path;
                this.descriptor = descriptor;
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing)
                {
                    descriptor.Dispose();
                }
            }

            public override IndexInput OpenSlice(string sliceDescription, long offset, long length)
            {
                return new NIOFSIndexInput("NIOFSIndexInput(" + sliceDescription + " in path=\"" + path + "\" slice=" + offset + ":" + (offset + length) + ")", descriptor, offset, length, 
                    BufferedIndexInput.GetBufferSize(context));
            }

            [Obsolete("Only for reading CFS files from 3.x indexes.")]
            public override IndexInput OpenFullSlice()
            {
                try
                {
                    return OpenSlice("full-slice", 0, descriptor.Length);
                }
                catch (IOException ex)
                {
                    throw new Exception(ex.ToString(), ex);
                }
            }
        }

        /// <summary>
        /// Reads bytes with the <see cref="FileStreamExtensions.Read(FileStream, ByteBuffer, long)"/>
        /// extension method for <see cref="FileStream"/>.
        /// </summary>
#if FEATURE_SERIALIZABLE
        [Serializable]
#endif
        protected class NIOFSIndexInput : BufferedIndexInput
        {
            /// <summary>
            /// The maximum chunk size for reads of 16384 bytes.
            /// </summary>
            private const int CHUNK_SIZE = 16384;

            /// <summary>
            /// the file channel we will read from </summary>
            protected readonly FileStream m_channel;

            /// <summary>
            /// is this instance a clone and hence does not own the file to close it </summary>
            internal bool isClone = false;

            /// <summary>
            /// start offset: non-zero in the slice case </summary>
            protected readonly long m_off;

            /// <summary>
            /// end offset (start+length) </summary>
            protected readonly long m_end;

            private ByteBuffer byteBuf; // wraps the buffer for NIO

            public NIOFSIndexInput(string resourceDesc, FileStream fc, IOContext context)
                : base(resourceDesc, context)
            {
                this.m_channel = fc;
                this.m_off = 0L;
                this.m_end = fc.Length;
            }

            public NIOFSIndexInput(string resourceDesc, FileStream fc, long off, long length, int bufferSize)
                : base(resourceDesc, bufferSize)
            {
                this.m_channel = fc;
                this.m_off = off;
                this.m_end = off + length;
                this.isClone = true;
            }

            protected override void Dispose(bool disposing)
            {
                if (disposing && !isClone)
                {
                    m_channel.Dispose();
                }
            }

            public override object Clone()
            {
                NIOFSIndexInput clone = (NIOFSIndexInput)base.Clone();
                clone.isClone = true;
                return clone;
            }

            public override sealed long Length
            {
                get { return m_end - m_off; }
            }

            protected override void NewBuffer(byte[] newBuffer)
            {
                base.NewBuffer(newBuffer);
                byteBuf = ByteBuffer.Wrap(newBuffer);
            }

            protected override void ReadInternal(byte[] b, int offset, int len)
            {
                ByteBuffer bb;

                // Determine the ByteBuffer we should use
                if (b == m_buffer && 0 == offset)
                {
                    // Use our own pre-wrapped byteBuf:
                    Debug.Assert(byteBuf != null);
                    byteBuf.Clear();
                    byteBuf.Limit = len;
                    bb = byteBuf;
                }
                else
                {
                    bb = ByteBuffer.Wrap(b, offset, len);
                }

                int readOffset = bb.Position;
                int readLength = bb.Limit - readOffset;
                long pos = GetFilePointer() + m_off;

                if (pos + len > m_end)
                {
                    throw new EndOfStreamException("read past EOF: " + this);
                }

                try
                {
                    while (readLength > 0)
                    {
                        int limit;
                        if (readLength > CHUNK_SIZE)
                        {
                            limit = readOffset + CHUNK_SIZE;
                        }
                        else
                        {
                            limit = readOffset + readLength;
                        }
                        bb.Limit = limit;
                        int i = m_channel.Read(bb, pos);
                        if (i <= 0) // be defensive here, even though we checked before hand, something could have changed
                        {
                            throw new Exception("read past EOF: " + this + " off: " + offset + " len: " + len + " pos: " + pos + " chunkLen: " + readLength + " end: " + m_end);
                        }
                        pos += i;
                        readOffset += i;
                        readLength -= i;
                    }
                    Debug.Assert(readLength == 0);
                }
                catch (IOException ioe)
                {
                    throw new IOException(ioe.ToString() + ": " + this, ioe);
                }
            }

            protected override void SeekInternal(long pos)
            {
            }
        }
    }
}