/*
 * Copyright (c) 2000, 2025, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package sun.nio.ch;

import java.io.Closeable;
import java.io.FileDescriptor;
import java.io.IOException;
import java.io.UncheckedIOException;
import java.lang.foreign.MemorySegment;
import java.lang.foreign.Arena;
import java.lang.ref.Cleaner.Cleanable;
import java.nio.ByteBuffer;
import java.nio.MappedByteBuffer;
import java.nio.channels.AsynchronousCloseException;
import java.nio.channels.Channel;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.FileLockInterruptionException;
import java.nio.channels.NonReadableChannelException;
import java.nio.channels.NonWritableChannelException;
import java.nio.channels.ReadableByteChannel;
import java.nio.channels.SelectableChannel;
import java.nio.channels.WritableByteChannel;
import java.util.Objects;

import jdk.internal.access.JavaIOFileDescriptorAccess;
import jdk.internal.access.SharedSecrets;
import jdk.internal.event.FileForceEvent;
import jdk.internal.foreign.MemorySessionImpl;
import jdk.internal.foreign.SegmentFactories;
import jdk.internal.misc.Blocker;
import jdk.internal.misc.ExtendedMapMode;
import jdk.internal.misc.Unsafe;
import jdk.internal.misc.VM;
import jdk.internal.misc.VM.BufferPool;
import jdk.internal.ref.Cleaner;
import jdk.internal.ref.CleanerFactory;
import jdk.internal.event.FileReadEvent;
import jdk.internal.event.FileWriteEvent;
import jdk.internal.access.foreign.UnmapperProxy;

public class FileChannelImpl
    extends FileChannel
{
    // Access to FileDescriptor internals
    private static final JavaIOFileDescriptorAccess fdAccess =
        SharedSecrets.getJavaIOFileDescriptorAccess();

    // Used to make native read and write calls
    private static final FileDispatcher nd = new FileDispatcherImpl();

    // Flag set by jdk.internal.event.JFRTracing to indicate if
    // file reads and writes should be traced by JFR.
    private static boolean jfrTracing;

    // File descriptor
    private final FileDescriptor fd;

    // File access mode (immutable)
    private final boolean writable;
    private final boolean readable;
    private final boolean sync;  // O_SYNC or O_DSYNC

    // Required to prevent finalization of creating stream (immutable)
    private final Closeable parent;

    // The path of the referenced file
    // (null if the parent stream is created with a file descriptor)
    private final String path;

    // Thread-safe set of IDs of native threads, for signalling
    private final NativeThreadSet threads = new NativeThreadSet(2);

    // Lock for operations involving position and size
    private final Object positionLock = new Object();

    // blocking operations are not interruptible
    private volatile boolean uninterruptible;

    // DirectIO flag
    private final boolean direct;

    // IO alignment value for DirectIO
    private final int alignment;

    // Cleanable with an action which closes this channel's file descriptor
    private final Cleanable closer;

    private static class Closer implements Runnable {
        private final FileDescriptor fd;

        Closer(FileDescriptor fd) {
            this.fd = fd;
        }

        public void run() {
            try {
                fdAccess.close(fd);
            } catch (IOException ioe) {
                // Rethrow as unchecked so the exception can be propagated as needed
                throw new UncheckedIOException("close", ioe);
            }
        }
    }

    private FileChannelImpl(FileDescriptor fd, String path, boolean readable,
                            boolean writable, boolean sync, boolean direct,
                            Closeable parent)
    {
        this.fd = fd;
        this.path = path;
        this.readable = readable;
        this.writable = writable;
        this.sync = sync;
        this.direct = direct;
        this.parent = parent;
        if (direct) {
            assert path != null;
            this.alignment = nd.setDirectIO(fd, path);
        } else {
            this.alignment = -1;
        }

        // Register a cleaning action if and only if there is no parent
        // as the parent will take care of closing the file descriptor.
        // FileChannel is used by the LambdaMetaFactory so a lambda cannot
        // be used here hence we use a nested class instead.
        this.closer = parent != null ? null :
            CleanerFactory.cleaner().register(this, new Closer(fd));
    }


    // Used by FileInputStream::getChannel, FileOutputStream::getChannel,
    // and RandomAccessFile::getChannel
    public static FileChannel open(FileDescriptor fd, String path,
                                   boolean readable, boolean writable,
                                   boolean sync, boolean direct, Closeable parent)
    {
        return new FileChannelImpl(fd, path, readable, writable, sync, direct, parent);
    }

    private void ensureOpen() throws IOException {
        if (!isOpen())
            throw new ClosedChannelException();
    }

    public void setUninterruptible() {
        uninterruptible = true;
    }

    private void beginBlocking() {
        if (!uninterruptible) begin();
    }

    private void endBlocking(boolean completed) throws AsynchronousCloseException {
        if (!uninterruptible) end(completed);
    }

    // -- Standard channel operations --

    protected void implCloseChannel() throws IOException {
        if (!fd.valid())
            return; // nothing to do

        // Release and invalidate any locks that we still hold
        if (fileLockTable != null) {
            for (FileLock fl: fileLockTable.removeAll()) {
                synchronized (fl) {
                    if (fl.isValid()) {
                        nd.release(fd, fl.position(), fl.size());
                        ((FileLockImpl)fl).invalidate();
                    }
                }
            }
        }

        // signal any threads blocked on this channel
        threads.signalAndWait();

        if (parent != null) {
            //
            // Close the fd via the parent stream's close method.  The parent
            // will reinvoke our close method, which is defined in the
            // superclass AbstractInterruptibleChannel, but the isOpen logic in
            // that method will prevent this method from being reinvoked.
            //
            parent.close();
        } else { // parent == null hence closer != null
            //
            // Perform the cleaning action so it is not redone when
            // this channel becomes phantom reachable.
            //
            try {
                closer.clean();
            } catch (UncheckedIOException uioe) {
                throw uioe.getCause();
            }
        }
    }

    @Override
    public int read(ByteBuffer dst) throws IOException {
        if (jfrTracing && FileReadEvent.enabled()) {
            return traceImplRead(dst);
        }
        return implRead(dst);
    }

    private int implRead(ByteBuffer dst) throws IOException {
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        synchronized (positionLock) {
            if (direct)
                Util.checkChannelPositionAligned(position(), alignment);
            int n = 0;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    boolean attempted = Blocker.begin(direct);
                    try {
                        n = IOUtil.read(fd, dst, -1, direct, alignment, nd);
                    } finally {
                        Blocker.end(attempted);
                    }
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                endBlocking(n > 0);
                assert IOStatus.check(n);
            }
        }
    }

    private int traceImplRead(ByteBuffer dst) throws IOException {
        int bytesRead = 0;
        long start = FileReadEvent.timestamp();
        try {
            bytesRead = implRead(dst);
        } finally {
            FileReadEvent.offer(start, path, bytesRead);
        }
        return bytesRead;
    }

    @Override
    public long read(ByteBuffer[] dsts, int offset, int length) throws IOException {
        if (jfrTracing && FileReadEvent.enabled()) {
            return traceImplRead(dsts, offset, length);
        }
        return implRead(dsts, offset, length);
    }

    private long implRead(ByteBuffer[] dsts, int offset, int length)
        throws IOException
    {
        Objects.checkFromIndexSize(offset, length, dsts.length);
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        synchronized (positionLock) {
            if (direct)
                Util.checkChannelPositionAligned(position(), alignment);
            long n = 0;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    boolean attempted = Blocker.begin(direct);
                    try {
                        n = IOUtil.read(fd, dsts, offset, length, direct, alignment, nd);
                    } finally {
                        Blocker.end(attempted);
                    }

                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                endBlocking(n > 0);
                assert IOStatus.check(n);
            }
        }
    }

    private long traceImplRead(ByteBuffer[] dsts, int offset, int length) throws IOException {
        long bytesRead = 0;
        long start = FileReadEvent.timestamp();
        try {
            bytesRead = implRead(dsts, offset, length);
        } finally {
            FileReadEvent.offer(start, path, bytesRead);
        }
        return bytesRead;
    }

    @Override
    public int write(ByteBuffer src) throws IOException {
        if (jfrTracing && FileWriteEvent.enabled()) {
            return traceImplWrite(src);
        }
        return implWrite(src);
    }

    private int implWrite(ByteBuffer src) throws IOException {
        ensureOpen();
        if (!writable)
            throw new NonWritableChannelException();
        synchronized (positionLock) {
            if (direct)
                Util.checkChannelPositionAligned(position(), alignment);
            int n = 0;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    boolean attempted = Blocker.begin(sync || direct);
                    try {
                        n = IOUtil.write(fd, src, -1, direct, alignment, nd);
                    } finally {
                        Blocker.end(attempted);
                    }

                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                endBlocking(n > 0);
                assert IOStatus.check(n);
            }
        }
    }

    private int traceImplWrite(ByteBuffer src) throws IOException {
        int bytesWritten = 0;
        long start = FileWriteEvent.timestamp();
        try {
            bytesWritten = implWrite(src);
        } finally {
            FileWriteEvent.offer(start, path, bytesWritten);
        }
        return bytesWritten;
    }

    @Override
    public long write(ByteBuffer[] srcs, int offset, int length) throws IOException {
        if (jfrTracing && FileWriteEvent.enabled()) {
            return traceImplWrite(srcs, offset, length);
        }
        return implWrite(srcs, offset, length);
    }

    private long implWrite(ByteBuffer[] srcs, int offset, int length) throws IOException {
        Objects.checkFromIndexSize(offset, length, srcs.length);
        ensureOpen();
        if (!writable)
            throw new NonWritableChannelException();
        synchronized (positionLock) {
            if (direct)
                Util.checkChannelPositionAligned(position(), alignment);
            long n = 0;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                do {
                    boolean attempted = Blocker.begin(sync || direct);
                    try {
                        n = IOUtil.write(fd, srcs, offset, length, direct, alignment, nd);
                    } finally {
                        Blocker.end(attempted);
                    }
                } while ((n == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(n);
            } finally {
                threads.remove(ti);
                endBlocking(n > 0);
                assert IOStatus.check(n);
            }
        }
    }

    private long traceImplWrite(ByteBuffer[] srcs, int offset, int length) throws IOException {
        long bytesWritten = 0;
        long start = FileWriteEvent.timestamp();
        try {
            bytesWritten = implWrite(srcs, offset, length);
        } finally {
            FileWriteEvent.offer(start, path, bytesWritten);
        }
        return bytesWritten;
    }

    // -- Other operations --

    @Override
    public long position() throws IOException {
        ensureOpen();
        synchronized (positionLock) {
            long p = -1;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return 0;
                boolean append = fdAccess.getAppend(fd);
                do {
                    // in append-mode then position is advanced to end before writing
                    p = (append) ? nd.size(fd) : nd.seek(fd, -1);
                } while ((p == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(p);
            } finally {
                threads.remove(ti);
                endBlocking(p > -1);
                assert IOStatus.check(p);
            }
        }
    }

    @Override
    public FileChannel position(long newPosition) throws IOException {
        ensureOpen();
        if (newPosition < 0)
            throw new IllegalArgumentException();
        synchronized (positionLock) {
            long p = -1;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return null;
                do {
                    p = nd.seek(fd, newPosition);
                } while ((p == IOStatus.INTERRUPTED) && isOpen());
                return this;
            } finally {
                threads.remove(ti);
                endBlocking(p > -1);
                assert IOStatus.check(p);
            }
        }
    }

    @Override
    public long size() throws IOException {
        ensureOpen();
        synchronized (positionLock) {
            long s = -1;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return -1;
                do {
                    s = nd.size(fd);
                } while ((s == IOStatus.INTERRUPTED) && isOpen());
                return IOStatus.normalize(s);
            } finally {
                threads.remove(ti);
                endBlocking(s > -1);
                assert IOStatus.check(s);
            }
        }
    }

    /**
     * Returns an estimate of the number of remaining bytes that can be read
     * from this channel without blocking.
     */
    int available() throws IOException {
        ensureOpen();
        synchronized (positionLock) {
            int a = -1;
            int ti = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return -1;
                a = nd.available(fd);
            } finally {
                threads.remove(ti);
                endBlocking(a > -1);
            }
            return a;
        }
    }

    /**
     * Tells whether the channel represents something other than a regular
     * file, directory, or symbolic link.
     */
    boolean isOther() throws IOException {
        ensureOpen();
        int ti = -1;
        Boolean isOther = null;
        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return false;
            return isOther = nd.isOther(fd);
        } finally {
            threads.remove(ti);
            endBlocking(isOther != null);
        }
    }

    @Override
    public FileChannel truncate(long newSize) throws IOException {
        ensureOpen();
        if (newSize < 0)
            throw new IllegalArgumentException("Negative size");
        if (!writable)
            throw new NonWritableChannelException();
        synchronized (positionLock) {
            int rv = -1;
            long p = -1;
            int ti = -1;
            long rp = -1;
            try {
                beginBlocking();
                ti = threads.add();
                if (!isOpen())
                    return null;

                // get current size
                long size;
                do {
                    size = nd.size(fd);
                } while ((size == IOStatus.INTERRUPTED) && isOpen());
                if (!isOpen())
                    return null;

                // get current position
                do {
                    p = nd.seek(fd, -1);
                } while ((p == IOStatus.INTERRUPTED) && isOpen());
                if (!isOpen())
                    return null;
                assert p >= 0;

                // truncate file if given size is less than the current size
                if (newSize < size) {
                    do {
                        rv = nd.truncate(fd, newSize);
                    } while ((rv == IOStatus.INTERRUPTED) && isOpen());
                    if (!isOpen())
                        return null;
                }

                // if position is beyond new size then adjust it
                if (p > newSize)
                    p = newSize;
                do {
                    rp = nd.seek(fd, p);
                } while ((rp == IOStatus.INTERRUPTED) && isOpen());
                return this;
            } finally {
                threads.remove(ti);
                endBlocking(rv > -1);
                assert IOStatus.check(rv);
            }
        }
    }

    private void implForce(boolean metaData) throws IOException {
        ensureOpen();
        int rv = -1;
        int ti = -1;
        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return;
            do {
                boolean attempted = Blocker.begin();
                try {
                    rv = nd.force(fd, metaData);
                } finally {
                    Blocker.end(attempted);
                }
            } while ((rv == IOStatus.INTERRUPTED) && isOpen());
        } finally {
            threads.remove(ti);
            endBlocking(rv > -1);
            assert IOStatus.check(rv);
        }
    }

    @Override
    public void force(boolean metaData) throws IOException {
        if (!FileForceEvent.enabled()) {
            implForce(metaData);
            return;
        }
        long start = FileForceEvent.timestamp();
        implForce(metaData);
        FileForceEvent.offer(start, path, metaData);
    }

    // Assume at first that the underlying kernel supports sendfile/equivalent;
    // set this to true if we find out later that it doesn't
    //
    private static volatile boolean transferToDirectNotSupported;

    // Assume at first that the underlying kernel supports copy_file_range/equivalent;
    // set this to true if we find out later that it doesn't
    //
    private static volatile boolean transferFromDirectNotSupported;

    /**
     * Marks the beginning of a transfer to or from this channel.
     * @throws ClosedChannelException if channel is closed
     */
    private int beforeTransfer() throws ClosedChannelException {
        int ti = threads.add();
        if (isOpen()) {
            return ti;
        } else {
            threads.remove(ti);
            throw new ClosedChannelException();
        }
    }

    /**
     * Marks the end of a transfer to or from this channel.
     * @throws AsynchronousCloseException if not completed and the channel is closed
     */
    private void afterTransfer(boolean completed, int ti) throws AsynchronousCloseException {
        threads.remove(ti);
        if (!completed && !isOpen()) {
            throw new AsynchronousCloseException();
        }
    }

    /**
     * Invoked when ClosedChannelException is thrown during a transfer. This method
     * translates it to an AsynchronousCloseException or ClosedByInterruptException. In
     * the case of ClosedByInterruptException, it ensures that this channel and the
     * source/target channel are closed.
     */
    private AsynchronousCloseException transferFailed(ClosedChannelException cce, Channel other) {
        ClosedByInterruptException cbie = null;
        if (cce instanceof ClosedByInterruptException e) {
            assert Thread.currentThread().isInterrupted();
            cbie = e;
        } else if (!uninterruptible && Thread.currentThread().isInterrupted()) {
            cbie = new ClosedByInterruptException();
        }
        if (cbie != null) {
            try {
                this.close();
            } catch (IOException ioe) {
                cbie.addSuppressed(ioe);
            }
            try {
                other.close();
            } catch (IOException ioe) {
                cbie.addSuppressed(ioe);
            }
            return cbie;

        }
        // one of the channels was closed during the transfer
        if (cce instanceof AsynchronousCloseException ace) {
            return ace;
        } else {
            var ace = new AsynchronousCloseException();
            ace.addSuppressed(cce);
            return ace;
        }
    }

    /**
     * Transfers bytes from this channel's file to the resource that the given file
     * descriptor is connected to.
     */
    private long transferToFileDescriptor(long position, int count, FileDescriptor targetFD) {
        long n;
        boolean append = fdAccess.getAppend(targetFD);
        do {
            n = nd.transferTo(fd, position, count, targetFD, append);
        } while ((n == IOStatus.INTERRUPTED) && isOpen());
        return n;
    }

    /**
     * Transfers bytes from this channel's file to the given channel's file.
     */
    private long transferToFileChannel(long position, int count, FileChannelImpl target)
        throws IOException
    {
        final FileChannelImpl source = this;
        boolean completed = false;
        try {
            beginBlocking();
            int sourceIndex = source.beforeTransfer();
            try {
                int targetIndex = target.beforeTransfer();
                try {
                    long n = transferToFileDescriptor(position, count, target.fd);
                    completed = (n >= 0);
                    return IOStatus.normalize(n);
                } finally {
                    target.afterTransfer(completed, targetIndex);
                }
            } finally {
                source.afterTransfer(completed, sourceIndex);
            }
        } finally {
            endBlocking(completed);
        }
    }

    /**
     * Transfers bytes from this channel's file to the given channel's socket.
     */
    private long transferToSocketChannel(long position, int count, SocketChannelImpl target)
        throws IOException
    {
        final FileChannelImpl source = this;
        boolean completed = false;
        try {
            beginBlocking();
            int sourceIndex = source.beforeTransfer();
            try {
                target.beforeTransferTo();
                try {
                    long n = transferToFileDescriptor(position, count, target.getFD());
                    completed = (n >= 0);
                    return IOStatus.normalize(n);
                } finally {
                    target.afterTransferTo(completed);
                }
            } finally {
                source.afterTransfer(completed, sourceIndex);
            }
        } finally {
            endBlocking(completed);
        }
    }

    /**
     * Transfers bytes from this channel's file from the given channel's file. This
     * implementation uses sendfile, copy_file_range or equivalent.
     */
    private long transferToDirectInternal(long position, int count, WritableByteChannel target)
        throws IOException
    {
        assert !nd.transferToDirectlyNeedsPositionLock() || Thread.holdsLock(positionLock);

        return switch (target) {
            case FileChannelImpl fci  -> transferToFileChannel(position, count, fci);
            case SocketChannelImpl sci -> transferToSocketChannel(position, count, sci);
            default -> IOStatus.UNSUPPORTED_CASE;
        };
    }

    /**
     * Transfers bytes from this channel's file to the given channel's file or socket.
     * @return the number of bytes transferred, UNSUPPORTED_CASE if this transfer cannot
     * be done directly, or UNSUPPORTED if there is no direct support
     */
    private long transferToDirect(long position, int count, WritableByteChannel target)
        throws IOException
    {
        if (transferToDirectNotSupported)
            return IOStatus.UNSUPPORTED;
        if (target instanceof SelectableChannel sc && !nd.canTransferToDirectly(sc))
            return IOStatus.UNSUPPORTED_CASE;

        long n;
        if (nd.transferToDirectlyNeedsPositionLock()) {
            synchronized (positionLock) {
                long pos = position();
                try {
                    n = transferToDirectInternal(position, count, target);
                } finally {
                    try {
                        position(pos);
                    } catch (ClosedChannelException ignore) {
                        // can't reset position if channel is closed
                    }
                }
            }
        } else {
            n = transferToDirectInternal(position, count, target);
        }
        if (n == IOStatus.UNSUPPORTED) {
            transferToDirectNotSupported = true;
        }
        return n;
    }

    // Size threshold above which to use a mapped buffer;
    // transferToArbitraryChannel() and transferFromArbitraryChannel()
    // are faster for smaller transfers
    private static final long MAPPED_TRANSFER_THRESHOLD = 16L*1024L;

    // Maximum size to map when using a mapped buffer
    private static final long MAPPED_TRANSFER_SIZE = 8L*1024L*1024L;

    /**
     * Transfers bytes from channel's file to the given channel. This implementation
     * memory maps this channel's file.
     */
    private long transferToTrustedChannel(long position, long count,
                                          WritableByteChannel target)
        throws IOException
    {
        if (count < MAPPED_TRANSFER_THRESHOLD)
            return IOStatus.UNSUPPORTED_CASE;

        boolean isSelChImpl = (target instanceof SelChImpl);
        if (!((target instanceof FileChannelImpl) || isSelChImpl))
            return IOStatus.UNSUPPORTED_CASE;

        if (target == this) {
            long posThis = position();
            if ((posThis - count + 1 <= position)
                    && (position - count + 1 <= posThis)
                    && !nd.canTransferToFromOverlappedMap()) {
                return IOStatus.UNSUPPORTED_CASE;
            }
        }

        long remaining = count;
        while (remaining > 0L) {
            long size = Math.min(remaining, MAPPED_TRANSFER_SIZE);
            try {
                MappedByteBuffer dbb = map(MapMode.READ_ONLY, position, size);
                try {
                    // write may block, closing this channel will not wake it up
                    int n = target.write(dbb);
                    assert n >= 0;
                    remaining -= n;
                    if (isSelChImpl) {
                        // one attempt to write to selectable channel
                        break;
                    }
                    assert n > 0;
                    position += n;
                } finally {
                    unmap(dbb);
                }
            } catch (IOException ioe) {
                // Only throw exception if no bytes have been written
                if (remaining == count)
                    throw ioe;
                break;
            }
        }
        return count - remaining;
    }

    /**
     * Transfers bytes from channel's file to the given channel.
     */
    private long transferToArbitraryChannel(long position, long count,
                                            WritableByteChannel target)
        throws IOException
    {
        // Untrusted target: Use a newly-erased buffer
        int c = (int) Math.min(count, TRANSFER_SIZE);
        ByteBuffer bb = ByteBuffer.allocate(c);
        long tw = 0;                    // Total bytes written
        long pos = position;
        try {
            while (tw < count) {
                bb.limit((int) Math.min(count - tw, TRANSFER_SIZE));
                int nr = read(bb, pos);
                if (nr <= 0)
                    break;
                bb.flip();
                // write may block, closing this channel will not wake it up
                int nw = target.write(bb);
                tw += nw;
                if (nw != nr)
                    break;
                pos += nw;
                bb.clear();
            }
            return tw;
        } catch (IOException x) {
            if (tw > 0)
                return tw;
            throw x;
        }
    }

    @Override
    public long transferTo(long position, long count, WritableByteChannel target)
        throws IOException
    {
        ensureOpen();
        if (!target.isOpen())
            throw new ClosedChannelException();
        if (!readable)
            throw new NonReadableChannelException();
        if (target instanceof FileChannelImpl && !((FileChannelImpl) target).writable)
            throw new NonWritableChannelException();
        if ((position < 0) || (count < 0))
            throw new IllegalArgumentException();

        try {
            final long sz = size();
            if (position > sz)
                return 0;

            // System calls supporting fast transfers might not work on files
            // which advertise zero size such as those in Linux /proc
            if (sz > 0) {
                // Now sz > 0 and position <= sz so remaining >= 0 and
                // remaining == 0 if and only if sz == position
                long remaining = sz - position;

                if (remaining >= 0 && remaining < count)
                    count = remaining;

                // Attempt a direct transfer, if the kernel supports it,
                // limiting the number of bytes according to which platform
                int icount = (int) Math.min(count, nd.maxDirectTransferSize());
                long n;
                if ((n = transferToDirect(position, icount, target)) >= 0)
                    return n;

                // Attempt a mapped transfer, but only to trusted channel types
                if ((n = transferToTrustedChannel(position, count, target)) >= 0)
                    return n;
            }

            // fallback to read/write loop
            return transferToArbitraryChannel(position, count, target);
        } catch (ClosedChannelException e) {
            // throw AsynchronousCloseException or ClosedByInterruptException
            throw transferFailed(e, target);
        }
    }

    /**
     * Transfers bytes into this channel's file from the resource that the given file
     * descriptor is connected to.
     */
    private long transferFromFileDescriptor(FileDescriptor srcFD, long position, long count) {
        long n;
        boolean append = fdAccess.getAppend(fd);
        do {
            n = nd.transferFrom(srcFD, fd, position, count, append);
        } while ((n == IOStatus.INTERRUPTED) && isOpen());
        return n;
    }

    /**
     * Transfers bytes into this channel's file from the given channel's file. This
     * implementation uses copy_file_range or equivalent.
     */
    private long transferFromDirect(FileChannelImpl src, long position, long count)
        throws IOException
    {
        if (transferFromDirectNotSupported)
            return IOStatus.UNSUPPORTED;

        final FileChannelImpl target = this;
        boolean completed = false;
        try {
            beginBlocking();
            int srcIndex = src.beforeTransfer();
            try {
                int targetIndex = target.beforeTransfer();
                try {
                    long n = transferFromFileDescriptor(src.fd, position, count);
                    if (n == IOStatus.UNSUPPORTED) {
                        transferFromDirectNotSupported = true;
                        return IOStatus.UNSUPPORTED;
                    }
                    completed = (n >= 0);
                    return IOStatus.normalize(n);
                } finally {
                    target.afterTransfer(completed, targetIndex);
                }
            } finally {
                src.afterTransfer(completed, srcIndex);
            }
        } finally {
            endBlocking(completed);
        }
    }

    /**
     * Transfers bytes into this channel's file from the given channel's file. This
     * implementation memory maps the given channel's file.
     */
    private long transferFromFileChannel(FileChannelImpl src, long position, long count)
        throws IOException
    {
        if (count < MAPPED_TRANSFER_THRESHOLD)
            return IOStatus.UNSUPPORTED_CASE;

        synchronized (src.positionLock) {
            long pos = src.position();
            long max = Math.min(count, src.size() - pos);

            if (src == this
                    && (position() - max + 1 <= pos)
                    && (pos - max + 1 <= position())
                    && !nd.canTransferToFromOverlappedMap()) {
                return IOStatus.UNSUPPORTED_CASE;
            }

            long remaining = max;
            long p = pos;
            while (remaining > 0L) {
                long size = Math.min(remaining, MAPPED_TRANSFER_SIZE);
                MappedByteBuffer bb = src.map(MapMode.READ_ONLY, p, size);
                try {
                    long n = write(bb, position);
                    assert n > 0;
                    p += n;
                    position += n;
                    remaining -= n;
                } catch (IOException ioe) {
                    // Only throw exception if no bytes have been written
                    if (remaining == max)
                        throw ioe;
                    break;
                } finally {
                    unmap(bb);
                }
            }
            long nwritten = max - remaining;
            src.position(pos + nwritten);
            return nwritten;
        }
    }

    private static final int TRANSFER_SIZE = 8192;

    /**
     * Transfers bytes into this channel's file from the given channel.
     */
    private long transferFromArbitraryChannel(ReadableByteChannel src,
                                              long position, long count)
        throws IOException
    {
        int c = (int) Math.min(count, TRANSFER_SIZE);
        ByteBuffer bb = ByteBuffer.allocate(c);
        long tw = 0;                    // Total bytes written
        long pos = position;
        try {
            while (tw < count) {
                bb.limit((int) Math.min((count - tw), TRANSFER_SIZE));
                // read may block, closing this channel will not wake it up
                int nr = src.read(bb);
                if (nr <= 0)
                    break;
                bb.flip();
                int nw = write(bb, pos);
                tw += nw;
                if (nw != nr)
                    break;
                pos += nw;
                bb.clear();
            }
            return tw;
        } catch (IOException x) {
            if (tw > 0)
                return tw;
            throw x;
        }
    }

    @Override
    public long transferFrom(ReadableByteChannel src, long position, long count)
        throws IOException
    {
        ensureOpen();
        if (!src.isOpen())
            throw new ClosedChannelException();
        if (src instanceof FileChannelImpl fci && !fci.readable)
            throw new NonReadableChannelException();
        if (!writable)
            throw new NonWritableChannelException();
        if ((position < 0) || (count < 0))
            throw new IllegalArgumentException();

        try {
            // System calls supporting fast transfers might not work on files
            // which advertise zero size such as those in Linux /proc
            if (src instanceof FileChannelImpl fci && fci.size() > 0) {
                long n;
                if ((n = transferFromDirect(fci, position, count)) >= 0)
                    return n;
                if ((n = transferFromFileChannel(fci, position, count)) >= 0)
                    return n;
            }

            // fallback to read/write loop
            return transferFromArbitraryChannel(src, position, count);
        } catch (ClosedChannelException e) {
            // throw AsynchronousCloseException or ClosedByInterruptException
            throw transferFailed(e, src);
        }
    }

    @Override
    public int read(ByteBuffer dst, long position) throws IOException {
        if (jfrTracing && FileReadEvent.enabled()) {
            return traceImplRead(dst, position);
        }
        return implRead(dst, position);
    }

    private int implRead(ByteBuffer dst, long position) throws IOException {
        if (dst == null)
            throw new NullPointerException();
        if (position < 0)
            throw new IllegalArgumentException("Negative position");
        ensureOpen();
        if (!readable)
            throw new NonReadableChannelException();
        if (direct)
            Util.checkChannelPositionAligned(position, alignment);
        if (nd.needsPositionLock()) {
            synchronized (positionLock) {
                return readInternal(dst, position);
            }
        } else {
            return readInternal(dst, position);
        }
    }

    private int traceImplRead(ByteBuffer dst, long position) throws IOException {
        int bytesRead = 0;
        long start = FileReadEvent.timestamp();
        try {
            bytesRead = implRead(dst, position);
        } finally {
            FileReadEvent.offer(start, path, bytesRead);
        }
        return bytesRead;
    }

    private int readInternal(ByteBuffer dst, long position) throws IOException {
        assert !nd.needsPositionLock() || Thread.holdsLock(positionLock);
        int n = 0;
        int ti = -1;

        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return -1;
            do {
                boolean attempted = Blocker.begin(direct);
                try {
                    n = IOUtil.read(fd, dst, position, direct, alignment, nd);
                } finally {
                    Blocker.end(attempted);
                }
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            endBlocking(n > 0);
            assert IOStatus.check(n);
        }
    }

    @Override
    public int write(ByteBuffer src, long position) throws IOException {
        if (jfrTracing && FileReadEvent.enabled()) {
            return traceImplWrite(src, position);
        }
        return implWrite(src, position);
    }

    private int implWrite(ByteBuffer src, long position) throws IOException {
        if (src == null)
            throw new NullPointerException();
        if (position < 0)
            throw new IllegalArgumentException("Negative position");
        ensureOpen();
        if (!writable)
            throw new NonWritableChannelException();
        if (direct)
            Util.checkChannelPositionAligned(position, alignment);
        if (nd.needsPositionLock()) {
            synchronized (positionLock) {
                return writeInternal(src, position);
            }
        } else {
            return writeInternal(src, position);
        }
    }

    private int traceImplWrite(ByteBuffer src, long position) throws IOException {
        int bytesWritten = 0;
        long start = FileWriteEvent.timestamp();
        try {
            bytesWritten = implWrite(src, position);
        } finally {
            FileWriteEvent.offer(start, path, bytesWritten);
        }
        return bytesWritten;
    }

    private int writeInternal(ByteBuffer src, long position) throws IOException {
        assert !nd.needsPositionLock() || Thread.holdsLock(positionLock);
        int n = 0;
        int ti = -1;
        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return -1;
            do {
                boolean attempted = Blocker.begin(sync || direct);
                try {
                    n = IOUtil.write(fd, src, position, direct, alignment, nd);
                } finally {
                    Blocker.end(attempted);
                }
            } while ((n == IOStatus.INTERRUPTED) && isOpen());
            return IOStatus.normalize(n);
        } finally {
            threads.remove(ti);
            endBlocking(n > 0);
            assert IOStatus.check(n);
        }
    }


    // -- Memory-mapped buffers --

    private abstract static sealed class Unmapper
        implements Runnable, UnmapperProxy
    {
        private final long address;
        protected final long size;
        protected final long cap;
        private final FileDescriptor fd;
        private final int pagePosition;

        private Unmapper(long address, long size, long cap,
                         FileDescriptor fd, int pagePosition)
        {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.cap = cap;
            this.fd = fd;
            this.pagePosition = pagePosition;
        }

        @Override
        public long address() {
            return address + pagePosition;
        }

        @Override
        public FileDescriptor fileDescriptor() {
            return fd;
        }

        @Override
        public void run() {
            unmap();
        }

        public long capacity() {
            return cap;
        }

        public void unmap() {
            nd.unmap(address, size);

            // if this mapping has a valid file descriptor then we close it
            if (fd.valid()) {
                try {
                    nd.close(fd);
                } catch (IOException ignore) {
                    // nothing we can do
                }
            }

            decrementStats();
        }
        protected abstract void incrementStats();
        protected abstract void decrementStats();
    }

    private static final class DefaultUnmapper extends Unmapper {

        // keep track of non-sync mapped buffer usage
        static volatile int count;
        static volatile long totalSize;
        static volatile long totalCapacity;

        public DefaultUnmapper(long address, long size, long cap,
                               FileDescriptor fd, int pagePosition) {
            super(address, size, cap, fd, pagePosition);
            incrementStats();
        }

        protected void incrementStats() {
            synchronized (DefaultUnmapper.class) {
                count++;
                totalSize += size;
                totalCapacity += cap;
            }
        }
        protected void decrementStats() {
            synchronized (DefaultUnmapper.class) {
                count--;
                totalSize -= size;
                totalCapacity -= cap;
            }
        }

        public boolean isSync() {
            return false;
        }
    }

    private static final class SyncUnmapper extends Unmapper {

        // keep track of mapped buffer usage
        static volatile int count;
        static volatile long totalSize;
        static volatile long totalCapacity;

        public SyncUnmapper(long address, long size, long cap,
                            FileDescriptor fd, int pagePosition) {
            super(address, size, cap, fd, pagePosition);
            incrementStats();
        }

        protected void incrementStats() {
            synchronized (SyncUnmapper.class) {
                count++;
                totalSize += size;
                totalCapacity += cap;
            }
        }
        protected void decrementStats() {
            synchronized (SyncUnmapper.class) {
                count--;
                totalSize -= size;
                totalCapacity -= cap;
            }
        }

        public boolean isSync() {
            return true;
        }
    }

    private static void unmap(MappedByteBuffer bb) {
        Cleaner cl = ((DirectBuffer)bb).cleaner();
        if (cl != null)
            cl.clean();
    }

    private static final int MAP_INVALID = -1;
    private static final int MAP_RO = 0;
    private static final int MAP_RW = 1;
    private static final int MAP_PV = 2;

    @Override
    public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException {
        if (size > Integer.MAX_VALUE)
            throw new IllegalArgumentException("Size exceeds Integer.MAX_VALUE");
        boolean isSync = isSync(Objects.requireNonNull(mode, "Mode is null"));
        int prot = toProt(mode);
        Unmapper unmapper = mapInternal(mode, position, size, prot, isSync);
        if (unmapper == null) {
            // a valid file descriptor is not required
            FileDescriptor dummy = new FileDescriptor();
            if ((!writable) || (prot == MAP_RO))
                return Util.newMappedByteBufferR(0, 0, dummy, null, isSync);
            else
                return Util.newMappedByteBuffer(0, 0, dummy, null, isSync);
        } else if ((!writable) || (prot == MAP_RO)) {
            return Util.newMappedByteBufferR((int)unmapper.capacity(),
                    unmapper.address(),
                    unmapper.fileDescriptor(),
                    unmapper, unmapper.isSync());
        } else {
            return Util.newMappedByteBuffer((int)unmapper.capacity(),
                    unmapper.address(),
                    unmapper.fileDescriptor(),
                    unmapper, unmapper.isSync());
        }
    }

    @Override
    public MemorySegment map(MapMode mode, long offset, long size, Arena arena)
        throws IOException
    {
        Objects.requireNonNull(mode,"Mode is null");
        Objects.requireNonNull(arena, "Arena is null");
        MemorySessionImpl sessionImpl = MemorySessionImpl.toMemorySession(arena);
        sessionImpl.checkValidState();
        if (offset < 0)
            throw new IllegalArgumentException("Requested bytes offset must be >= 0.");
        if (size < 0)
            throw new IllegalArgumentException("Requested bytes size must be >= 0.");

        boolean isSync = isSync(mode);
        int prot = toProt(mode);
        Unmapper unmapper = mapInternal(mode, offset, size, prot, isSync);
        boolean readOnly = false;
        if (mode == MapMode.READ_ONLY) {
            readOnly = true;
        }
        return SegmentFactories.mapSegment(size, unmapper, readOnly, sessionImpl);
    }

    private Unmapper mapInternal(MapMode mode, long position, long size, int prot, boolean isSync)
        throws IOException
    {
        ensureOpen();
        if (mode == null)
            throw new NullPointerException("Mode is null");
        if (position < 0L)
            throw new IllegalArgumentException("Negative position");
        if (size < 0L)
            throw new IllegalArgumentException("Negative size");
        if (position + size < 0)
            throw new IllegalArgumentException("Position + size overflow");

        checkMode(mode, prot, isSync);
        long addr = -1;
        int ti = -1;
        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return null;

            long mapSize;
            int pagePosition;
            synchronized (positionLock) {
                long filesize;
                do {
                    filesize = nd.size(fd);
                } while ((filesize == IOStatus.INTERRUPTED) && isOpen());
                if (!isOpen())
                    return null;

                if (filesize < position + size) { // Extend file size
                    if (!writable) {
                        throw new IOException("Channel not open for writing " +
                            "- cannot extend file to required size");
                    }
                    int rv;
                    do {
                        rv = nd.truncate(fd, position + size);
                    } while ((rv == IOStatus.INTERRUPTED) && isOpen());
                    if (!isOpen())
                        return null;
                }

                if (size == 0) {
                    return null;
                }

                pagePosition = (int)(position % nd.allocationGranularity());
                long mapPosition = position - pagePosition;
                mapSize = size + pagePosition;
                try {
                    // If map did not throw an exception, the address is valid
                    addr = nd.map(fd, prot, mapPosition, mapSize, isSync);
                } catch (OutOfMemoryError x) {
                    // An OutOfMemoryError may indicate that we've exhausted
                    // memory so force gc and re-attempt map
                    System.gc();
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException y) {
                        Thread.currentThread().interrupt();
                    }
                    try {
                        addr = nd.map(fd, prot, mapPosition, mapSize, isSync);
                    } catch (OutOfMemoryError y) {
                        // After a second OOME, fail
                        throw new IOException("Map failed", y);
                    }
                }
            } // synchronized

            // On Windows, and potentially other platforms, we need an open
            // file descriptor for some mapping operations.
            FileDescriptor mfd;
            try {
                mfd = nd.duplicateForMapping(fd);
            } catch (IOException ioe) {
                nd.unmap(addr, mapSize);
                throw ioe;
            }

            assert (IOStatus.checkAll(addr));
            assert (addr % nd.allocationGranularity() == 0);
            Unmapper um = (isSync
                ? new SyncUnmapper(addr, mapSize, size, mfd, pagePosition)
                : new DefaultUnmapper(addr, mapSize, size, mfd, pagePosition));
            return um;
        } finally {
            threads.remove(ti);
            endBlocking(IOStatus.checkAll(addr));
        }
    }

    private boolean isSync(MapMode mode) {
        // Do not want to initialize ExtendedMapMode until
        // after the module system has been initialized
        return !VM.isModuleSystemInited() ? false :
            (mode == ExtendedMapMode.READ_ONLY_SYNC ||
                mode == ExtendedMapMode.READ_WRITE_SYNC);
    }

    private int toProt(MapMode mode) {
        int prot;
        if (mode == MapMode.READ_ONLY) {
            prot = MAP_RO;
        } else if (mode == MapMode.READ_WRITE) {
            prot = MAP_RW;
        } else if (mode == MapMode.PRIVATE) {
            prot = MAP_PV;
        } else if (mode == ExtendedMapMode.READ_ONLY_SYNC) {
            prot = MAP_RO;
        } else if (mode == ExtendedMapMode.READ_WRITE_SYNC) {
            prot = MAP_RW;
        } else {
            prot = MAP_INVALID;
        }
        return prot;
    }

    private void checkMode(MapMode mode, int prot, boolean isSync) {
        if (prot == MAP_INVALID) {
            throw new UnsupportedOperationException();
        }
        if ((mode != MapMode.READ_ONLY) && mode != ExtendedMapMode.READ_ONLY_SYNC && !writable)
            throw new NonWritableChannelException();
        if (!readable)
            throw new NonReadableChannelException();
        // reject SYNC request if writeback is not enabled for this platform
        if (isSync && !Unsafe.isWritebackEnabled()) {
            throw new UnsupportedOperationException();
        }
    }

    /**
     * Invoked by sun.management.ManagementFactoryHelper to create the management
     * interface for mapped buffers.
     */
    public static BufferPool getMappedBufferPool() {
        return new BufferPool() {
            @Override
            public String getName() {
                return "mapped";
            }
            @Override
            public long getCount() {
                return DefaultUnmapper.count;
            }
            @Override
            public long getTotalCapacity() {
                return DefaultUnmapper.totalCapacity;
            }
            @Override
            public long getMemoryUsed() {
                return DefaultUnmapper.totalSize;
            }
        };
    }

    /**
     * Invoked by sun.management.ManagementFactoryHelper to create the management
     * interface for sync mapped buffers.
     */
    public static BufferPool getSyncMappedBufferPool() {
        return new BufferPool() {
            @Override
            public String getName() {
                return "mapped - 'non-volatile memory'";
            }
            @Override
            public long getCount() {
                return SyncUnmapper.count;
            }
            @Override
            public long getTotalCapacity() {
                return SyncUnmapper.totalCapacity;
            }
            @Override
            public long getMemoryUsed() {
                return SyncUnmapper.totalSize;
            }
        };
    }

    // -- Locks --

    // keeps track of locks on this file
    private volatile FileLockTable fileLockTable;

    private FileLockTable fileLockTable() throws IOException {
        if (fileLockTable == null) {
            synchronized (this) {
                if (fileLockTable == null) {
                    int ti = threads.add();
                    try {
                        ensureOpen();
                        fileLockTable = new FileLockTable(this, fd);
                    } finally {
                        threads.remove(ti);
                    }
                }
            }
        }
        return fileLockTable;
    }

    @Override
    public FileLock lock(long position, long size, boolean shared)
        throws IOException
    {
        ensureOpen();
        if (shared && !readable)
            throw new NonReadableChannelException();
        if (!shared && !writable)
            throw new NonWritableChannelException();
        if (size == 0)
            size = Long.MAX_VALUE - Math.max(0, position);
        FileLockImpl fli = new FileLockImpl(this, position, size, shared);
        FileLockTable flt = fileLockTable();
        flt.add(fli);
        boolean completed = false;
        int ti = -1;
        try {
            beginBlocking();
            ti = threads.add();
            if (!isOpen())
                return null;
            int n;
            do {
                boolean attempted = Blocker.begin();
                try {
                    n = nd.lock(fd, true, position, size, shared);
                } finally {
                    Blocker.end(attempted);
                }
            } while ((n == FileDispatcher.INTERRUPTED) && isOpen());
            if (isOpen()) {
                if (n == FileDispatcher.RET_EX_LOCK) {
                    assert shared;
                    FileLockImpl fli2 = new FileLockImpl(this, position, size,
                                                         false);
                    flt.replace(fli, fli2);
                    fli = fli2;
                }
                completed = true;
            }
        } finally {
            if (!completed)
                flt.remove(fli);
            threads.remove(ti);
            try {
                endBlocking(completed);
            } catch (ClosedByInterruptException e) {
                throw new FileLockInterruptionException();
            }
        }
        return fli;
    }

    @Override
    public FileLock tryLock(long position, long size, boolean shared)
        throws IOException
    {
        ensureOpen();
        if (shared && !readable)
            throw new NonReadableChannelException();
        if (!shared && !writable)
            throw new NonWritableChannelException();
        if (size == 0)
            size = Long.MAX_VALUE - Math.max(0, position);
        FileLockImpl fli = new FileLockImpl(this, position, size, shared);
        FileLockTable flt = fileLockTable();
        flt.add(fli);
        int result;

        int ti = threads.add();
        try {
            try {
                ensureOpen();
                result = nd.lock(fd, false, position, size, shared);
            } catch (IOException e) {
                flt.remove(fli);
                throw e;
            }
            if (result == FileDispatcher.NO_LOCK) {
                flt.remove(fli);
                return null;
            }
            if (result == FileDispatcher.RET_EX_LOCK) {
                assert shared;
                FileLockImpl fli2 = new FileLockImpl(this, position, size,
                                                     false);
                flt.replace(fli, fli2);
                return fli2;
            }
            return fli;
        } finally {
            threads.remove(ti);
        }
    }

    void release(FileLockImpl fli) throws IOException {
        int ti = threads.add();
        try {
            ensureOpen();
            nd.release(fd, fli.position(), fli.size());
        } finally {
            threads.remove(ti);
        }
        assert fileLockTable != null;
        fileLockTable.remove(fli);
    }
}
