/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://aws.amazon.com/apache2.0
 *
 * or in the "license" file accompanying this file. This file 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 software.amazon.awssdk.http.nio.netty;

import io.netty.channel.Channel;
import io.netty.channel.ChannelFactory;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import java.util.Optional;
import java.util.concurrent.ThreadFactory;
import software.amazon.awssdk.annotations.SdkPublicApi;
import software.amazon.awssdk.http.nio.netty.internal.utils.ChannelResolver;
import software.amazon.awssdk.utils.ThreadFactoryBuilder;
import software.amazon.awssdk.utils.Validate;

/**
 * Provides {@link EventLoopGroup} and {@link ChannelFactory} for {@link NettyNioAsyncHttpClient}.
 * <p>
 * There are three ways to create a new instance.
 *
 * <ul>
 * <li>using {@link #builder()} to provide custom configuration of {@link EventLoopGroup}.
 * This is the preferred configuration method when you just want to customize the {@link EventLoopGroup}</li>
 *
 *
 * <li>Using {@link #create(EventLoopGroup)} to provide a custom {@link EventLoopGroup}. {@link ChannelFactory} will
 * be resolved based on the type of {@link EventLoopGroup} provided via
 * {@link ChannelResolver#resolveSocketChannelFactory(EventLoopGroup)} and
 * {@link ChannelResolver#resolveDatagramChannelFactory(EventLoopGroup)}
 * </li>
 *
 * <li>Using {@link #create(EventLoopGroup, ChannelFactory)} to provide a custom {@link EventLoopGroup} and
 * {@link ChannelFactory}
 * </ul>
 *
 * <p>
 * When configuring the {@link EventLoopGroup} of {@link NettyNioAsyncHttpClient}, if {@link SdkEventLoopGroup.Builder} is
 * passed to {@link NettyNioAsyncHttpClient.Builder#eventLoopGroupBuilder},
 * the {@link EventLoopGroup} is managed by the SDK and will be shutdown when the HTTP client is closed. Otherwise,
 * if an instance of {@link SdkEventLoopGroup} is passed to {@link NettyNioAsyncHttpClient.Builder#eventLoopGroup},
 * the {@link EventLoopGroup} <b>MUST</b> be closed by the caller when it is ready to be disposed. The SDK will not
 * close the {@link EventLoopGroup} when the HTTP client is closed. See {@link EventLoopGroup#shutdownGracefully()} to
 * properly close the event loop group.
 *
 * @see NettyNioAsyncHttpClient.Builder#eventLoopGroupBuilder(Builder)
 * @see NettyNioAsyncHttpClient.Builder#eventLoopGroup(SdkEventLoopGroup)
 */
@SdkPublicApi
public final class SdkEventLoopGroup {

    private final EventLoopGroup eventLoopGroup;
    private final ChannelFactory<? extends Channel> channelFactory;
    private final ChannelFactory<? extends DatagramChannel> datagramChannelFactory;

    SdkEventLoopGroup(EventLoopGroup eventLoopGroup, ChannelFactory<? extends Channel> channelFactory) {
        Validate.paramNotNull(eventLoopGroup, "eventLoopGroup");
        Validate.paramNotNull(channelFactory, "channelFactory");
        this.eventLoopGroup = eventLoopGroup;
        this.channelFactory = channelFactory;
        this.datagramChannelFactory = ChannelResolver.resolveDatagramChannelFactory(eventLoopGroup);
    }

    /**
     * Create an instance of {@link SdkEventLoopGroup} from the builder
     */
    private SdkEventLoopGroup(DefaultBuilder builder) {
        this.eventLoopGroup = resolveEventLoopGroup(builder);
        this.channelFactory = resolveSocketChannelFactory(builder);
        this.datagramChannelFactory = resolveDatagramChannelFactory(builder);
    }

    /**
     * @return the {@link EventLoopGroup} to be used with Netty Http client.
     */
    public EventLoopGroup eventLoopGroup() {
        return eventLoopGroup;
    }

    /**
     * @return the {@link ChannelFactory} to be used with Netty Http Client.
     */
    public ChannelFactory<? extends Channel> channelFactory() {
        return channelFactory;
    }

    /**
     * @return the {@link ChannelFactory} for datagram channels to be used with Netty Http Client.
     */
    public ChannelFactory<? extends DatagramChannel> datagramChannelFactory() {
        return datagramChannelFactory;
    }

