/*
 *  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.tomcat.util.net;

import java.io.EOFException;
import java.io.IOException;
import java.net.SocketTimeoutException;
import java.nio.ByteBuffer;
import java.nio.channels.CompletionHandler;
import java.nio.channels.InterruptedByTimeoutException;
import java.nio.channels.ReadPendingException;
import java.nio.channels.WritePendingException;
import java.util.concurrent.Executor;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import jakarta.servlet.ServletConnection;

import org.apache.juli.logging.Log;
import org.apache.juli.logging.LogFactory;
import org.apache.tomcat.util.ExceptionUtils;
import org.apache.tomcat.util.res.StringManager;

/**
 * Base class for wrapping a socket connection.
 * @param <E> the socket type
 */
public abstract class SocketWrapperBase<E> {

    private static final Log log = LogFactory.getLog(SocketWrapperBase.class);

    /** String manager for this class. */
    protected static final StringManager sm = StringManager.getManager(SocketWrapperBase.class);

    /*
     * At 100,000 connections a second there are enough IDs here for ~3,000,000 years before it overflows (and then we
     * have another 3,000,000 years before it gets back to zero).
     *
     * Local testing shows that 5 threads can obtain 60,000,000+ IDs a second from a single AtomicLong. That is about
     * 17ns per request. It does not appear that the introduction of this counter will cause a bottleneck for connection
     * processing.
     */
    private static final AtomicLong connectionIdGenerator = new AtomicLong(0);

    private E socket;
    private final AbstractEndpoint<E,?> endpoint;
    private final Lock lock = new ReentrantLock();

    /** Indicates whether the socket has been closed. */
    protected final AtomicBoolean closed = new AtomicBoolean(false);

    // Volatile because I/O and setting the timeout values occurs on a different
    // thread to the thread checking the timeout.
    /** Read timeout in milliseconds. */
    private volatile long readTimeout = -1;
    /** Write timeout in milliseconds. */
    private volatile long writeTimeout = -1;

    /** The previous I/O exception that occurred on this socket. */
    protected volatile IOException previousIOException = null;

    private volatile int keepAliveLeft = 100;
    private String negotiatedProtocol = null;

    private final String connectionId;

    /*
     * Following cached for speed / reduced GC
     */
    /** Local address. */
    protected String localAddr = null;
    /** Local name. */
    protected String localName = null;
    /** Local port. */
    protected int localPort = -1;
    /** Remote address. */
    protected String remoteAddr = null;
    /** Remote host. */
    protected String remoteHost = null;
    /** Remote port. */
    protected int remotePort = -1;
    /** Servlet connection. */
    protected volatile ServletConnection servletConnection = null;

    /** SNI host name. */
    protected String sniHostName = null;

    /**
     * Used to record the first IOException that occurs during non-blocking read/writes that can't be usefully
     * propagated up the stack since there is no user code or appropriate container code in the stack to handle it.
     */
    private volatile IOException error = null;

    /**
     * The buffers used for communicating with the socket.
     */
    protected volatile SocketBufferHandler socketBufferHandler = null;

    /**
     * The max size of the individual buffered write buffers
     */
    protected int bufferedWriteSize = 64 * 1024; // 64k default write buffer

    /**
     * Additional buffer used for non-blocking writes. Non-blocking writes need to return immediately even if the data
     * cannot be written immediately but the socket buffer may not be big enough to hold all of the unwritten data. This
     * structure provides an additional buffer to hold the data until it can be written. Not that while the Servlet API
     * only allows one non-blocking write at a time, due to buffering and the possible need to write HTTP headers, this
     * layer may see multiple writes.
     */
    protected final WriteBuffer nonBlockingWriteBuffer = new WriteBuffer(bufferedWriteSize);

    /*
     * Asynchronous operations.
     */
    /** Semaphore for pending read operations. */
    protected final Semaphore readPending;
    /** Current read operation state. */
    protected volatile OperationState<?> readOperation = null;
    /** Semaphore for pending write operations. */
    protected final Semaphore writePending;
    /** Current write operation state. */
    protected volatile OperationState<?> writeOperation = null;

    /**
     * The org.apache.coyote.Processor instance currently associated with the wrapper. Only populated when required to
     * maintain wrapper<->Processor mapping between calls to
     * {@link AbstractEndpoint.Handler#process(SocketWrapperBase, SocketEvent)}.
     */
    private final AtomicReference<Object> currentProcessor = new AtomicReference<>();

    /**
     * Creates a new socket wrapper.
     * @param socket the socket to wrap
     * @param endpoint the endpoint
     */
    public SocketWrapperBase(E socket, AbstractEndpoint<E,?> endpoint) {
        this.socket = socket;
        this.endpoint = endpoint;
        if (endpoint.getUseAsyncIO() || needSemaphores()) {
            readPending = new Semaphore(1);
            writePending = new Semaphore(1);
        } else {
            readPending = null;
            writePending = null;
        }
        connectionId = Long.toHexString(connectionIdGenerator.getAndIncrement());
    }

    /**
     * Returns the wrapped socket.
     * @return the socket
     */
    public E getSocket() {
        return socket;
    }

   /**
     * Resets the wrapped socket with a new socket instance.
     *
     * @param closedSocket the new socket to wrap
     */
    protected void reset(E closedSocket) {
        socket = closedSocket;
    }

    /**
     * Returns the endpoint.
     * @return the endpoint
     */
    protected AbstractEndpoint<E,?> getEndpoint() {
        return endpoint;
    }

    /**
     * Returns the lock for this wrapper.
     * @return the lock
     */
    public Lock getLock() {
        return lock;
    }

    /**
     * Returns the current processor.
     * @return the current processor
     */
    public Object getCurrentProcessor() {
        return currentProcessor.get();
    }

    /**
     * Sets the current processor associated with this wrapper.
     *
     * @param currentProcessor the processor to associate
     */
    public void setCurrentProcessor(Object currentProcessor) {
        this.currentProcessor.set(currentProcessor);
    }

    /**
     * Retrieves and clears the current processor associated with this wrapper.
     *
     * @return the previous processor, or {@code null} if none was set
     */
    public Object takeCurrentProcessor() {
        return currentProcessor.getAndSet(null);
    }

    /**
     * Transfers processing to a container thread.
     *
     * @param runnable The actions to process on a container thread
     *
     * @throws RejectedExecutionException If the runnable cannot be executed
     */
    public void execute(Runnable runnable) {
        Executor executor = endpoint.getExecutor();
        if (!endpoint.isRunning() || executor == null) {
            throw new RejectedExecutionException();
        }
        executor.execute(runnable);
    }

    /**
     * Returns the first I/O error that occurred on this socket.
     *
     * @return the recorded error, or {@code null} if no error has occurred
     */
    public IOException getError() {
        return error;
    }

