/*
 * 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.
 */
package org.apache.lucene.store;

import java.io.EOFException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedChannelException; // javadoc @link
import java.nio.channels.FileChannel;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.util.concurrent.Future; // javadoc
import org.apache.lucene.util.IOUtils;

/**
 * An {@link FSDirectory} implementation that uses java.nio's FileChannel's positional read, which
 * allows multiple threads to read from the same file without synchronizing.
 *
 * <p>This class only uses FileChannel when reading; writing is achieved with {@link
 * FSDirectory.FSIndexOutput}.
 *
 * <p><b>NOTE</b>: 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="https://bugs.java.com/bugdatabase/view_bug?bug_id=6265734">here</a> for details.
 *
 * <p><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 {@link
 * NIOFSDirectory} will throw a {@link ClosedChannelException}. If your application uses either
 * {@link Thread#interrupt()} or {@link Future#cancel(boolean)} you should use the legacy {@code
 * RAFDirectory} from the Lucene {@code misc} module in favor of {@link NIOFSDirectory}.
 */
public class NIOFSDirectory extends FSDirectory {

  /**
   * Create a new NIOFSDirectory for the named location. The directory is created at the named
   * location if it does not yet exist.
   *
   * @param path the path of the directory
   * @param lockFactory the lock factory to use
   * @throws IOException if there is a low-level I/O error
   */
  public NIOFSDirectory(Path path, LockFactory lockFactory) throws IOException {
    super(path, lockFactory);
  }

  /**
   * Create a new NIOFSDirectory for the named location and {@link FSLockFactory#getDefault()}. The
   * directory is created at the named location if it does not yet exist.
   *
   * @param path the path of the directory
   * @throws IOException if there is a low-level I/O error
   */
  public NIOFSDirectory(Path path) throws IOException {
    this(path, FSLockFactory.getDefault());
  }

  @Override
  public IndexInput openInput(String name, IOContext context) throws IOException {
    ensureOpen();
    ensureCanRead(name);
    Path path = getDirectory().resolve(name);
    FileChannel fc = FileChannel.open(path, StandardOpenOption.READ);
    boolean success = false;
    try {
      final NIOFSIndexInput indexInput =
          new NIOFSIndexInput("NIOFSIndexInput(path=\"" + path + "\")", fc, context);
      success = true;
      return indexInput;
    } finally {
      if (success == false) {
        IOUtils.closeWhileHandlingException(fc);
      }
    }
  }

  /** Reads bytes with {@link FileChannel#read(ByteBuffer, long)} */
  static final class NIOFSIndexInput extends BufferedIndexInput {
    /** The maximum chunk size for reads of 16384 bytes. */
    private static final int CHUNK_SIZE = 16384;

    /** the file channel we will read from */
    protected final FileChannel channel;

    /** is this instance a clone and hence does not own the file to close it */
    boolean isClone = false;

    /** start offset: non-zero in the slice case */
    protected final long off;

    /** end offset (start+length) */
    protected final long end;

    public NIOFSIndexInput(String resourceDesc, FileChannel fc, IOContext context)
        throws IOException {
      super(resourceDesc, context);
      this.channel = fc;
      this.off = 0L;
      this.end = fc.size();
    }

    public NIOFSIndexInput(
        String resourceDesc, FileChannel fc, long off, long length, int bufferSize) {
      super(resourceDesc, bufferSize);
      this.channel = fc;
      this.off = off;
      this.end = off + length;
      this.isClone = true;
    }

    @Override
    public void close() throws IOException {
      if (!isClone) {
        channel.close();
      }
    }

    @Override
    public NIOFSIndexInput clone() {
      NIOFSIndexInput clone = (NIOFSIndexInput) super.clone();
      clone.isClone = true;
      return clone;
    }

    @Override
    public IndexInput slice(String sliceDescription, long offset, long length) throws IOException {
      if ((length | offset) < 0 || length > this.length() - offset) {
        throw new IllegalArgumentException(
            "slice() "
                + sliceDescription
                + " out of bounds: offset="
                + offset
                + ",length="
                + length
                + ",fileLength="
                + this.length()
                + ": "
                + this);
      }
      return new NIOFSIndexInput(
          getFullSliceDescription(sliceDescription),
          channel,
          off + offset,
          length,
          getBufferSize());
    }

    @Override
    public final long length() {
      return end - off;
    }

    @Override
    protected void readInternal(ByteBuffer b) throws IOException {
      long pos = getFilePointer() + off;

      if (pos + b.remaining() > end) {
        throw new EOFException("read past EOF: " + this);
      }

      try {
        int readLength = b.remaining();
        while (readLength > 0) {
          final int toRead = Math.min(CHUNK_SIZE, readLength);
          b.limit(b.position() + toRead);
          assert b.remaining() == toRead;
          final int i = channel.read(b, pos);
          if (i < 0) {
            // be defensive here, even though we checked before hand, something could have changed
            throw new EOFException(
                "read past EOF: "
                    + this
                    + " buffer: "
                    + b
                    + " chunkLen: "
                    + toRead
                    + " end: "
                    + end);
          }
          assert i > 0
              : "FileChannel.read with non zero-length bb.remaining() must always read at least "
                  + "one byte (FileChannel is in blocking mode, see spec of ReadableByteChannel)";
          pos += i;
          readLength -= i;
        }
        assert readLength == 0;
      } catch (IOException ioe) {
        throw new IOException(ioe.getMessage() + ": " + this, ioe);
      }
    }

    @Override
    protected void seekInternal(long pos) throws IOException {
      if (pos > length()) {
        throw new EOFException(
            "read past EOF: pos=" + pos + " vs length=" + length() + ": " + this);
      }
    }
  }
}
