/*
 * ====================================================================
 * 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.
 * ====================================================================
 *
 * This software consists of voluntary contributions made by many
 * individuals on behalf of the Apache Software Foundation.  For more
 * information on the Apache Software Foundation, please see
 * <http://www.apache.org/>.
 *
 */

package org.apache.hc.core5.reactor.ssl;

import java.io.IOException;
import java.net.SocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ByteChannel;
import java.nio.channels.CancelledKeyException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLEngineResult;
import javax.net.ssl.SSLEngineResult.HandshakeStatus;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLHandshakeException;
import javax.net.ssl.SSLSession;

import org.apache.hc.core5.annotation.Contract;
import org.apache.hc.core5.annotation.Internal;
import org.apache.hc.core5.annotation.ThreadingBehavior;
import org.apache.hc.core5.concurrent.FutureCallback;
import org.apache.hc.core5.function.Callback;
import org.apache.hc.core5.io.CloseMode;
import org.apache.hc.core5.io.SocketTimeoutExceptionFactory;
import org.apache.hc.core5.net.NamedEndpoint;
import org.apache.hc.core5.reactor.Command;
import org.apache.hc.core5.reactor.EventMask;
import org.apache.hc.core5.reactor.IOEventHandler;
import org.apache.hc.core5.reactor.IOSession;
import org.apache.hc.core5.util.Args;
import org.apache.hc.core5.util.Asserts;
import org.apache.hc.core5.util.Timeout;

/**
 * {@code SSLIOSession} is a decorator class intended to transparently extend
 * an {@link IOSession} with transport layer security capabilities based on
 * the SSL/TLS protocol.
 *
 * @since 4.2
 */
@Contract(threading = ThreadingBehavior.SAFE_CONDITIONAL)
@Internal
public class SSLIOSession implements IOSession {

    enum TLSHandShakeState { READY, INITIALIZED, HANDSHAKING, COMPLETE }

    private static final ByteBuffer EMPTY_BUFFER = ByteBuffer.allocate(0);

    private final NamedEndpoint targetEndpoint;
    private final IOSession session;
    private final SSLEngine sslEngine;
    private final SSLManagedBuffer inEncrypted;
    private final SSLManagedBuffer outEncrypted;
    private final SSLManagedBuffer inPlain;
    private final SSLSessionInitializer initializer;
    private final SSLSessionVerifier verifier;
    private final Callback<SSLIOSession> sessionStartCallback;
    private final Callback<SSLIOSession> sessionEndCallback;
    private final AtomicReference<FutureCallback<SSLSession>> handshakeCallbackRef;
    private final Timeout handshakeTimeout;
    private final SSLMode sslMode;
    private final AtomicInteger outboundClosedCount;
    private final AtomicReference<TLSHandShakeState> handshakeStateRef;
    private final IOEventHandler internalEventHandler;
    private final int packetBufferSize;

    private int appEventMask;

    private volatile boolean endOfStream;
    private volatile Status status = Status.ACTIVE;
    private volatile Timeout socketTimeout;
    private volatile TlsDetails tlsDetails;
    private volatile boolean appClosed;

    /**
     * Creates new instance of {@code SSLIOSession} class.
     *
     * @param session I/O session to be decorated with the TLS/SSL capabilities.
     * @param sslMode SSL mode (client or server)
     * @param targetEndpoint target endpoint (applicable in client mode only). May be {@code null}.
     * @param sslContext SSL context to use for this I/O session.
     * @param sslBufferMode buffer management mode
     * @param initializer optional SSL session initializer. May be {@code null}.
     * @param verifier optional SSL session verifier. May be {@code null}.
     * @param connectTimeout timeout to apply for the TLS/SSL handshake. May be {@code null}.
     *
     * @since 5.0
     */
    public SSLIOSession(
            final NamedEndpoint targetEndpoint,
            final IOSession session,
            final SSLMode sslMode,
            final SSLContext sslContext,
            final SSLBufferMode sslBufferMode,
            final SSLSessionInitializer initializer,
            final SSLSessionVerifier verifier,
            final Callback<SSLIOSession> sessionStartCallback,
            final Callback<SSLIOSession> sessionEndCallback,
            final Timeout connectTimeout) {
        this(targetEndpoint, session, sslMode, sslContext, sslBufferMode, initializer, verifier, connectTimeout,
                sessionStartCallback, sessionEndCallback, null);
    }

