/*
 * 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
 *
 *   https://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.plc4x.java.spi.connection;

import io.netty.bootstrap.Bootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandler;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import org.apache.plc4x.java.api.exceptions.PlcConnectionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.net.SocketAddress;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * Adapter with sensible defaults for a Netty Based Channel Factory.
 * <p>
 * By Default Nettys {@link NioEventLoopGroup} is used.
 * Transports which have to use a different EventLoopGroup have to override {@link #getEventLoopGroup()}.
 */
public abstract class NettyChannelFactory implements ChannelFactory {

    private static final Logger logger = LoggerFactory.getLogger(NettyChannelFactory.class);

    private final Map<Channel, EventLoopGroup> eventLoops = new ConcurrentHashMap<>();

    private final SocketAddress localAddress;
    private final SocketAddress remoteAddress;

    protected NettyChannelFactory(SocketAddress remoteAddress) {
        this(null, remoteAddress);
    }

    protected NettyChannelFactory(SocketAddress localAddress, SocketAddress remoteAddress) {
        this.localAddress = localAddress;
        this.remoteAddress = remoteAddress;
    }

    /**
     * Channel to Use, e.g. NiO, EiO
     */
    public abstract Class<? extends Channel> getChannel();

    /**
     * We need to be able to override this in the TestChanelFactory
     *
     * @return the Bootstrap instance we will be using to initialize the channel.
     */
    protected Bootstrap createBootstrap() {
        return new Bootstrap();
    }

    /**
     * This Method is used to modify the Bootstrap Element of Netty, if one wishes to do so.
     * E.g. for Protocol Specific extension.
     * For TCP e.g.
     * <code>
     * bootstrap.option(ChannelOption.SO_KEEPALIVE, true);
     * bootstrap.option(ChannelOption.TCP_NODELAY, true);
     * </code>
     */
    public abstract void configureBootstrap(Bootstrap bootstrap);

    /**
     * Event Loop Group to use.
     * Has to be in accordance with {@link #getChannel()}
     * otherwise a Runtime Exception will be produced by Netty
     * <p>
     * By Default Netty's {@link NioEventLoopGroup} is used.
     * Transports which have to use a different EventLoopGroup have to override {#getEventLoopGroup()}.
     */
    public EventLoopGroup getEventLoopGroup() {
        // NOTE: Per default netty creates two threads per cpu core. However, we are not running a server but usually
        // are expecting only a request for a response, Netty also cleans up idle threads in the group, so we're
        // setting this to a low number as a test, if this proves to not cause issues, we'll leave it. If there are
        // issues, I hope this note helps track them down.
        return new NioEventLoopGroup(1);
    }

    @Override
    public Channel createChannel(ChannelHandler channelHandler) throws PlcConnectionException {
        try {
            Bootstrap bootstrap = createBootstrap();

            EventLoopGroup workerGroup = getEventLoopGroup();
            if (workerGroup != null) {
                bootstrap.group(workerGroup);
            }

            bootstrap.channel(getChannel());
            // Callback to allow subclasses to modify the Bootstrap

            configureBootstrap(bootstrap);
            bootstrap.handler(channelHandler);
            // Start the client.
            final ChannelFuture f = (localAddress == null) ?
                bootstrap.connect(remoteAddress) : bootstrap.connect(remoteAddress, localAddress);
            f.addListener(future -> {
                if (!future.isSuccess()) {
                    logger.info("Unable to connect, shutting down worker thread.");
                    if (workerGroup != null) {
                        workerGroup.shutdownGracefully();
                    }
                }
            });

            final Channel channel = f.channel();

            if (workerGroup != null) {
                // Shut down the workerGroup when channel closing to avoid open too many files
                channel.closeFuture().addListener(future -> workerGroup.shutdownGracefully());
                // Add to event-loop group
                eventLoops.put(channel, workerGroup);
            }

            // It seems the embedded channel operates differently.
            // Intentionally using the class name as we don't want to require a
            // hard dependency on the test-channel.
            if (!"Plc4xEmbeddedChannel".equals(channel.getClass().getSimpleName())) {
                // Wait for sync
                f.sync();
                // Wait till the session is finished initializing.
                f.awaitUninterruptibly(); // jf: unsure if we need that
            }

            return channel;
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            throw new PlcConnectionException("Error creating channel.", e);
        } catch (Throwable t) {
            t.printStackTrace();
            throw new PlcConnectionException("Error creating channel.", t);
        }
    }

    @Override
    public void closeEventLoopForChannel(Channel channel) {
        if (eventLoops.containsKey(channel)) {
            logger.info("Channel is closed, closing worker Group also");
            EventLoopGroup eventExecutors = eventLoops.get(channel);
            eventLoops.remove(channel);
            eventExecutors.shutdownGracefully().awaitUninterruptibly(2000);
            logger.info("Worker Group was closed successfully!");
        } else {
            logger.warn("Trying to remove EventLoop for Channel {} but have none stored", channel);
        }
    }

}