    /**
     * Records the first I/O error that occurs on this socket. Subsequent calls are ignored.
     *
     * @param error the error to record
     */
    public void setError(IOException error) {
        // Not perfectly thread-safe but good enough. Just needs to ensure that
        // once this.error is non-null, it can never be null.
        if (this.error != null) {
            return;
        }
        this.error = error;
    }

    /**
     * Throws the recorded error if one has occurred.
     *
     * @throws IOException if an error was previously recorded
     */
    public void checkError() throws IOException {
        if (error != null) {
            throw error;
        }
    }

    /**
     * Returns the protocol negotiated for this connection.
     *
     * @return the negotiated protocol, or {@code null} if none has been negotiated
     */
    public String getNegotiatedProtocol() {
        return negotiatedProtocol;
    }

    /**
     * Sets the protocol negotiated for this connection.
     *
     * @param negotiatedProtocol the negotiated protocol
     */
    public void setNegotiatedProtocol(String negotiatedProtocol) {
        this.negotiatedProtocol = negotiatedProtocol;
    }

    /**
     * Returns the SNI host name.
     *
     * @return the SNI host name
     */
    public String getSniHostName() {
        return this.sniHostName;
    }

    /**
     * Sets the SNI host name.
     *
     * @param sniHostName the SNI host name to set
     */
    public void setSniHostName(String sniHostName) {
        this.sniHostName = sniHostName;
    }

    /**
     * Set the timeout for reading. Values of zero or less will be changed to -1.
     *
     * @param readTimeout The timeout in milliseconds. A value of -1 indicates an infinite timeout.
     */
    public void setReadTimeout(long readTimeout) {
        if (readTimeout > 0) {
            this.readTimeout = readTimeout;
        } else {
            this.readTimeout = -1;
        }
    }

    /**
     * Returns the read timeout in milliseconds.
     *
     * @return the read timeout, or -1 for infinite timeout
     */
    public long getReadTimeout() {
        return this.readTimeout;
    }

    /**
     * Set the timeout for writing. Values of zero or less will be changed to -1.
     *
     * @param writeTimeout The timeout in milliseconds. A value of zero or less indicates an infinite timeout.
     */
    public void setWriteTimeout(long writeTimeout) {
        if (writeTimeout > 0) {
            this.writeTimeout = writeTimeout;
        } else {
            this.writeTimeout = -1;
        }
    }

    /**
     * Returns the write timeout in milliseconds.
     *
     * @return the write timeout, or -1 for infinite timeout
     */
    public long getWriteTimeout() {
        return this.writeTimeout;
    }


    /**
     * Sets the number of keep-alive requests remaining.
     *
     * @param keepAliveLeft the number of keep-alive requests remaining
     */
    public void setKeepAliveLeft(int keepAliveLeft) {
        this.keepAliveLeft = keepAliveLeft;
    }

    /**
     * Decrements the number of keep-alive requests remaining.
     *
     * @return the updated number of keep-alive requests remaining
     */
    public int decrementKeepAlive() {
        return (--keepAliveLeft);
    }

    /**
     * Returns the remote host name.
     *
     * @return the remote host name
     */
    public String getRemoteHost() {
        if (remoteHost == null) {
            populateRemoteHost();
        }
        return remoteHost;
    }

  /**
     * Populates the cached remote host name from the underlying socket.
     */
    protected abstract void populateRemoteHost();

    /**
     * Returns the remote IP address.
     *
     * @return the remote IP address
     */
    public String getRemoteAddr() {
        if (remoteAddr == null) {
            populateRemoteAddr();
        }
        return remoteAddr;
    }

  /**
     * Populates the cached remote IP address from the underlying socket.
     */
    protected abstract void populateRemoteAddr();

    /**
     * Returns the remote port number.
     *
     * @return the remote port number
     */
    public int getRemotePort() {
        if (remotePort == -1) {
            populateRemotePort();
        }
        return remotePort;
    }

 /**
     * Populates the cached remote port number from the underlying socket.
     */
    protected abstract void populateRemotePort();

    /**
     * Returns the local host name.
     *
     * @return the local host name
     */
    public String getLocalName() {
        if (localName == null) {
            populateLocalName();
        }
        return localName;
    }

/**
     * Populates the cached local host name from the underlying socket.
     */
    protected abstract void populateLocalName();

    /**
     * Returns the local IP address.
     *
     * @return the local IP address
     */
    public String getLocalAddr() {
        if (localAddr == null) {
            populateLocalAddr();
        }
        return localAddr;
    }

/**
     * Populates the cached local IP address from the underlying socket.
     */
    protected abstract void populateLocalAddr();

    /**
     * Returns the local port number.
     *
     * @return the local port number
     */
    public int getLocalPort() {
        if (localPort == -1) {
            populateLocalPort();
        }
        return localPort;
    }

/**
     * Populates the cached local port number from the underlying socket.
     */
    protected abstract void populateLocalPort();

    /**
     * Returns the socket buffer handler.
     *
     * @return the socket buffer handler
     */
    public SocketBufferHandler getSocketBufferHandler() {
        return socketBufferHandler;
    }

    /**
     * Checks if there is data available to read from the socket.
     *
     * @return {@code true} since it is always safe to attempt a read
     */
    public boolean hasDataToRead() {
        // Return true because it is always safe to make a read attempt
        return true;
    }

    /**
     * Checks if there is data waiting to be written to the socket.
     *
     * @return {@code true} if there is pending write data
     */
    public boolean hasDataToWrite() {
        return (socketBufferHandler != null && !socketBufferHandler.isWriteBufferEmpty()) || !nonBlockingWriteBuffer.isEmpty();
    }

    /**
     * Checks to see if there are any writes pending and if there are calls {@link #registerWriteInterest()} to trigger
     * a callback once the pending writes have completed.
     * <p>
     * Note: Once this method has returned <code>false</code> it <b>MUST NOT</b> be called again until the pending write
     * has completed and the callback has been fired. TODO: Modify {@link #registerWriteInterest()} so the above
     * restriction is enforced there rather than relying on the caller.
     *
     * @return <code>true</code> if no writes are pending and data can be written otherwise <code>false</code>
     */
    public boolean isReadyForWrite() {
        boolean result = canWrite();
        if (!result) {
            registerWriteInterest();
        }
        return result;
    }


    /**
     * Checks if the socket write buffer is writable and there is no pending non-blocking write data.
     *
     * @return {@code true} if the socket is ready for writing
     */
    public boolean canWrite() {
        if (socketBufferHandler == null) {
            throw new IllegalStateException(sm.getString("socket.closed"));
        }
        return socketBufferHandler.isWriteBufferWritable() && nonBlockingWriteBuffer.isEmpty();
    }


