package io.ripc.transport.netty4.tcp;

import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelPromise;
import io.netty.util.ReferenceCountUtil;
import io.ripc.protocol.tcp.TcpHandler;
import org.reactivestreams.Publisher;
import org.reactivestreams.Subscriber;
import org.reactivestreams.Subscription;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * A bridge between netty's {@link Channel} and {@link io.ripc.protocol.tcp.TcpConnection}. It has the following
 * responsibilities:
 * <p>
 * <ul>
 * <li>Create a new {@link io.ripc.protocol.tcp.TcpConnection} instance when the channel is active and forwards it to
 * the configured
 * {@link TcpHandler}.</li>
 * <li>Reads any data from the channel and forwards it to the {@link Subscriber} attached via the event
 * {@link ChannelToConnectionBridge.ConnectionInputSubscriberEvent}</li>
 * <li>Accepts writes of {@link Publisher} on the channel and translates the items emitted from that publisher to the
 * channel.</li>
 * </ul>
 *
 * @param <R> The type of objects read from the underneath channel.
 * @param <W> The type of objects read written to the underneath channel.
 */
public class ChannelToConnectionBridge<R, W> extends ChannelDuplexHandler {

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

    private final TcpHandler<R, W>        handler;
    private       TcpConnectionImpl<R, W> conn;
    private       Subscriber<R>           inputSubscriber; /*Populated via event ConnectionInputSubscriberEvent*/

    public ChannelToConnectionBridge(TcpHandler<R, W> handler) {
        this.handler = handler;
    }

    @Override
    public void channelActive(final ChannelHandlerContext ctx) throws Exception {
        super.channelActive(ctx);
        conn = new TcpConnectionImpl<>(ctx.channel());
        handler.handle(conn)
            .subscribe(new Subscriber<Void>() {
                @Override
                public void onSubscribe(Subscription s) {
                    s.request(Long.MAX_VALUE); //no op
                }

                @Override
                public void onNext(Void aVoid) {
                    // Void, no op
                }

                @Override
                public void onError(Throwable t) {
                    logger.error("Error processing connection. Closing the channel.", t);
                    ctx.channel().close();
                }

                @Override
                public void onComplete() {
                    ctx.channel().close();
                }
            });
    }

    @SuppressWarnings("unchecked")
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (null == conn || null == inputSubscriber) {
            logger.error("No connection input subscriber available. Disposing data.");
            ReferenceCountUtil.release(msg);
            return;
        }

        try {
            inputSubscriber.onNext((R) msg);
        } catch (ClassCastException e) {
            logger.error("Invalid message type read from the pipeline.", e);
            inputSubscriber.onError(e);
        }
    }

    @Override
    public void channelInactive(ChannelHandlerContext ctx) throws Exception {
        if (null != conn && inputSubscriber != null) {
            inputSubscriber.onComplete();
        }
        super.channelInactive(ctx);
    }

    @Override
    public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
        if (evt instanceof ConnectionInputSubscriberEvent) {
            @SuppressWarnings("unchecked")
            ConnectionInputSubscriberEvent<R> subscriberEvent = (ConnectionInputSubscriberEvent<R>) evt;
            if (null == inputSubscriber) {
                inputSubscriber = subscriberEvent.getInputSubscriber();
                subscriberEvent.init(ctx);
            } else {
                inputSubscriber.onError(new IllegalStateException("Only one connection input subscriber allowed."));
            }
        }
        super.userEventTriggered(ctx, evt);
    }

    @Override
    public void write(final ChannelHandlerContext ctx, Object msg, final ChannelPromise promise) throws Exception {
        if (msg instanceof Publisher) {
            @SuppressWarnings("unchecked")
            final Publisher<W> data = (Publisher<W>) msg;

            data.subscribe(new Subscriber<W>() {

                // TODO: Needs to be fixed to wire all futures to the promise of the Publisher write.
                private ChannelFuture lastWriteFuture;

                @Override
                public void onSubscribe(Subscription s) {
                    s.request(Long.MAX_VALUE); // TODO: Backpressure
                }

                @Override
                public void onNext(W w) {
                    lastWriteFuture = ctx.channel().write(w);
                }

                @Override
                public void onError(Throwable t) {
                    onTerminate();
                }

                @Override
                public void onComplete() {
                    onTerminate();
                }

                private void onTerminate() {
                    ctx.channel().flush();
                    lastWriteFuture.addListener(new ChannelFutureListener() {
                        @Override
                        public void operationComplete(ChannelFuture future) throws Exception {
                            if (future.isSuccess()) {
                                promise.trySuccess();
                            } else {
                                promise.tryFailure(future.cause());
                            }
                        }
                    });
                }
            });
        } else {
            super.write(ctx, msg, promise);
        }
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
        logger.error(cause.getMessage(), cause);
    }

    /**
     * An event to attach a {@link Subscriber} to the {@link io.ripc.protocol.tcp.TcpConnection} created by {@link
     * ChannelToConnectionBridge}
     *
     * @param <R>
     */
    public static final class ConnectionInputSubscriberEvent<R> {

        private final Subscriber<R> inputSubscriber;

        public ConnectionInputSubscriberEvent(Subscriber<R> inputSubscriber) {
            if (null == inputSubscriber) {
                throw new IllegalArgumentException("Connection input subscriber must not be null.");
            }
            this.inputSubscriber = inputSubscriber;
        }

        public Subscriber<R> getInputSubscriber() {
            return inputSubscriber;
        }

        void init(ChannelHandlerContext ctx) {
            try {
                inputSubscriber.onSubscribe(new Subscription() {
                    @Override
                    public void request(long n) {
            /*if(n == Long.MAX_VALUE){
                            ctx.channel().config().setAutoRead(true);
						}*/
                        //ctx.read(); implements backpressure
                        ctx.channel().config().setAutoRead(true);
                    }

                    @Override
                    public void cancel() {
                        //implements close on cancel (must be after any pending onComplete)
                    }
                });
            } catch (Throwable error) {
                inputSubscriber.onError(error);
            }
        }
    }
}