    /**
     * Creates new instance of {@code SSLIOSession} class.
     *
     * @param session I/O session to be decorated with the TLS/SSL capabilities.
     * @param sslMode SSL mode (client or server)
     * @param targetEndpoint target endpoint (applicable in client mode only). May be {@code null}.
     * @param sslContext SSL context to use for this I/O session.
     * @param sslBufferMode buffer management mode
     * @param initializer optional SSL session initializer. May be {@code null}.
     * @param verifier optional SSL session verifier. May be {@code null}.
     * @param handshakeTimeout timeout to apply for the TLS/SSL handshake. May be {@code null}.
     * @param resultCallback result callback. May be {@code null}.
     *
     * @since 5.2
     */
    public SSLIOSession(
            final NamedEndpoint targetEndpoint,
            final IOSession session,
            final SSLMode sslMode,
            final SSLContext sslContext,
            final SSLBufferMode sslBufferMode,
            final SSLSessionInitializer initializer,
            final SSLSessionVerifier verifier,
            final Timeout handshakeTimeout,
            final Callback<SSLIOSession> sessionStartCallback,
            final Callback<SSLIOSession> sessionEndCallback,
            final FutureCallback<SSLSession> resultCallback) {
        super();
        Args.notNull(session, "IO session");
        Args.notNull(sslContext, "SSL context");
        this.targetEndpoint = targetEndpoint;
        this.session = session;
        this.sslMode = sslMode;
        this.initializer = initializer;
        this.verifier = verifier;
        this.sessionStartCallback = sessionStartCallback;
        this.sessionEndCallback = sessionEndCallback;
        this.handshakeCallbackRef = new AtomicReference<>(resultCallback);

        this.appEventMask = session.getEventMask();
        if (this.sslMode == SSLMode.CLIENT && targetEndpoint != null) {
            this.sslEngine = sslContext.createSSLEngine(targetEndpoint.getHostName(), targetEndpoint.getPort());
        } else {
            this.sslEngine = sslContext.createSSLEngine();
        }

        final SSLSession sslSession = this.sslEngine.getSession();
        // Allocate buffers for network (encrypted) data
        this.packetBufferSize = sslSession.getPacketBufferSize();
        this.inEncrypted = SSLManagedBuffer.create(sslBufferMode, packetBufferSize);
        this.outEncrypted = SSLManagedBuffer.create(sslBufferMode, packetBufferSize);

        // Allocate buffers for application (unencrypted) data
        final int appBufferSize = sslSession.getApplicationBufferSize();
        this.inPlain = SSLManagedBuffer.create(sslBufferMode, appBufferSize);
        this.outboundClosedCount = new AtomicInteger(0);
        this.handshakeStateRef = new AtomicReference<>(TLSHandShakeState.READY);
        this.handshakeTimeout = handshakeTimeout;
        this.internalEventHandler = new IOEventHandler() {

            @Override
            public void connected(final IOSession protocolSession) throws IOException {
                beginHandshake(protocolSession);
            }

            @Override
            public void inputReady(final IOSession protocolSession, final ByteBuffer src) throws IOException {
                receiveEncryptedData();
                doHandshake(protocolSession);
                decryptData(protocolSession);
                updateEventMask();
            }

            @Override
            public void outputReady(final IOSession protocolSession) throws IOException {
                encryptData(protocolSession);
                sendEncryptedData();
                doHandshake(protocolSession);
                updateEventMask();
            }

            @Override
            public void timeout(final IOSession protocolSession, final Timeout timeout) throws IOException {
                if (sslEngine.isInboundDone() && !sslEngine.isInboundDone()) {
                    // The session failed to terminate cleanly
                    close(CloseMode.IMMEDIATE);
                }
                if (handshakeStateRef.get() != TLSHandShakeState.COMPLETE) {
                    exception(protocolSession, SocketTimeoutExceptionFactory.create(handshakeTimeout));
                } else {
                    ensureHandler().timeout(protocolSession, timeout);
                }
            }

            @Override
            public void exception(final IOSession protocolSession, final Exception cause) {
                final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
                if (resultCallback != null) {
                    resultCallback.failed(cause);
                }
                final IOEventHandler handler = session.getHandler();
                if (handshakeStateRef.get() != TLSHandShakeState.COMPLETE) {
                    if (cause instanceof SSLHandshakeException) {
                        close(CloseMode.GRACEFUL);
                    } else {
                        session.close(CloseMode.GRACEFUL);
                        close(CloseMode.IMMEDIATE);
                    }
                }
                if (handler != null) {
                    handler.exception(protocolSession, cause);
                }
            }

            @Override
            public void disconnected(final IOSession protocolSession) {
                final IOEventHandler handler = session.getHandler();
                if (handler != null) {
                    handler.disconnected(protocolSession);
                }
            }

        };

    }