    /**
     * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup} and {@link ChannelFactory}
     * to be used with {@link NettyNioAsyncHttpClient}.
     *
     * @param eventLoopGroup the EventLoopGroup to be used
     * @param channelFactory the channel factor to be used
     * @return a new instance of SdkEventLoopGroup
     */
    public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup, ChannelFactory<? extends Channel> channelFactory) {
        return new SdkEventLoopGroup(eventLoopGroup, channelFactory);
    }

    /**
     * Creates a new instance of SdkEventLoopGroup with {@link EventLoopGroup}.
     *
     * <p>
     * {@link ChannelFactory} will be resolved based on the type of {@link EventLoopGroup} provided. IllegalArgumentException will
     * be thrown for any unknown EventLoopGroup type.
     *
     * @param eventLoopGroup the EventLoopGroup to be used
     * @return a new instance of SdkEventLoopGroup
     */
    public static SdkEventLoopGroup create(EventLoopGroup eventLoopGroup) {
        return create(eventLoopGroup, ChannelResolver.resolveSocketChannelFactory(eventLoopGroup));
    }

    public static Builder builder() {
        return new DefaultBuilder();
    }

    private EventLoopGroup resolveEventLoopGroup(DefaultBuilder builder) {
        int numThreads = Optional.ofNullable(builder.numberOfThreads).orElse(0);
        ThreadFactory threadFactory = Optional.ofNullable(builder.threadFactory)
                                              .orElseGet(() -> new ThreadFactoryBuilder()
                                                  .threadNamePrefix("aws-java-sdk-NettyEventLoop")
                                                  .build());
        return new NioEventLoopGroup(numThreads, threadFactory);
        /*
        Need to investigate why epoll is raising channel inactive after successful response that causes
        problems with retries.

        if (Epoll.isAvailable() && isNotAwsLambda()) {
            return new EpollEventLoopGroup(numThreads, resolveThreadFactory());
        } else {

        }*/
    }

    private ChannelFactory<? extends Channel> resolveSocketChannelFactory(DefaultBuilder builder) {
        return builder.channelFactory;
    }

    private ChannelFactory<? extends DatagramChannel> resolveDatagramChannelFactory(DefaultBuilder builder) {
        return builder.datagramChannelFactory;
    }

    private static ChannelFactory<? extends Channel> defaultSocketChannelFactory() {
        return NioSocketChannel::new;
    }

    private static ChannelFactory<? extends DatagramChannel> defaultDatagramChannelFactory() {
        return NioDatagramChannel::new;
    }

    /**
     * A builder for {@link SdkEventLoopGroup}.
     *
     * <p>All implementations of this interface are mutable and not thread safe.
     */
    public interface Builder {

        /**
         * Number of threads to use for the {@link EventLoopGroup}. If not set, the default
         * Netty thread count is used (which is double the number of available processors unless the io.netty.eventLoopThreads
         * system property is set.
         *
         * @param numberOfThreads Number of threads to use.
         * @return This builder for method chaining.
         */
        Builder numberOfThreads(Integer numberOfThreads);

        /**
         * {@link ThreadFactory} to create threads used by the {@link EventLoopGroup}. If not set,
         * a generic thread factory is used.
         *
         * @param threadFactory ThreadFactory to use.
         * @return This builder for method chaining.
         */
        Builder threadFactory(ThreadFactory threadFactory);

        /**
         * {@link ChannelFactory} to create socket channels used by the {@link EventLoopGroup}. If not set,
         * NioSocketChannel is used.
         *
         * @param channelFactory ChannelFactory to use.
         * @return This builder for method chaining.
         */
        Builder channelFactory(ChannelFactory<? extends Channel> channelFactory);

        /**
         * {@link ChannelFactory} to create datagram channels used by the {@link EventLoopGroup}. If not set,
         * NioDatagramChannel is used.
         *
         * @param datagramChannelFactory ChannelFactory to use.
         * @return This builder for method chaining.
         */
        Builder datagramChannelFactory(ChannelFactory<? extends DatagramChannel> datagramChannelFactory);

        SdkEventLoopGroup build();
    }

    private static final class DefaultBuilder implements Builder {

        private Integer numberOfThreads;
        private ThreadFactory threadFactory;
        private ChannelFactory<? extends Channel> channelFactory = defaultSocketChannelFactory();
        private ChannelFactory<? extends DatagramChannel> datagramChannelFactory = defaultDatagramChannelFactory();

        private DefaultBuilder() {
        }

        @Override
        public Builder numberOfThreads(Integer numberOfThreads) {
            this.numberOfThreads = numberOfThreads;
            return this;
        }

        public void setNumberOfThreads(Integer numberOfThreads) {
            numberOfThreads(numberOfThreads);
        }

        @Override
        public Builder threadFactory(ThreadFactory threadFactory) {
            this.threadFactory = threadFactory;
            return this;
        }

        public void setThreadFactory(ThreadFactory threadFactory) {
            threadFactory(threadFactory);
        }

        @Override
        public Builder channelFactory(ChannelFactory<? extends Channel> channelFactory) {
            this.channelFactory = channelFactory;
            return this;
        }

        public void setChannelFactory(ChannelFactory<? extends Channel> channelFactory) {
            channelFactory(channelFactory);
        }

        @Override
        public Builder datagramChannelFactory(ChannelFactory<? extends DatagramChannel> datagramChannelFactory) {
            this.datagramChannelFactory = datagramChannelFactory;
            return this;
        }

        public void setDatagramChannelFactory(ChannelFactory<? extends DatagramChannel> datagramChannelFactory) {
            datagramChannelFactory(datagramChannelFactory);
        }

        @Override
        public SdkEventLoopGroup build() {
            return new SdkEventLoopGroup(this);
        }
    }
}
