package io.joyrpc.transport.netty4.transport;

/*-
 * #%L
 * joyrpc
 * %%
 * Copyright (C) 2019 joyrpc.io
 * %%
 * Licensed 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.
 * #L%
 */

import io.joyrpc.constants.Constants;
import io.joyrpc.event.AsyncResult;
import io.joyrpc.exception.ConnectionException;
import io.joyrpc.extension.URL;
import io.joyrpc.transport.channel.Channel;
import io.joyrpc.transport.codec.AdapterContext;
import io.joyrpc.transport.netty4.channel.NettyChannel;
import io.joyrpc.transport.netty4.channel.NettyServerChannel;
import io.joyrpc.transport.netty4.codec.ProtocolAdapterContext;
import io.joyrpc.transport.netty4.handler.ConnectionChannelHandler;
import io.joyrpc.transport.netty4.handler.ProtocolAdapterDecoder;
import io.joyrpc.transport.netty4.ssl.SslContextManager;
import io.joyrpc.transport.transport.AbstractServerTransport;
import io.joyrpc.transport.transport.ChannelTransport;
import io.joyrpc.transport.transport.ServerTransport;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.*;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.ssl.SslContext;

import java.net.InetSocketAddress;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;

/**
 * 服务端
 *
 * @date: 2019/2/21
 */
public class NettyServerTransport extends AbstractServerTransport {

    protected final BiFunction<Channel, URL, ChannelTransport> function;

    protected final Supplier<List<Channel>> supplier = this::getChannels;

    /**
     * 构造函数
     *
     * @param url
     * @param function
     */
    public NettyServerTransport(final URL url,
                                final BiFunction<Channel, URL, ChannelTransport> function) {
        super(url);
        this.function = function;
    }

    /**
     * 构造函数
     *
     * @param url
     * @param beforeOpen
     * @param afterClose
     * @param function
     */
    public NettyServerTransport(final URL url,
                                final Function<ServerTransport, CompletableFuture<Void>> beforeOpen,
                                final Function<ServerTransport, CompletableFuture<Void>> afterClose,
                                final BiFunction<Channel, URL, ChannelTransport> function) {
        super(url, beforeOpen, afterClose);
        this.function = function;
    }

    @Override
    protected void bind(final String host, final int port, final Consumer<AsyncResult<Channel>> consumer) {
        //消费者不会为空
        if (codec == null && adapter == null) {
            consumer.accept(new AsyncResult<>(new ConnectionException(
                    String.format("Failed binding server at %s:%d, caused by codec or adapter can not be null!",
                            host, port))));
        } else {
            try {
                SslContext sslContext = SslContextManager.getServerSslContext(url);
                EventLoopGroup bossGroup = EventLoopGroupFactory.getBossGroup(url);
                EventLoopGroup workerGroup = EventLoopGroupFactory.getWorkerGroup(url);
                ServerBootstrap bootstrap = configure(new ServerBootstrap().group(bossGroup, workerGroup), sslContext);
                bootstrap.bind(new InetSocketAddress(host, port)).addListener((ChannelFutureListener) f -> {
                    NettyServerChannel channel = new NettyServerChannel(f.channel(), bossGroup, workerGroup, supplier);
                    if (f.isSuccess()) {
                        consumer.accept(new AsyncResult<>(channel));
                    } else {
                        //自动解绑
                        Throwable error = f.cause();
                        channel.close(o -> consumer.accept(new AsyncResult<>(
                                new ConnectionException(
                                        String.format("Failed binding server at %s:%d, caused by %s",
                                                host, port, error.getMessage()), error))));
                    }
                });
            } catch (Throwable e) {
                consumer.accept(new AsyncResult<>(e));
            }
        }
    }

    /**
     * 配置
     *
     * @param bootstrap
     * @param sslContext
     */
    protected ServerBootstrap configure(final ServerBootstrap bootstrap, final SslContext sslContext) {
        //io.netty.bootstrap.Bootstrap - Unknown channel option 'SO_BACKLOG' for channel
        bootstrap.channel(Constants.isUseEpoll(url) ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                .childHandler(new MyChannelInitializer(url, sslContext))
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, url.getPositiveInt(Constants.CONNECT_TIMEOUT_OPTION))
                .option(ChannelOption.SO_REUSEADDR, url.getBoolean(Constants.SO_REUSE_PORT_OPTION))
                .option(ChannelOption.SO_BACKLOG, url.getPositiveInt(Constants.SO_BACKLOG_OPTION))
                .option(ChannelOption.RCVBUF_ALLOCATOR, AdaptiveRecvByteBufAllocator.DEFAULT)
                .option(ChannelOption.WRITE_BUFFER_WATER_MARK, new WriteBufferWaterMark(url.getPositiveInt(Constants.WRITE_BUFFER_LOW_WATERMARK_OPTION),
                        url.getPositiveInt(Constants.WRITE_BUFFER_HIGH_WATERMARK_OPTION)))
                .childOption(ChannelOption.SO_RCVBUF, url.getPositiveInt(Constants.SO_RECEIVE_BUF_OPTION))
                .childOption(ChannelOption.SO_SNDBUF, url.getPositiveInt(Constants.SO_SEND_BUF_OPTION))
                .childOption(ChannelOption.SO_KEEPALIVE, url.getBoolean(Constants.SO_KEEPALIVE_OPTION))
                .childOption(ChannelOption.TCP_NODELAY, url.getBoolean(Constants.TCP_NODELAY))
                .childOption(ChannelOption.ALLOCATOR, BufAllocator.create(url));

        return bootstrap;
    }

    /**
     * 通道初始化
     */
    protected class MyChannelInitializer extends ChannelInitializer<SocketChannel> {
        /**
         * URL
         */
        protected URL url;
        /**
         * SSL上下文
         */
        protected SslContext sslContext;

        /**
         * 构造函数
         *
         * @param url
         * @param sslContext
         */
        public MyChannelInitializer(URL url, SslContext sslContext) {
            this.url = url;
            this.sslContext = sslContext;
        }

        @Override
        protected void initChannel(final SocketChannel ch) {
            //及时发送 与 缓存发送
            Channel channel = new NettyChannel(ch, true);
            //设置payload,添加业务线程池到channel
            channel.setAttribute(Channel.PAYLOAD, url.getPositiveInt(Constants.PAYLOAD))
                    .setAttribute(Channel.BIZ_THREAD_POOL, bizThreadPool, (k, v) -> v != null);
            if (sslContext != null) {
                ch.pipeline().addFirst("ssl", sslContext.newHandler(ch.alloc()));
            }
            ch.pipeline().addLast("connection", new ConnectionChannelHandler(channel, publisher) {
                @Override
                public void channelInactive(final ChannelHandlerContext ctx) throws Exception {
                    removeChannel(channel);
                    super.channelInactive(ctx);
                    logger.info(String.format("disconnect %s", ctx.channel().remoteAddress()));
                }
            });

            if (adapter != null) {
                ch.pipeline().addLast("adapter", new ProtocolAdapterDecoder(adapter, channel));
            } else {
                AdapterContext context = new ProtocolAdapterContext(channel, ch.pipeline());
                context.bind(codec, chain);
            }

            ChannelTransport transport = function.apply(channel, url);
            channel.setAttribute(Channel.CHANNEL_TRANSPORT, transport);
            channel.setAttribute(Channel.SERVER_CHANNEL, getServerChannel());
            addChannel(channel, transport);
        }
    }

}