    private IOEventHandler ensureHandler() {
        final IOEventHandler handler = session.getHandler();
        Asserts.notNull(handler, "IO event handler");
        return handler;
    }

    @Override
    public IOEventHandler getHandler() {
        return internalEventHandler;
    }

    public void beginHandshake(final IOSession protocolSession) throws IOException {
        if (handshakeStateRef.compareAndSet(TLSHandShakeState.READY, TLSHandShakeState.INITIALIZED)) {
            initialize(protocolSession);
        }
    }

    private void initialize(final IOSession protocolSession) throws IOException {
        // Save the initial socketTimeout of the underlying IOSession, to be restored after the handshake is finished
        this.socketTimeout = this.session.getSocketTimeout();
        if (handshakeTimeout != null) {
            this.session.setSocketTimeout(handshakeTimeout);
        }

        this.session.getLock().lock();
        try {
            if (this.status.compareTo(Status.CLOSING) >= 0) {
                return;
            }
            switch (this.sslMode) {
                case CLIENT:
                    this.sslEngine.setUseClientMode(true);
                    break;
                case SERVER:
                    this.sslEngine.setUseClientMode(false);
                    break;
            }
            if (this.initializer != null) {
                this.initializer.initialize(this.targetEndpoint, this.sslEngine);
            }
            this.handshakeStateRef.set(TLSHandShakeState.HANDSHAKING);
            this.sslEngine.beginHandshake();

            this.inEncrypted.release();
            this.outEncrypted.release();
            doHandshake(protocolSession);
            updateEventMask();
        } finally {
            this.session.getLock().unlock();
        }
    }

    // A works-around for exception handling craziness in Sun/Oracle's SSLEngine
    // implementation.
    //
    // sun.security.pkcs11.wrapper.PKCS11Exception is re-thrown as
    // plain RuntimeException in sun.security.ssl.Handshaker#checkThrown
    private SSLException convert(final RuntimeException ex) {
        Throwable cause = ex.getCause();
        if (cause == null) {
            cause = ex;
        }
        return new SSLException(cause);
    }