    /**
     * Overridden for debug purposes. No guarantees are made about the format of this message which may vary
     * significantly between point releases.
     * <p>
     * {@inheritDoc}
     */
    @Override
    public String toString() {
        return super.toString() + ":" + String.valueOf(socket);
    }


    /**
     * Reads data from the socket into a byte array.
     *
     * @param block Whether to block
     * @param b The byte array to read into
     * @param off The offset in the array
     * @param len The number of bytes to read
     *
     * @return the number of bytes read
     *
     * @throws IOException If an I/O error occurs
     */
    public abstract int read(boolean block, byte[] b, int off, int len) throws IOException;

    /**
     * Reads data from the socket into a ByteBuffer.
     *
     * @param block Whether to block
     * @param to The ByteBuffer to read into
     *
     * @return the number of bytes read
     *
     * @throws IOException If an I/O error occurs
     */
    public abstract int read(boolean block, ByteBuffer to) throws IOException;

    /**
     * Checks if the socket is ready for reading.
     *
     * @return {@code true} if data is available to read
     *
     * @throws IOException If an I/O error occurs
     */
    public abstract boolean isReadyForRead() throws IOException;

    /**
     * Sets the application-level read buffer handler.
     *
     * @param handler The application buffer handler
     */
    public abstract void setAppReadBufHandler(ApplicationBufferHandler handler);

    /**
     * Copies available data from the internal read buffer into the provided byte array.
     *
     * @param b the byte array to populate
     * @param off the offset in the array
     * @param len the maximum number of bytes to copy
     * @return the number of bytes copied
     */
    protected int populateReadBuffer(byte[] b, int off, int len) {
        socketBufferHandler.configureReadBufferForRead();
        ByteBuffer readBuffer = socketBufferHandler.getReadBuffer();
        int remaining = readBuffer.remaining();

        // Is there enough data in the read buffer to satisfy this request?
        // Copy what data there is in the read buffer to the byte array
        if (remaining > 0) {
            remaining = Math.min(remaining, len);
            readBuffer.get(b, off, remaining);

            if (log.isTraceEnabled()) {
                log.trace("Socket: [" + this + "], Read from buffer: [" + remaining + "]");
            }
        }
        return remaining;
    }


    /**
     * Copies available data from the internal read buffer into the provided ByteBuffer.
     *
     * @param to the ByteBuffer to populate
     * @return the number of bytes copied
     */
    protected int populateReadBuffer(ByteBuffer to) {
        // Is there enough data in the read buffer to satisfy this request?
        // Copy what data there is in the read buffer to the byte array
        socketBufferHandler.configureReadBufferForRead();
        int nRead = transfer(socketBufferHandler.getReadBuffer(), to);

        if (log.isTraceEnabled()) {
            log.trace("Socket: [" + this + "], Read from buffer: [" + nRead + "]");
        }
        return nRead;
    }


    /**
     * Return input that has been read to the input buffer for re-reading by the correct component. There are times when
     * a component may read more data than it needs before it passes control to another component. One example of this
     * is during HTTP upgrade. If an (arguably misbehaving client) sends data associated with the upgraded protocol
     * before the HTTP upgrade completes, the HTTP handler may read it. This method provides a way for that data to be
     * returned so it can be processed by the correct component.
     *
     * @param returnedInput The input to return to the input buffer.
     */
    public void unRead(ByteBuffer returnedInput) {
        if (returnedInput != null) {
            socketBufferHandler.unReadReadBuffer(returnedInput);
        }
    }


    /**
     * Close the socket wrapper.
     */
    public void close() {
        if (closed.compareAndSet(false, true)) {
            try {
                getEndpoint().getHandler().release(this);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                if (log.isDebugEnabled()) {
                    log.error(sm.getString("endpoint.debug.handlerRelease"), t);
                }
            } finally {
                getEndpoint().countDownConnection();
                doClose();
            }
        }
    }

    /**
     * Perform the actual close. The closed atomic boolean guarantees this will be called only once per wrapper.
     */
    protected abstract void doClose();

    /**
     * Checks if the socket wrapper has been closed.
     *
     * @return {@code true} if the wrapper has been closed
     */
    public boolean isClosed() {
        return closed.get();
    }


