/*
 * Copyright 2017 The Netty Project
 *
 * The Netty Project 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 io.netty.microbench.handler.ssl;

import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufAllocator;
import io.netty.channel.ChannelHandler;
import io.netty.handler.codec.ByteToMessageDecoder;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.ssl.SslProvider;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.netty.microbench.channel.EmbeddedChannelWriteAccumulatingHandlerContext;
import io.netty.microbench.util.AbstractMicrobenchmark;
import io.netty.util.ReferenceCountUtil;
import org.openjdk.jmh.annotations.Param;

import java.io.File;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;

import static io.netty.handler.codec.ByteToMessageDecoder.COMPOSITE_CUMULATOR;
import static org.junit.Assert.assertNull;

public class AbstractSslHandlerBenchmark extends AbstractMicrobenchmark {
    private static final String PROTOCOL_TLS_V1_2 = "TLSv1.2";

    public enum SslEngineProvider {
        JDK {
            @Override
            SslProvider sslProvider() {
                return SslProvider.JDK;
            }
        },
        OPENSSL {
            @Override
            SslProvider sslProvider() {
                return SslProvider.OPENSSL;
            }
        },
        OPENSSL_REFCNT {
            @Override
            SslProvider sslProvider() {
                return SslProvider.OPENSSL_REFCNT;
            }
        };
        private final SslContext clientContext = newClientContext();
        private final SslContext serverContext = newServerContext();

        private SslContext newClientContext() {
            try {
                return SslContextBuilder.forClient()
                        .sslProvider(sslProvider())
                        .trustManager(InsecureTrustManagerFactory.INSTANCE)
                        .build();
            } catch (SSLException e) {
                throw new IllegalStateException(e);
            }
        }

        private SslContext newServerContext() {
            try {
                File keyFile = new File(getClass().getResource("test_unencrypted.pem").getFile());
                File crtFile = new File(getClass().getResource("test.crt").getFile());

                return SslContextBuilder.forServer(crtFile, keyFile)
                        .sslProvider(sslProvider())
                        .build();
            } catch (Exception e) {
                throw new IllegalStateException(e);
            }
        }

        SslHandler newClientHandler(ByteBufAllocator allocator, String cipher) {
            SslHandler handler = clientContext.newHandler(allocator);
            configureEngine(handler.engine(), cipher);
            return handler;
        }

        SslHandler newServerHandler(ByteBufAllocator allocator, String cipher) {
            SslHandler handler = serverContext.newHandler(allocator);
            configureEngine(handler.engine(), cipher);
            return handler;
        }

        abstract SslProvider sslProvider();

        static SSLEngine configureEngine(SSLEngine engine, String cipher) {
            engine.setEnabledProtocols(new String[]{ PROTOCOL_TLS_V1_2 });
            engine.setEnabledCipherSuites(new String[]{ cipher });
            return engine;
        }
    }

    @Param
    public SslEngineProvider sslProvider;

    // Includes cipher required by HTTP/2
    @Param({ "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" })
    public String cipher;

    protected SslHandler clientSslHandler;
    protected SslHandler serverSslHandler;
    protected EmbeddedChannelWriteAccumulatingHandlerContext clientCtx;
    protected EmbeddedChannelWriteAccumulatingHandlerContext serverCtx;

    protected final void initSslHandlers(ByteBufAllocator allocator) {
        clientSslHandler = newClientHandler(allocator);
        serverSslHandler = newServerHandler(allocator);
        clientCtx = new SslThroughputBenchmarkHandlerContext(allocator, clientSslHandler, COMPOSITE_CUMULATOR);
        serverCtx = new SslThroughputBenchmarkHandlerContext(allocator, serverSslHandler, COMPOSITE_CUMULATOR);
    }

    protected final void destroySslHandlers() {
        try {
            if (clientSslHandler != null) {
                ReferenceCountUtil.release(clientSslHandler.engine());
            }
        } finally {
            if (serverSslHandler != null) {
                ReferenceCountUtil.release(serverSslHandler.engine());
            }
        }
    }

    protected final void doHandshake() throws Exception {
        serverSslHandler.handlerAdded(serverCtx);
        clientSslHandler.handlerAdded(clientCtx);
        do {
            ByteBuf clientCumulation = clientCtx.cumulation();
            if (clientCumulation != null) {
                serverSslHandler.channelRead(serverCtx, clientCumulation.retain());
                clientCtx.releaseCumulation();
            }
            ByteBuf serverCumulation = serverCtx.cumulation();
            if (serverCumulation != null) {
                clientSslHandler.channelRead(clientCtx, serverCumulation.retain());
                serverCtx.releaseCumulation();
            }
        } while (!clientSslHandler.handshakeFuture().isDone() || !serverSslHandler.handshakeFuture().isDone());
    }

    protected final SslHandler newClientHandler(ByteBufAllocator allocator) {
        return sslProvider.newClientHandler(allocator, cipher);
    }

    protected final SslHandler newServerHandler(ByteBufAllocator allocator) {
        return sslProvider.newServerHandler(allocator, cipher);
    }

    private static final class SslThroughputBenchmarkHandlerContext extends
            EmbeddedChannelWriteAccumulatingHandlerContext {
        SslThroughputBenchmarkHandlerContext(ByteBufAllocator alloc, ChannelHandler handler,
                                                    ByteToMessageDecoder.Cumulator writeCumulator) {
            super(alloc, handler, writeCumulator);
        }

        @Override
        protected void handleException(Throwable t) {
            handleUnexpectedException(t);
        }
    }

    public static void handleUnexpectedException(Throwable t) {
        assertNull(t);
    }
}