    private SSLEngineResult doWrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
        try {
            return this.sslEngine.wrap(src, dst);
        } catch (final RuntimeException ex) {
            throw convert(ex);
        }
    }

    private SSLEngineResult doUnwrap(final ByteBuffer src, final ByteBuffer dst) throws SSLException {
        try {
            return this.sslEngine.unwrap(src, dst);
        } catch (final RuntimeException ex) {
            throw convert(ex);
        }
    }

    private void doRunTask() {
        final Runnable r = this.sslEngine.getDelegatedTask();
        if (r != null) {
            r.run();
        }
    }

    private void doHandshake(final IOSession protocolSession) throws IOException {
        boolean handshaking = true;

        SSLEngineResult result = null;
        while (handshaking) {
             HandshakeStatus handshakeStatus = this.sslEngine.getHandshakeStatus();

            // Work-around for what appears to be a bug in Conscrypt SSLEngine that does not
            // transition into the handshaking state upon #closeOutbound() call but still
            // has some handshake data stuck in its internal buffer.
            if (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING && outboundClosedCount.get() > 0) {
                handshakeStatus = HandshakeStatus.NEED_WRAP;
            }

            switch (handshakeStatus) {
            case NEED_WRAP:
                // Generate outgoing handshake data

                this.session.getLock().lock();
                try {
                    // Acquire buffers
                    final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();

                    // Just wrap an empty buffer because there is no data to write.
                    result = doWrap(EMPTY_BUFFER, outEncryptedBuf);

                    if (result.getStatus() != SSLEngineResult.Status.OK || result.getHandshakeStatus() == HandshakeStatus.NEED_WRAP) {
                        handshaking = false;
                    }
                    break;
                } finally {
                    this.session.getLock().unlock();
                }
            case NEED_UNWRAP:
                // Process incoming handshake data

                // Acquire buffers
                final ByteBuffer inEncryptedBuf = this.inEncrypted.acquire();
                final ByteBuffer inPlainBuf = this.inPlain.acquire();

                // Perform operations
                inEncryptedBuf.flip();
                try {
                    result = doUnwrap(inEncryptedBuf, inPlainBuf);
                } finally {
                    inEncryptedBuf.compact();
                }

                try {
                    if (!inEncryptedBuf.hasRemaining() && result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
                        throw new SSLException("Input buffer is full");
                    }
                } finally {
                    // Release inEncrypted if empty
                    if (inEncryptedBuf.position() == 0) {
                        this.inEncrypted.release();
                    }
                }

                if (this.status.compareTo(Status.CLOSING) >= 0) {
                    this.inPlain.release();
                }
                if (result.getStatus() != SSLEngineResult.Status.OK) {
                    handshaking = false;
                }
                break;
            case NEED_TASK:
                doRunTask();
                break;
            case NOT_HANDSHAKING:
                handshaking = false;
                break;
            }
        }

        // The SSLEngine has just finished handshaking. This value is only generated by a call
        // to SSLEngine.wrap()/unwrap() when that call finishes a handshake.
        // It is never generated by SSLEngine.getHandshakeStatus().
        if (result != null && result.getHandshakeStatus() == HandshakeStatus.FINISHED) {
            this.handshakeStateRef.set(TLSHandShakeState.COMPLETE);
            this.session.setSocketTimeout(this.socketTimeout);
            if (this.verifier != null) {
                this.tlsDetails = this.verifier.verify(this.targetEndpoint, this.sslEngine);
            }
            String applicationProtocol;
            if (this.tlsDetails == null) {
                final SSLSession sslSession = this.sslEngine.getSession();
                try {
                    applicationProtocol = this.sslEngine.getApplicationProtocol();
                } catch (final UnsupportedOperationException e) {
                    // If the underlying provider does not support the operation, the getApplicationProtocol() method throws an UnsupportedOperationException.
                    // In this case, we fall back to "http/1.1" as the application protocol.
                    // This is a workaround to allow older applications that do not support the getApplicationProtocol() method to continue working.
                    // This workaround is temporary and is meant to maintain compatibility with older systems.
                    applicationProtocol = "http/1.1";
                }
                this.tlsDetails = new TlsDetails(sslSession, applicationProtocol);
            }

            ensureHandler().connected(protocolSession);

            if (this.sessionStartCallback != null) {
                this.sessionStartCallback.execute(this);
            }
            final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
            if (resultCallback != null) {
                resultCallback.completed(sslEngine.getSession());
            }
        }
    }

    private void updateEventMask() {
        this.session.getLock().lock();
        try {
            // Graceful session termination
            if (this.status == Status.ACTIVE
                    && (this.endOfStream || this.sslEngine.isInboundDone())) {
                this.status = Status.CLOSING;
                final FutureCallback<SSLSession> resultCallback = handshakeCallbackRef.getAndSet(null);
                if (resultCallback != null) {
                    resultCallback.failed(new SSLHandshakeException("TLS handshake failed"));
                }
            }
            if (this.status == Status.CLOSING && !this.outEncrypted.hasData()) {
                this.sslEngine.closeOutbound();
                this.outboundClosedCount.incrementAndGet();
            }
            final HandshakeStatus handshakeStatus = this.sslEngine.getHandshakeStatus();
            if (this.status == Status.CLOSING
                    && (handshakeStatus == HandshakeStatus.NOT_HANDSHAKING || handshakeStatus == HandshakeStatus.FINISHED)
                    && !this.outEncrypted.hasData()
                    && this.sslEngine.isOutboundDone()
                    && (this.endOfStream || this.sslEngine.isInboundDone())
                    && appClosed) {
                this.status = Status.CLOSED;
            }
            // Abnormal session termination
            if (this.status.compareTo(Status.CLOSING) <= 0 && this.endOfStream
                    && handshakeStatus == HandshakeStatus.NEED_UNWRAP) {
                this.status = Status.CLOSED;
            }
            if (this.status == Status.CLOSED) {
                this.session.close();
                if (sessionEndCallback != null) {
                    sessionEndCallback.execute(this);
                }
                return;
            }
            // Is there a task pending?
            if (handshakeStatus == HandshakeStatus.NEED_TASK) {
                doRunTask();
            }
            // Need to toggle the event mask for this channel?
            final int oldMask = this.session.getEventMask();
            int newMask = oldMask;
            switch (this.sslEngine.getHandshakeStatus()) {
                case NEED_WRAP:
                    newMask = EventMask.READ_WRITE;
                    break;
                case NEED_UNWRAP:
                    newMask = EventMask.READ;
                    break;
                case NOT_HANDSHAKING:
                    newMask = this.appEventMask;
                    break;
            }

            if (this.endOfStream && !this.inPlain.hasData()) {
                newMask = newMask & ~EventMask.READ;
            } else if (this.status == Status.CLOSING) {
                newMask = newMask | EventMask.READ;
            }

            // Do we have encrypted data ready to be sent?
            if (this.outEncrypted.hasData()) {
                newMask = newMask | EventMask.WRITE;
            }

            // Update the mask if necessary
            if (oldMask != newMask) {
                this.session.setEventMask(newMask);
            }
        } finally {
            this.session.getLock().unlock();
        }
    }

    private int sendEncryptedData() throws IOException {
        this.session.getLock().lock();
        try {
            if (this.status == Status.ACTIVE && !this.outEncrypted.hasData()) {
                // If the buffer isn't acquired or is empty, call write() with an empty buffer.
                // This will ensure that tests performed by write() still take place without
                // having to acquire and release an empty buffer (e.g. connection closed,
                // interrupted thread, etc..)
                return this.session.write(EMPTY_BUFFER);
            }

            // Acquire buffer
            final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();

            // Clear output buffer if the session has been closed
            // in case there is still `close_notify` data stuck in it
            if (this.status == Status.CLOSED) {
                outEncryptedBuf.clear();
            }

            // Perform operation
            int bytesWritten = 0;
            if (outEncryptedBuf.position() > 0) {
                outEncryptedBuf.flip();
                try {
                    bytesWritten = this.session.write(outEncryptedBuf);
                } finally {
                    outEncryptedBuf.compact();
                }
            }

            // Release if empty
            if (outEncryptedBuf.position() == 0) {
                this.outEncrypted.release();
            }
            return bytesWritten;
        } finally {
            this.session.getLock().unlock();
        }
    }

    private int receiveEncryptedData() throws IOException {
        if (this.endOfStream) {
            return -1;
        }

        // Acquire buffer
        final ByteBuffer inEncryptedBuf = this.inEncrypted.acquire();

        // Perform operation
        final int bytesRead = this.session.read(inEncryptedBuf);

        // Release if empty
        if (inEncryptedBuf.position() == 0) {
            this.inEncrypted.release();
        }
        if (bytesRead == -1) {
            this.endOfStream = true;
        }
        return bytesRead;
    }

    private void decryptData(final IOSession protocolSession) throws IOException {
        final HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
        if ((handshakeStatus == HandshakeStatus.NOT_HANDSHAKING || handshakeStatus == HandshakeStatus.FINISHED)
                && inEncrypted.hasData()) {
            final ByteBuffer inEncryptedBuf = inEncrypted.acquire();
            inEncryptedBuf.flip();
            try {
                while (inEncryptedBuf.hasRemaining()) {
                    final ByteBuffer inPlainBuf = inPlain.acquire();
                    try {
                        final SSLEngineResult result = doUnwrap(inEncryptedBuf, inPlainBuf);
                        if (!inEncryptedBuf.hasRemaining() && result.getHandshakeStatus() == HandshakeStatus.NEED_UNWRAP) {
                            throw new SSLException("Unable to complete SSL handshake");
                        }
                        if (sslEngine.isInboundDone()) {
                            endOfStream = true;
                        }
                        if (inPlainBuf.position() > 0) {
                            inPlainBuf.flip();
                            try {
                                ensureHandler().inputReady(protocolSession, inPlainBuf.hasRemaining() ? inPlainBuf : null);
                            } finally {
                                inPlainBuf.clear();
                            }
                        }
                        if (result.getStatus() == SSLEngineResult.Status.BUFFER_UNDERFLOW && endOfStream) {
                            throw new SSLException("Unable to decrypt incoming data due to unexpected end of stream");
                        }
                        if (result.getStatus() != SSLEngineResult.Status.OK ||
                                result.getHandshakeStatus() != HandshakeStatus.NOT_HANDSHAKING && result.getHandshakeStatus() != HandshakeStatus.FINISHED) {
                            break;
                        }
                    } finally {
                        inPlain.release();
                    }
                }
            } finally {
                inEncryptedBuf.compact();
                // Release inEncrypted if empty
                if (inEncryptedBuf.position() == 0) {
                    inEncrypted.release();
                }
            }
        }
        if (endOfStream && !inEncrypted.hasData()) {
            ensureHandler().inputReady(protocolSession, null);
        }
    }

    private void encryptData(final IOSession protocolSession) throws IOException {
        final boolean appReady;
        this.session.getLock().lock();
        try {
            appReady = (this.appEventMask & SelectionKey.OP_WRITE) > 0
                    && this.status.compareTo(Status.CLOSED) < 0
                    && this.sslEngine.getHandshakeStatus() == HandshakeStatus.NOT_HANDSHAKING;
        } finally {
            this.session.getLock().unlock();
        }
        if (appReady) {
            ensureHandler().outputReady(protocolSession);
        }
    }

    @Override
    public int write(final ByteBuffer src) throws IOException {
        Args.notNull(src, "Byte buffer");
        this.session.getLock().lock();
        try {
            if (this.status != Status.ACTIVE) {
                throw new ClosedChannelException();
            }
            if (this.handshakeStateRef.get() == TLSHandShakeState.READY) {
                return 0;
            }

            for (;;) {
                final ByteBuffer outEncryptedBuf = this.outEncrypted.acquire();
                final SSLEngineResult result = doWrap(src, outEncryptedBuf);
                if (result.getStatus() == SSLEngineResult.Status.BUFFER_OVERFLOW) {
                    // We don't release the buffer here, it will be expanded (if needed)
                    // and returned by the next attempt of SSLManagedBuffer#acquire() call.
                    this.outEncrypted.ensureWriteable(packetBufferSize);
                } else {
                    return result.bytesConsumed();
                }
            }
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public int read(final ByteBuffer dst) {
        return endOfStream ? -1 : 0;
    }

    @Override
    public String getId() {
        return session.getId();
    }

    @Override
    public Lock getLock() {
        return this.session.getLock();
    }

    @Override
    public void upgrade(final IOEventHandler handler) {
        this.session.upgrade(handler);
    }

    public TlsDetails getTlsDetails() {
        return tlsDetails;
    }

    @Override
    public boolean isOpen() {
        return this.status == Status.ACTIVE && this.session.isOpen();
    }

    @Override
    public void close() {
        close(CloseMode.GRACEFUL);
    }

    @Override
    public void close(final CloseMode closeMode) {
        this.session.getLock().lock();
        try {
            appClosed = true;
            if (closeMode == CloseMode.GRACEFUL) {
                if (this.status.compareTo(Status.CLOSING) >= 0) {
                    return;
                }
                this.status = Status.CLOSING;
                if (this.session.getSocketTimeout().isDisabled()) {
                    this.session.setSocketTimeout(Timeout.ofMilliseconds(1000));
                }
                try {
                    // Catch all unchecked exceptions in case something goes wrong
                    // in the JSSE provider. For instance
                    // com.android.org.conscrypt.NativeCrypto#SSL_get_shutdown can
                    // throw NPE at this point
                    doHandshake(this);
                    sendEncryptedData();
                    updateEventMask();
                } catch (final CancelledKeyException ex) {
                    this.session.close(CloseMode.GRACEFUL);
                } catch (final Exception ex) {
                    this.session.close(CloseMode.IMMEDIATE);
                }
            } else {
                if (this.status == Status.CLOSED) {
                    return;
                }
                this.inEncrypted.release();
                this.outEncrypted.release();
                this.inPlain.release();

                this.status = Status.CLOSED;
                this.session.close(closeMode);
            }
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public Status getStatus() {
        return this.status;
    }

    @Override
    public void enqueue(final Command command, final Command.Priority priority) {
        this.session.getLock().lock();
        try {
            this.session.enqueue(command, priority);
            if (isOpen()) {
                setEvent(SelectionKey.OP_WRITE);
            } else {
                command.cancel();
            }
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public boolean hasCommands() {
        return this.session.hasCommands();
    }

    @Override
    public Command poll() {
        return this.session.poll();
    }

    @Override
    public ByteChannel channel() {
        return this.session.channel();
    }

    @Override
    public SocketAddress getLocalAddress() {
        return this.session.getLocalAddress();
    }

    @Override
    public SocketAddress getRemoteAddress() {
        return this.session.getRemoteAddress();
    }

    @Override
    public int getEventMask() {
        this.session.getLock().lock();
        try {
            return this.appEventMask;
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public void setEventMask(final int ops) {
        this.session.getLock().lock();
        try {
            this.appEventMask = ops;
            updateEventMask();
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public void setEvent(final int op) {
        this.session.getLock().lock();
        try {
            this.appEventMask = this.appEventMask | op;
            updateEventMask();
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public void clearEvent(final int op) {
        this.session.getLock().lock();
        try {
            this.appEventMask = this.appEventMask & ~op;
            updateEventMask();
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public Timeout getSocketTimeout() {
        return this.session.getSocketTimeout();
    }

    @Override
    public void setSocketTimeout(final Timeout timeout) {
        this.socketTimeout = timeout;
        if (this.sslEngine.getHandshakeStatus() == HandshakeStatus.FINISHED) {
            this.session.setSocketTimeout(timeout);
        }
    }

    @Override
    public void updateReadTime() {
        this.session.updateReadTime();
    }

    @Override
    public void updateWriteTime() {
        this.session.updateWriteTime();
    }

    @Override
    public long getLastReadTime() {
        return this.session.getLastReadTime();
    }

    @Override
    public long getLastWriteTime() {
        return this.session.getLastWriteTime();
    }

    @Override
    public long getLastEventTime() {
        return this.session.getLastEventTime();
    }

    private static void formatOps(final StringBuilder buffer, final int ops) {
        if ((ops & SelectionKey.OP_READ) > 0) {
            buffer.append('r');
        }
        if ((ops & SelectionKey.OP_WRITE) > 0) {
            buffer.append('w');
        }
    }

    @Override
    public String toString() {
        this.session.getLock().lock();
        try {
            final StringBuilder buffer = new StringBuilder();
            buffer.append(this.session);
            buffer.append("[");
            buffer.append(this.status);
            buffer.append("][");
            formatOps(buffer, this.appEventMask);
            buffer.append("][");
            buffer.append(this.sslEngine.getHandshakeStatus());
            if (this.sslEngine.isInboundDone()) {
                buffer.append("][inbound done][");
            }
            if (this.sslEngine.isOutboundDone()) {
                buffer.append("][outbound done][");
            }
            if (this.endOfStream) {
                buffer.append("][EOF][");
            }
            buffer.append("][");
            buffer.append(!this.inEncrypted.hasData() ? 0 : inEncrypted.acquire().position());
            buffer.append("][");
            buffer.append(!this.inPlain.hasData() ? 0 : inPlain.acquire().position());
            buffer.append("][");
            buffer.append(!this.outEncrypted.hasData() ? 0 : outEncrypted.acquire().position());
            buffer.append("]");
            return buffer.toString();
        } finally {
            this.session.getLock().unlock();
        }
    }

    @Override
    public int getPendingCommandCount() {
        return this.session.getPendingCommandCount();
    }
}