    /**
     * Writes the provided data to the socket write buffer. If the socket write buffer fills during the write, the
     * content of the socket write buffer is written to the network and this method starts to fill the socket write
     * buffer again. Depending on the size of the data to write, there may be multiple writes to the network.
     * <p>
     * Non-blocking writes must return immediately and the byte array holding the data to be written must be immediately
     * available for re-use. It may not be possible to write sufficient data to the network to allow this to happen. In
     * this case data that cannot be written to the network and cannot be held by the socket buffer is stored in the
     * non-blocking write buffer.
     * <p>
     * Note: There is an implementation assumption that, before switching from non-blocking writes to blocking writes,
     * any data remaining in the non-blocking write buffer will have been written to the network.
     *
     * @param block <code>true</code> if a blocking write should be used, otherwise a non-blocking write will be used
     * @param buf   The byte array containing the data to be written
     * @param off   The offset within the byte array of the data to be written
     * @param len   The length of the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    public final void write(boolean block, byte[] buf, int off, int len) throws IOException {
        if (len == 0 || buf == null) {
            return;
        }

        /*
         * While the implementations for blocking and non-blocking writes are very similar they have been split into
         * separate methods: - To allow subclasses to override them individually. NIO2, for example, overrides the
         * non-blocking write but not the blocking write. - To enable a marginally more efficient implemented for
         * blocking writes which do not require the additional checks related to the use of the non-blocking write
         * buffer
         */
        if (block) {
            writeBlocking(buf, off, len);
        } else {
            writeNonBlocking(buf, off, len);
        }
    }


    /**
     * Writes the provided data to the socket write buffer. If the socket write buffer fills during the write, the
     * content of the socket write buffer is written to the network and this method starts to fill the socket write
     * buffer again. Depending on the size of the data to write, there may be multiple writes to the network.
     * <p>
     * Non-blocking writes must return immediately and the ByteBuffer holding the data to be written must be immediately
     * available for re-use. It may not be possible to write sufficient data to the network to allow this to happen. In
     * this case data that cannot be written to the network and cannot be held by the socket buffer is stored in the
     * non-blocking write buffer.
     * <p>
     * Note: There is an implementation assumption that, before switching from non-blocking writes to blocking writes,
     * any data remaining in the non-blocking write buffer will have been written to the network.
     *
     * @param block <code>true</code> if a blocking write should be used, otherwise a non-blocking write will be used
     * @param from  The ByteBuffer containing the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    public final void write(boolean block, ByteBuffer from) throws IOException {
        if (from == null || from.remaining() == 0) {
            return;
        }

        /*
         * While the implementations for blocking and non-blocking writes are very similar they have been split into
         * separate methods: - To allow subclasses to override them individually. NIO2, for example, overrides the
         * non-blocking write but not the blocking write. - To enable a marginally more efficient implemented for
         * blocking writes which do not require the additional checks related to the use of the non-blocking write
         * buffer
         */
        if (block) {
            writeBlocking(from);
        } else {
            writeNonBlocking(from);
        }
    }


    /**
     * Writes the provided data to the socket write buffer. If the socket write buffer fills during the write, the
     * content of the socket write buffer is written to the network using a blocking write. Once that blocking write is
     * complete, this method starts to fill the socket write buffer again. Depending on the size of the data to write,
     * there may be multiple writes to the network. On completion of this method there will always be space remaining in
     * the socket write buffer.
     *
     * @param buf The byte array containing the data to be written
     * @param off The offset within the byte array of the data to be written
     * @param len The length of the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void writeBlocking(byte[] buf, int off, int len) throws IOException {
        if (len > 0) {
            socketBufferHandler.configureWriteBufferForWrite();
            int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer());
            len -= thisTime;
            while (len > 0) {
                off += thisTime;
                doWrite(true);
                socketBufferHandler.configureWriteBufferForWrite();
                thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer());
                len -= thisTime;
            }
        }
    }


    /**
     * Writes the provided data to the socket write buffer. If the socket write buffer fills during the write, the
     * content of the socket write buffer is written to the network using a blocking write. Once that blocking write is
     * complete, this method starts to fill the socket write buffer again. Depending on the size of the data to write,
     * there may be multiple writes to the network. On completion of this method there will always be space remaining in
     * the socket write buffer.
     *
     * @param from The ByteBuffer containing the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void writeBlocking(ByteBuffer from) throws IOException {
        if (from.hasRemaining()) {
            socketBufferHandler.configureWriteBufferForWrite();
            transfer(from, socketBufferHandler.getWriteBuffer());
            while (from.hasRemaining()) {
                doWrite(true);
                socketBufferHandler.configureWriteBufferForWrite();
                transfer(from, socketBufferHandler.getWriteBuffer());
            }
        }
    }


    /**
     * Transfers the data to the socket write buffer (writing that data to the socket if the buffer fills up using a
     * non-blocking write) until either all the data has been transferred and space remains in the socket write buffer
     * or a non-blocking write leaves data in the socket write buffer. After an incomplete write, any data remaining to
     * be transferred to the socket write buffer will be copied to the socket write buffer. If the remaining data is too
     * big for the socket write buffer, the socket write buffer will be filled and the additional data written to the
     * non-blocking write buffer.
     *
     * @param buf The byte array containing the data to be written
     * @param off The offset within the byte array of the data to be written
     * @param len The length of the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void writeNonBlocking(byte[] buf, int off, int len) throws IOException {
        if (len > 0 && nonBlockingWriteBuffer.isEmpty() && socketBufferHandler.isWriteBufferWritable()) {
            socketBufferHandler.configureWriteBufferForWrite();
            int thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer());
            len -= thisTime;
            while (len > 0) {
                off = off + thisTime;
                doWrite(false);
                if (socketBufferHandler.isWriteBufferWritable()) {
                    socketBufferHandler.configureWriteBufferForWrite();
                    thisTime = transfer(buf, off, len, socketBufferHandler.getWriteBuffer());
                } else {
                    // Didn't write any data in the last non-blocking write.
                    // Therefore, the write buffer will still be full. Nothing
                    // else to do here. Exit the loop.
                    break;
                }
                len -= thisTime;
            }
        }

        if (len > 0) {
            // Remaining data must be buffered
            nonBlockingWriteBuffer.add(buf, off, len);
        }
    }


    /**
     * Transfers the data to the socket write buffer (writing that data to the socket if the buffer fills up using a
     * non-blocking write) until either all the data has been transferred and space remains in the socket write buffer
     * or a non-blocking write leaves data in the socket write buffer. After an incomplete write, any data remaining to
     * be transferred to the socket write buffer will be copied to the socket write buffer. If the remaining data is too
     * big for the socket write buffer, the socket write buffer will be filled and the additional data written to the
     * non-blocking write buffer.
     *
     * @param from The ByteBuffer containing the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void writeNonBlocking(ByteBuffer from) throws IOException {

        if (from.hasRemaining() && nonBlockingWriteBuffer.isEmpty() && socketBufferHandler.isWriteBufferWritable()) {
            writeNonBlockingInternal(from);
        }

        if (from.hasRemaining()) {
            // Remaining data must be buffered
            nonBlockingWriteBuffer.add(from);
        }
    }


    /**
     * Separate method so it can be re-used by the socket write buffer to write data to the network
     *
     * @param from The ByteBuffer containing the data to be written
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void writeNonBlockingInternal(ByteBuffer from) throws IOException {
        socketBufferHandler.configureWriteBufferForWrite();
        transfer(from, socketBufferHandler.getWriteBuffer());
        while (from.hasRemaining()) {
            doWrite(false);
            if (socketBufferHandler.isWriteBufferWritable()) {
                socketBufferHandler.configureWriteBufferForWrite();
                transfer(from, socketBufferHandler.getWriteBuffer());
            } else {
                break;
            }
        }
    }


    /**
     * Writes as much data as possible from any that remains in the buffers.
     *
     * @param block <code>true</code> if a blocking write should be used, otherwise a non-blocking write will be used
     *
     * @return <code>true</code> if data remains to be flushed after this method completes, otherwise
     *             <code>false</code>. In blocking mode therefore, the return value should always be <code>false</code>
     *
     * @throws IOException If an IO error occurs during the write
     */
    public boolean flush(boolean block) throws IOException {
        boolean result = false;
        if (block) {
            // A blocking flush will always empty the buffer.
            flushBlocking();
        } else {
            result = flushNonBlocking();
        }

        return result;
    }


    /**
     * Writes all remaining data from the buffers and blocks until the write is complete.
     *
     * @throws IOException If an IO error occurs during the write
     */
    protected void flushBlocking() throws IOException {
        doWrite(true);

        if (!nonBlockingWriteBuffer.isEmpty()) {
            nonBlockingWriteBuffer.write(this, true);

            if (!socketBufferHandler.isWriteBufferEmpty()) {
                doWrite(true);
            }
        }

    }


 /**
     * Flushes remaining buffered data using a non-blocking write.
     *
     * @return {@code true} if data remains to be flushed after this method completes, otherwise {@code false}
     * @throws IOException If an IO error occurs during the write
     */
    protected abstract boolean flushNonBlocking() throws IOException;


    /**
     * Write the contents of the socketWriteBuffer to the socket. For blocking writes either then entire contents of the
     * buffer will be written or an IOException will be thrown. Partial blocking writes will not occur.
     *
     * @param block Should the write be blocking or not?
     *
     * @throws IOException If an I/O error such as a timeout occurs during the write
     */
    protected void doWrite(boolean block) throws IOException {
        socketBufferHandler.configureWriteBufferForRead();
        doWrite(block, socketBufferHandler.getWriteBuffer());
    }


    /**
     * Write the contents of the ByteBuffer to the socket. For blocking writes either then entire contents of the buffer
     * will be written or an IOException will be thrown. Partial blocking writes will not occur.
     *
     * @param block Should the write be blocking or not?
     * @param from  the ByteBuffer containing the data to be written
     *
     * @throws IOException If an I/O error such as a timeout occurs during the write
     */
    protected abstract void doWrite(boolean block, ByteBuffer from) throws IOException;


    /**
     * Processes a socket event.
     *
     * @param socketStatus The socket event status
     * @param dispatch Whether to dispatch to a worker thread
     */
    public void processSocket(SocketEvent socketStatus, boolean dispatch) {
        endpoint.processSocket(this, socketStatus, dispatch);
    }


    /**
     * Registers interest in read events.
     */
    public abstract void registerReadInterest();

    /**
     * Registers interest in write events.
     */
    public abstract void registerWriteInterest();

    /**
     * Creates a sendfile data object for the specified file.
     *
     * @param filename The file to send
     * @param pos The starting position in the file
     * @param length The number of bytes to send
     *
     * @return a new sendfile data object
     */
    public abstract SendfileDataBase createSendfileData(String filename, long pos, long length);

    /**
     * Starts the sendfile process. It is expected that if the sendfile process does not complete during this call and
     * does not report an error, that the caller <b>will not</b> add the socket to the poller (or equivalent). That is
     * the responsibility of this method.
     *
     * @param sendfileData Data representing the file to send
     *
     * @return The state of the sendfile process after the first write.
     */
    public abstract SendfileState processSendfile(SendfileDataBase sendfileData);

    /**
     * Require the client to perform CLIENT-CERT authentication if it hasn't already done so.
     *
     * @param sslSupport The SSL/TLS support instance currently being used by the connection that may need updating
     *                       after the client authentication
     *
     * @throws IOException If authentication is required then there will be I/O with the client and this exception will
     *                         be thrown if that goes wrong
     */
    public abstract void doClientAuth(SSLSupport sslSupport) throws IOException;

    /**
     * Obtain an SSLSupport instance for this socket.
     *
     * @return An SSLSupport instance for this socket.
     */
    public abstract SSLSupport getSslSupport();


    // ------------------------------------------------------- NIO 2 style APIs


    /**
     * Defines the blocking behavior for I/O operations.
     */
    public enum BlockingMode {
        /**
         * The operation will not block. If there are pending operations, the operation will throw a pending exception.
         */
        CLASSIC,
        /**
         * The operation will not block. If there are pending operations, the operation will return
         * CompletionState.NOT_DONE.
         */
        NON_BLOCK,
        /**
         * The operation will block until pending operations are completed, but will not block after performing it.
         */
        SEMI_BLOCK,
        /**
         * The operation will block until completed.
         */
        BLOCK
    }

    /**
     * Represents the completion state of an asynchronous I/O operation.
     */
    public enum CompletionState {
        /**
         * Operation is still pending.
         */
        PENDING,
        /**
         * Operation was pending and non-blocking.
         */
        NOT_DONE,
        /**
         * The operation completed inline.
         */
        INLINE,
        /**
         * The operation completed inline but failed.
         */
        ERROR,
        /**
         * The operation completed, but not inline.
         */
        DONE
    }

    /**
     * Defines the possible outcomes for completion handler invocation.
     */
    public enum CompletionHandlerCall {
        /**
         * Operation should continue, the completion handler shouldn't be called.
         */
        CONTINUE,
        /**
         * The operation completed but the completion handler shouldn't be called.
         */
        NONE,
        /**
         * The operation is complete, the completion handler should be called.
         */
        DONE
    }

    /**
     * Interface for determining whether a completion handler should be invoked.
     */
    public interface CompletionCheck {
        /**
         * Determine what call, if any, should be made to the completion handler.
         *
         * @param state   of the operation (done or done in-line since the IO call is done)
         * @param buffers ByteBuffer[] that has been passed to the original IO call
         * @param offset  that has been passed to the original IO call
         * @param length  that has been passed to the original IO call
         *
         * @return The call, if any, to make to the completion handler
         */
        CompletionHandlerCall callHandler(CompletionState state, ByteBuffer[] buffers, int offset, int length);
    }

    /**
     * This utility CompletionCheck will cause the write to fully write all remaining data. If the operation completes
     * inline, the completion handler will not be called.
     */
    public static final CompletionCheck COMPLETE_WRITE = (state, buffers, offset, length) -> {
        for (int i = 0; i < length; i++) {
            if (buffers[offset + i].hasRemaining()) {
                return CompletionHandlerCall.CONTINUE;
            }
        }
        return (state == CompletionState.DONE) ? CompletionHandlerCall.DONE : CompletionHandlerCall.NONE;
    };

    /**
     * This utility CompletionCheck will cause the write to fully write all remaining data. The completion handler will
     * then be called.
     */
    public static final CompletionCheck COMPLETE_WRITE_WITH_COMPLETION = (state, buffers, offset, length) -> {
        for (int i = 0; i < length; i++) {
            if (buffers[offset + i].hasRemaining()) {
                return CompletionHandlerCall.CONTINUE;
            }
        }
        return CompletionHandlerCall.DONE;
    };

    /**
     * This utility CompletionCheck will cause the completion handler to be called once some data has been read. If the
     * operation completes inline, the completion handler will not be called.
     */
    public static final CompletionCheck READ_DATA = (state, buffers, offset,
            length) -> (state == CompletionState.DONE) ? CompletionHandlerCall.DONE : CompletionHandlerCall.NONE;

    /**
     * This utility CompletionCheck will cause the completion handler to be called once the given buffers are full. The
     * completion handler will then be called.
     */
    public static final CompletionCheck COMPLETE_READ_WITH_COMPLETION = COMPLETE_WRITE_WITH_COMPLETION;

    /**
     * This utility CompletionCheck will cause the completion handler to be called once the given buffers are full. If
     * the operation completes inline, the completion handler will not be called.
     */
    public static final CompletionCheck COMPLETE_READ = COMPLETE_WRITE;

    /**
     * Internal state tracker for vectored operations.
     *
     * @param <A> The attachment type
     */
    protected abstract class OperationState<A> implements Runnable {
        /** Indicates whether this is a read operation. */
        protected final boolean read;
        /** Buffers used for the vectored I/O operation. */
        protected final ByteBuffer[] buffers;
        /** Offset in the buffer array. */
        protected final int offset;
        /** Number of buffers in the operation. */
        protected final int length;
        /** Attachment object passed to the completion handler. */
        protected final A attachment;
        /** Timeout value for the operation. */
        protected final long timeout;
        /** Time unit for the timeout. */
        protected final TimeUnit unit;
        /** Blocking mode for the operation. */
        protected final BlockingMode block;
        /** Completion check to determine handler invocation. */
        protected final CompletionCheck check;
        /** Completion handler to invoke when the operation completes. */
        protected final CompletionHandler<Long,? super A> handler;
        /** Semaphore for synchronizing concurrent operations. */
        protected final Semaphore semaphore;
        /** Vectored I/O completion handler. */
        protected final VectoredIOCompletionHandler<A> completion;
        /** Flag to ensure the completion handler is called only once. */
        protected final AtomicBoolean callHandler;

        /**
         * Creates a new operation state for a vectored I/O operation.
         *
         * @param read Whether this is a read operation
         * @param buffers The buffers for the operation
         * @param offset The offset in the buffer array
         * @param length The number of buffers
         * @param block The blocking mode
         * @param timeout The timeout duration
         * @param unit The timeout time unit
         * @param attachment An attachment object
         * @param check The completion check
         * @param handler The completion handler
         * @param semaphore The semaphore for synchronization
         * @param completion The vectored I/O completion handler
         */
        protected OperationState(boolean read, ByteBuffer[] buffers, int offset, int length, BlockingMode block,
                long timeout, TimeUnit unit, A attachment, CompletionCheck check,
                CompletionHandler<Long,? super A> handler, Semaphore semaphore,
                VectoredIOCompletionHandler<A> completion) {
            this.read = read;
            this.buffers = buffers;
            this.offset = offset;
            this.length = length;
            this.block = block;
            this.timeout = timeout;
            this.unit = unit;
            this.attachment = attachment;
            this.check = check;
            this.handler = handler;
            this.semaphore = semaphore;
            this.completion = completion;
            callHandler = (handler != null) ? new AtomicBoolean(true) : null;
        }

        /** Total number of bytes transferred by the operation. */
        protected volatile long nBytes = 0;
        /** Current completion state of the operation. */
        protected volatile CompletionState state = CompletionState.PENDING;
        /** Indicates whether the vectored I/O completion processing is done. */
        protected boolean completionDone = true;

      /**
         * Determines whether the operation is still executing on the original caller thread.
         *
         * @return {@code true} if the operation is still inline, {@code false} if running on a separate thread
         */
        protected abstract boolean isInline();

        /**
      * Checks if there is remaining outbound data in the buffers.
      *
      * @return {@code false} by default; overridden by NIO implementations
      */
        protected boolean hasOutboundRemaining() {
            // NIO2 and APR never have remaining outbound data when the
            // completion handler is called. NIO needs to override this.
            return false;
        }


        /**
         * Process the operation using the connector executor.
         *
         * @return true if the operation was accepted, false if the executor rejected execution
         */
        protected boolean process() {
            try {
                getEndpoint().getExecutor().execute(this);
                return true;
            } catch (RejectedExecutionException ree) {
                log.warn(sm.getString("endpoint.executor.fail", SocketWrapperBase.this), ree);
            } catch (Throwable t) {
                ExceptionUtils.handleThrowable(t);
                // This means we got an OOM or similar creating a thread, or that
                // the pool and its queue are full
                log.error(sm.getString("endpoint.process.fail"), t);
            }
            return false;
        }

        /**
         * Start the operation, this will typically call run.
         */
        protected void start() {
            run();
        }

        /**
         * End the operation.
         */
        protected void end() {
        }

    }

    /**
     * Completion handler for vectored operations. This will check the completion of the operation, then either continue
     * or call the user provided completion handler.
     *
     * @param <A> The attachment type
     */
    protected class VectoredIOCompletionHandler<A> implements CompletionHandler<Long,OperationState<A>> {
        /**
         * Creates a new vectored I/O completion handler.
         */
        protected VectoredIOCompletionHandler() {
        }

        @Override
        public void completed(Long nBytes, OperationState<A> state) {
            if (nBytes.longValue() < 0) {
                failed(new EOFException(), state);
            } else {
                state.nBytes += nBytes.longValue();
                CompletionState currentState = state.isInline() ? CompletionState.INLINE : CompletionState.DONE;
                boolean complete = true;
                boolean completion = true;
                if (state.check != null) {
                    CompletionHandlerCall call =
                            state.check.callHandler(currentState, state.buffers, state.offset, state.length);
                    if (call == CompletionHandlerCall.CONTINUE || (!state.read && state.hasOutboundRemaining())) {
                        complete = false;
                    } else if (call == CompletionHandlerCall.NONE) {
                        completion = false;
                    }
                }
                if (complete) {
                    boolean notify = false;
                    if (state.read) {
                        readOperation = null;
                    } else {
                        writeOperation = null;
                    }
                    // Semaphore must be released after [read|write]Operation is cleared
                    // to ensure that the next thread to hold the semaphore hasn't
                    // written a new value to [read|write]Operation by the time it is
                    // cleared.
                    state.semaphore.release();
                    if (state.block == BlockingMode.BLOCK && currentState != CompletionState.INLINE) {
                        notify = true;
                    } else {
                        state.state = currentState;
                    }
                    state.end();
                    if (completion && state.handler != null && state.callHandler.compareAndSet(true, false)) {
                        state.handler.completed(Long.valueOf(state.nBytes), state.attachment);
                    }
                    synchronized (state) {
                        state.completionDone = true;
                        if (notify) {
                            state.state = currentState;
                            state.notify();
                        }
                    }
                } else {
                    synchronized (state) {
                        state.completionDone = true;
                    }
                    state.run();
                }
            }
        }

        @Override
        public void failed(Throwable exc, OperationState<A> state) {
            IOException ioe = null;
            if (exc instanceof InterruptedByTimeoutException) {
                ioe = new SocketTimeoutException();
                exc = ioe;
            } else if (exc instanceof IOException) {
                ioe = (IOException) exc;
            }
            setError(ioe);
            boolean notify = false;
            if (state.read) {
                readOperation = null;
            } else {
                writeOperation = null;
            }
            // Semaphore must be released after [read|write]Operation is cleared
            // to ensure that the next thread to hold the semaphore hasn't
            // written a new value to [read|write]Operation by the time it is
            // cleared.
            state.semaphore.release();
            if (state.block == BlockingMode.BLOCK) {
                notify = true;
            } else {
                state.state = state.isInline() ? CompletionState.ERROR : CompletionState.DONE;
            }
            state.end();
            if (state.handler != null && state.callHandler.compareAndSet(true, false)) {
                state.handler.failed(exc, state.attachment);
            }
            synchronized (state) {
                state.completionDone = true;
                if (notify) {
                    state.state = state.isInline() ? CompletionState.ERROR : CompletionState.DONE;
                    state.notify();
                }
            }
        }
    }

    /**
     * Allows using NIO2 style read/write.
     *
     * @return {@code true} if the connector has the capability enabled
     */
    public boolean hasAsyncIO() {
        // The semaphores are only created if async IO is enabled
        return (readPending != null);
    }

    /**
     * Allows indicating if the connector needs semaphores.
     *
     * @return This default implementation always returns {@code false}
     */
    public boolean needSemaphores() {
        return false;
    }

    /**
     * Allows indicating if the connector supports per operation timeout.
     *
     * @return This default implementation always returns {@code false}
     */
    public boolean hasPerOperationTimeout() {
        return false;
    }

    /**
     * Allows checking if an asynchronous read operation is currently pending.
     *
     * @return <code>true</code> if the endpoint supports asynchronous IO and a read operation is being processed
     *             asynchronously
     */
    public boolean isReadPending() {
        return false;
    }

    /**
     * Allows checking if an asynchronous write operation is currently pending.
     *
     * @return <code>true</code> if the endpoint supports asynchronous IO and a write operation is being processed
     *             asynchronously
     */
    public boolean isWritePending() {
        return false;
    }

    /**
     * Scatter read. The completion handler will be called once some data has been read or an error occurred. The
     * default NIO2 behavior is used: the completion handler will be called as soon as some data has been read, even if
     * the read has completed inline.
     *
     * @param timeout    timeout duration for the read
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param handler    to call when the IO is complete
     * @param dsts       buffers
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState read(long timeout, TimeUnit unit, A attachment,
            CompletionHandler<Long,? super A> handler, ByteBuffer... dsts) {
        if (dsts == null) {
            throw new IllegalArgumentException();
        }
        return read(dsts, 0, dsts.length, BlockingMode.CLASSIC, timeout, unit, attachment, null, handler);
    }

    /**
     * Scatter read. The completion handler will be called once some data has been read or an error occurred. If a
     * CompletionCheck object has been provided, the completion handler will only be called if the callHandler method
     * returned true. If no CompletionCheck object has been provided, the default NIO2 behavior is used: the completion
     * handler will be called as soon as some data has been read, even if the read has completed inline.
     *
     * @param block      is the blocking mode that will be used for this operation
     * @param timeout    timeout duration for the read
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param check      for the IO operation completion
     * @param handler    to call when the IO is complete
     * @param dsts       buffers
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState read(BlockingMode block, long timeout, TimeUnit unit, A attachment,
            CompletionCheck check, CompletionHandler<Long,? super A> handler, ByteBuffer... dsts) {
        if (dsts == null) {
            throw new IllegalArgumentException();
        }
        return read(dsts, 0, dsts.length, block, timeout, unit, attachment, check, handler);
    }

    /**
     * Scatter read. The completion handler will be called once some data has been read or an error occurred. If a
     * CompletionCheck object has been provided, the completion handler will only be called if the callHandler method
     * returned true. If no CompletionCheck object has been provided, the default NIO2 behavior is used: the completion
     * handler will be called as soon as some data has been read, even if the read has completed inline.
     *
     * @param dsts       buffers
     * @param offset     in the buffer array
     * @param length     in the buffer array
     * @param block      is the blocking mode that will be used for this operation
     * @param timeout    timeout duration for the read
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param check      for the IO operation completion
     * @param handler    to call when the IO is complete
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState read(ByteBuffer[] dsts, int offset, int length, BlockingMode block, long timeout,
            TimeUnit unit, A attachment, CompletionCheck check, CompletionHandler<Long,? super A> handler) {
        return vectoredOperation(true, dsts, offset, length, block, timeout, unit, attachment, check, handler);
    }

    /**
     * Gather write. The completion handler will be called once some data has been written or an error occurred. The
     * default NIO2 behavior is used: the completion handler will be called, even if the write is incomplete and data
     * remains in the buffers, or if the write completed inline.
     *
     * @param timeout    timeout duration for the write
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param handler    to call when the IO is complete
     * @param srcs       buffers
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState write(long timeout, TimeUnit unit, A attachment,
            CompletionHandler<Long,? super A> handler, ByteBuffer... srcs) {
        if (srcs == null) {
            throw new IllegalArgumentException();
        }
        return write(srcs, 0, srcs.length, BlockingMode.CLASSIC, timeout, unit, attachment, null, handler);
    }

    /**
     * Gather write. The completion handler will be called once some data has been written or an error occurred. If a
     * CompletionCheck object has been provided, the completion handler will only be called if the callHandler method
     * returned true. If no CompletionCheck object has been provided, the default NIO2 behavior is used: the completion
     * handler will be called, even if the write is incomplete and data remains in the buffers, or if the write
     * completed inline.
     *
     * @param block      is the blocking mode that will be used for this operation
     * @param timeout    timeout duration for the write
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param check      for the IO operation completion
     * @param handler    to call when the IO is complete
     * @param srcs       buffers
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState write(BlockingMode block, long timeout, TimeUnit unit, A attachment,
            CompletionCheck check, CompletionHandler<Long,? super A> handler, ByteBuffer... srcs) {
        if (srcs == null) {
            throw new IllegalArgumentException();
        }
        return write(srcs, 0, srcs.length, block, timeout, unit, attachment, check, handler);
    }

    /**
     * Gather write. The completion handler will be called once some data has been written or an error occurred. If a
     * CompletionCheck object has been provided, the completion handler will only be called if the callHandler method
     * returned true. If no CompletionCheck object has been provided, the default NIO2 behavior is used: the completion
     * handler will be called, even if the write is incomplete and data remains in the buffers, or if the write
     * completed inline.
     *
     * @param srcs       buffers
     * @param offset     in the buffer array
     * @param length     in the buffer array
     * @param block      is the blocking mode that will be used for this operation
     * @param timeout    timeout duration for the write
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param check      for the IO operation completion
     * @param handler    to call when the IO is complete
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    public final <A> CompletionState write(ByteBuffer[] srcs, int offset, int length, BlockingMode block, long timeout,
            TimeUnit unit, A attachment, CompletionCheck check, CompletionHandler<Long,? super A> handler) {
        return vectoredOperation(false, srcs, offset, length, block, timeout, unit, attachment, check, handler);
    }


    /**
     * Vectored operation. The completion handler will be called once the operation is complete or an error occurred. If
     * a CompletionCheck object has been provided, the completion handler will only be called if the callHandler method
     * returned true. If no CompletionCheck object has been provided, the default NIO2 behavior is used: the completion
     * handler will be called, even if the operation is incomplete, or if the operation completed inline.
     *
     * @param read       true if the operation is a read, false if it is a write
     * @param buffers    buffers
     * @param offset     in the buffer array
     * @param length     in the buffer array
     * @param block      is the blocking mode that will be used for this operation
     * @param timeout    timeout duration for the write
     * @param unit       units for the timeout duration
     * @param attachment an object to attach to the I/O operation that will be used when calling the completion handler
     * @param check      for the IO operation completion
     * @param handler    to call when the IO is complete
     * @param <A>        The attachment type
     *
     * @return the completion state (done, done inline, or still pending)
     */
    protected final <A> CompletionState vectoredOperation(boolean read, ByteBuffer[] buffers, int offset, int length,
            BlockingMode block, long timeout, TimeUnit unit, A attachment, CompletionCheck check,
            CompletionHandler<Long,? super A> handler) {
        IOException ioe = getError();
        if (ioe != null) {
            if (handler != null) {
                handler.failed(ioe, attachment);
            }
            return CompletionState.ERROR;
        }
        if (timeout == -1) {
            timeout = AbstractEndpoint.toTimeout(read ? getReadTimeout() : getWriteTimeout());
            unit = TimeUnit.MILLISECONDS;
        } else if (!hasPerOperationTimeout() &&
                (unit.toMillis(timeout) != (read ? getReadTimeout() : getWriteTimeout()))) {
            if (read) {
                setReadTimeout(unit.toMillis(timeout));
            } else {
                setWriteTimeout(unit.toMillis(timeout));
            }
        }
        if (block == BlockingMode.BLOCK || block == BlockingMode.SEMI_BLOCK) {
            try {
                if (read ? !readPending.tryAcquire(timeout, unit) : !writePending.tryAcquire(timeout, unit)) {
                    if (handler != null) {
                        handler.failed(new SocketTimeoutException(), attachment);
                    }
                    return CompletionState.ERROR;
                }
            } catch (InterruptedException e) {
                if (handler != null) {
                    handler.failed(e, attachment);
                }
                return CompletionState.ERROR;
            }
        } else {
            if (read ? !readPending.tryAcquire() : !writePending.tryAcquire()) {
                if (block == BlockingMode.NON_BLOCK) {
                    return CompletionState.NOT_DONE;
                } else {
                    if (handler != null) {
                        handler.failed(read ? new ReadPendingException() : new WritePendingException(), attachment);
                    }
                    return CompletionState.ERROR;
                }
            }
        }
        VectoredIOCompletionHandler<A> completion = new VectoredIOCompletionHandler<>();
        OperationState<A> state = newOperationState(read, buffers, offset, length, block, timeout, unit, attachment,
                check, handler, read ? readPending : writePending, completion);
        if (read) {
            readOperation = state;
        } else {
            writeOperation = state;
        }
        state.start();
        if (block == BlockingMode.BLOCK) {
            synchronized (state) {
                if (state.state == CompletionState.PENDING) {
                    try {
                        long timeoutExpiry = System.nanoTime() + unit.toNanos(timeout);
                        long timeoutMillis = unit.toMillis(timeout);
                        // Spurious wake-ups are possible. Keep waiting until state changes or timeout expires.
                        while (state.state == CompletionState.PENDING && timeoutMillis > 0) {
                            state.wait(unit.toMillis(timeout));
                            timeoutMillis = (timeoutExpiry - System.nanoTime()) / 1_000_000;
                        }
                        if (state.state == CompletionState.PENDING) {
                            if (handler != null && state.callHandler.compareAndSet(true, false)) {
                                handler.failed(new SocketTimeoutException(getTimeoutMsg(read)), attachment);
                            }
                            return CompletionState.ERROR;
                        }
                    } catch (InterruptedException e) {
                        if (handler != null && state.callHandler.compareAndSet(true, false)) {
                            handler.failed(new SocketTimeoutException(getTimeoutMsg(read)), attachment);
                        }
                        return CompletionState.ERROR;
                    }
                }
            }
        }
        return state.state;
    }


    private String getTimeoutMsg(boolean read) {
        if (read) {
            return sm.getString("socketWrapper.readTimeout");
        } else {
            return sm.getString("socketWrapper.writeTimeout");
        }
    }


    /**
     * Creates a new operation state for vectored I/O operations.
     *
     * @param read Whether this is a read operation
     * @param buffers The buffers for the operation
     * @param offset The offset in the buffer array
     * @param length The number of buffers
     * @param block The blocking mode
     * @param timeout The timeout duration
     * @param unit The timeout time unit
     * @param attachment An attachment object
     * @param check The completion check
     * @param handler The completion handler
     * @param semaphore The semaphore for synchronization
     * @param completion The vectored I/O completion handler
     * @param <A> The attachment type
     *
     * @return a new operation state instance
     */
    protected abstract <A> OperationState<A> newOperationState(boolean read, ByteBuffer[] buffers, int offset,
            int length, BlockingMode block, long timeout, TimeUnit unit, A attachment, CompletionCheck check,
            CompletionHandler<Long,? super A> handler, Semaphore semaphore, VectoredIOCompletionHandler<A> completion);


    // --------------------------------------------------------- Utility methods

    /**
     * Transfers data from a byte array into a ByteBuffer.
     *
     * @param from the source byte array
     * @param offset the offset in the source array
     * @param length the maximum number of bytes to transfer
     * @param to the destination ByteBuffer
     * @return the number of bytes transferred
     */
    protected static int transfer(byte[] from, int offset, int length, ByteBuffer to) {
        int max = Math.min(length, to.remaining());
        if (max > 0) {
            to.put(from, offset, max);
        }
        return max;
    }

    /**
     * Transfers data from one ByteBuffer to another.
     *
     * @param from the source ByteBuffer
     * @param to the destination ByteBuffer
     * @return the number of bytes transferred
     */
    protected static int transfer(ByteBuffer from, ByteBuffer to) {
        int max = Math.min(from.remaining(), to.remaining());
        if (max > 0) {
            int fromLimit = from.limit();
            from.limit(from.position() + max);
            to.put(from);
            from.limit(fromLimit);
        }
        return max;
    }

    /**
     * Checks if any buffer in the specified range has remaining capacity.
     *
     * @param buffers the array of ByteBuffers
     * @param offset the starting index in the array
     * @param length the number of buffers to check
     * @return {@code true} if any buffer has remaining capacity
     */
    protected static boolean buffersArrayHasRemaining(ByteBuffer[] buffers, int offset, int length) {
        for (int pos = offset; pos < offset + length; pos++) {
            if (buffers[pos].hasRemaining()) {
                return true;
            }
        }
        return false;
    }


    // -------------------------------------------------------------- ID methods

    /**
     * Returns the ServletConnection for this socket, creating it if necessary.
     *
     * @param protocol the protocol name
     * @param protocolConnectionId the protocol-level connection ID
     * @return the ServletConnection instance
     */
    public ServletConnection getServletConnection(String protocol, String protocolConnectionId) {
        if (servletConnection == null) {
            servletConnection =
                    new ServletConnectionImpl(connectionId, protocol, protocolConnectionId, endpoint.isSSLEnabled());
        }
        return servletConnection;
    }
}
