package io.jpower.kcp.netty;

import static io.jpower.kcp.netty.Consts.sheduleUpdateLog;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.util.List;
import java.util.concurrent.TimeUnit;

import io.netty.buffer.ByteBuf;
import io.netty.channel.AbstractChannel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelMetadata;
import io.netty.channel.ChannelOutboundBuffer;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.nio.NioEventLoop;
import io.netty.util.internal.StringUtil;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.InternalLoggerFactory;

/**
 * @author <a href="mailto:[email protected]">szh</a>
 */
public final class UkcpClientChannel extends AbstractChannel implements UkcpChannel, Runnable {

    private static final InternalLogger log = InternalLoggerFactory.getInstance(UkcpClientChannel.class);

    private static final ChannelMetadata METADATA = new ChannelMetadata(false);

    private static final String EXPECTED_TYPES =
            " (expected: " + StringUtil.simpleClassName(ByteBuf.class) + ')';

    private final DefaultUkcpClientChannelConfig config;

    private final UkcpClientUdpChannel udpChannel;

    private final Ukcp ukcp;

    private final KcpOutput output = new UkcpClientOutput();

    private long tsUpdate = -1;

    private boolean flushPending;

    boolean closeAnother = false;

    public UkcpClientChannel() {
        super(null);
        this.udpChannel = new UkcpClientUdpChannel(this);
        this.ukcp = createUkcp();
        this.config = new DefaultUkcpClientChannelConfig(this, ukcp, udpChannel.javaChannel().socket());
    }

    private Ukcp createUkcp() {
        Ukcp ukcp = new Ukcp(0, output); // temp conv, need to set conv in outter
        ukcp.channel(this);

        return ukcp;
    }

    @Override
    public ChannelMetadata metadata() {
        return METADATA;
    }

    @Override
    public UkcpClientChannelConfig config() {
        return config;
    }

    @Override
    public UkcpClientUnsafe unsafe() {
        return (UkcpClientUnsafe) super.unsafe();
    }

    @Override
    protected UkcpClientUnsafe newUnsafe() {
        return new UkcpClientUnsafe();
    }

    @Override
    public boolean isOpen() {
        return udpChannel.isOpen();
    }

    @Override
    public boolean isActive() {
        return udpChannel.isActive();
    }

    @Override
    protected boolean isCompatible(EventLoop loop) {
        return loop instanceof NioEventLoop;
    }

    @Override
    protected SocketAddress localAddress0() {
        return udpChannel.localAddress();
    }

    @Override
    protected SocketAddress remoteAddress0() {
        return udpChannel.remoteAddress();
    }

    @Override
    protected void doRegister() throws Exception {
        eventLoop().register(udpChannel).addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture future) throws Exception {
                if (!future.isSuccess()) {
                    forceClose(future.cause());
                }
            }
        });
    }

    private void forceClose(Throwable t) {
        unsafe().closeForcibly();
        ((ChannelPromise) closeFuture()).trySuccess();
        log.warn("Failed to register an UkcpClientUdpChannel: {}", this, t);
    }

    @Override
    protected void doBind(SocketAddress localAddress) throws Exception {
        udpChannel.doBind(localAddress);
    }

    private boolean doConnect(SocketAddress remoteAddress, SocketAddress localAddress) throws Exception {
        return udpChannel.doConnect(remoteAddress, localAddress);
    }

    @Override
    protected void doDisconnect() throws Exception {
        udpChannel.doDisconnect();
    }

    @Override
    protected void doClose() throws Exception {
        ukcp.setClosed(true);
        if (!closeAnother) {
            closeAnother = true;
            udpChannel.unsafe().close(udpChannel.unsafe().voidPromise());
        }
    }

    @Override
    protected void doBeginRead() throws Exception {
        udpChannel.doBeginRead();
    }

    @Override
    protected void doWrite(ChannelOutboundBuffer in) throws Exception {
        boolean sent = false;
        for (; ; ) {
            Object msg = in.current();
            if (msg == null) {
                flushPending = false;
                break;
            }
            try {
                boolean done = false;
                if (kcpSend((ByteBuf) msg)) {
                    done = true;
                    sent = true;
                }

                if (done) {
                    in.remove();
                } else {
                    flushPending = true;
                    break;
                }
            } catch (IOException e) {
                throw e; // throw exception and close channel
            }
        }

        if (sent) {
            // update kcp
            if (ukcp.isFastFlush()) {
                updateKcp();
            } else {
                kcpTsUpdate(-1);
            }
        }
    }

    @Override
    protected final Object filterOutboundMessage(Object msg) {
        if (msg instanceof ByteBuf) {
            return msg;
        }

        throw new UnsupportedOperationException(
                "unsupported message type: " + StringUtil.simpleClassName(msg) + EXPECTED_TYPES);
    }

    @Override
    public InetSocketAddress localAddress() {
        return (InetSocketAddress) super.localAddress();
    }

    @Override
    public InetSocketAddress remoteAddress() {
        return (InetSocketAddress) super.remoteAddress();
    }

    public boolean isFlushPending() {
        return flushPending;
    }

    public int conv() {
        return ukcp.getConv();
    }

    public UkcpClientChannel conv(int conv) {
        ukcp.setConv(conv);
        return this;
    }

    void kcpReceive(ByteBuf buf) throws IOException {
        ukcp.receive(buf);
    }

    void kcpReceive(List<ByteBuf> bufList) {
        ukcp.receive(bufList);
    }

    void kcpInput(ByteBuf buf) throws IOException {
        ukcp.input(buf);
    }

    boolean kcpSend(ByteBuf buf) throws IOException {
        if (ukcp.canSend(true)) {
            ukcp.send(buf);
            return true;
        } else {
            return false;
        }
    }

    boolean kcpCanRecv() {
        return ukcp.canRecv();
    }

    boolean kcpCanSend() {
        return ukcp.canSend(!flushPending);
    }

    int kcpPeekSize() {
        return ukcp.peekSize();
    }

    long kcpUpdate(long current) {
        return ukcp.update(current);
    }

    long kcpCheck(long current) {
        return ukcp.check(current);
    }

    long kcpTsUpdate() {
        return ukcp.getTsUpdate();
    }

    void kcpTsUpdate(long tsUpdate) {
        ukcp.setTsUpdate(tsUpdate);
    }

    int kcpState() {
        return ukcp.getState();
    }

    void scheduleUpdate(long tsUpdate, long current) {
        if (sheduleUpdateLog.isDebugEnabled()) {
            sheduleUpdateLog.debug("schedule delay: " + (tsUpdate - current));
        }
        this.tsUpdate = tsUpdate;
        eventLoop().schedule(this, tsUpdate - current, TimeUnit.MILLISECONDS);
    }

    @Override
    public void run() {
        if (!isActive()) {
            return;
        }
        long current = System.currentTimeMillis();

        long nextTsUpadte = -1;
        long tsUp = kcpTsUpdate();
        Throwable exception = null;
        if (current >= tsUp) {
            try {
                nextTsUpadte = kcpUpdate(current);
            } catch (Throwable t) {
                exception = t;
            }

            if (kcpState() == -1 && exception == null) {
                if (log.isDebugEnabled()) {
                    log.debug("getState=-1 after update(). channel={}", this);
                }
                exception = new KcpException("State=-1 after update()");
            }
        } else {
            nextTsUpadte = tsUp;
        }

        boolean close = false;
        if (exception != null) {
            close = true;
            nextTsUpadte = -1;
        } else {
            if (isFlushPending() && kcpCanSend()) {
                unsafe().forceFlush();
            }
        }

        tsUpdate = nextTsUpadte;
        if (tsUpdate != -1) {
            scheduleUpdate(tsUpdate, current);
        }

        if (close) {
            Utils.fireExceptionAndClose(this, exception, true);
        }
    }

    private void updateKcp() {
        long current = System.currentTimeMillis();
        Throwable exception = null;
        try {
            kcpUpdate(current);
        } catch (Throwable t) {
            exception = t;
        }

        if (kcpState() == -1 && exception == null) {
            if (log.isDebugEnabled()) {
                log.debug("getState=-1 after update(). channel={}", this);
            }
            exception = new KcpException("State=-1 after update()");
        }

        if (exception != null) {
            Utils.fireExceptionAndClose(this, exception, true);
        }
    }

    final class UkcpClientUnsafe extends AbstractUnsafe {

        @Override
        public void connect(SocketAddress remoteAddress, SocketAddress localAddress, ChannelPromise promise) {
            if (!promise.setUncancellable() || !ensureOpen(promise)) {
                return;
            }

            try {
                boolean wasActive = isActive();
                if (doConnect(remoteAddress, localAddress)) {
                    fulfillConnectPromise(promise, wasActive);
                } else {
                    throw new Error();
                }
            } catch (Throwable t) {
                promise.tryFailure(annotateConnectException(t, remoteAddress));
                closeIfClosed();
            }
        }

        private void fulfillConnectPromise(ChannelPromise promise, boolean wasActive) {
            if (promise == null) {
                // Closed via cancellation and the promise has been notified already.
                return;
            }

            // Get the state as trySuccess() may trigger an ChannelFutureListener that will close the Channel.
            // We still need to ensure we call fireChannelActive() in this case.
            boolean active = isActive();

            // trySuccess() will return false if a user cancelled the connection attempt.
            boolean promiseSet = promise.trySuccess();

            // Regardless if the connection attempt was cancelled, channelActive() event should be triggered,
            // because what happened is what happened.
            if (!wasActive && active) {
                pipeline().fireChannelActive();
            }

            // If a user cancelled the connection attempt, close the channel, which is followed by channelInactive().
            if (!promiseSet) {
                close(voidPromise());
            }
        }

        @Override
        protected void flush0() {
            if (isFlushPending()) {
                return;
            }
            super.flush0();
        }

        void forceFlush() {
            super.flush0();
        }

    }

    private static class UkcpClientOutput implements KcpOutput {

        @Override
        public void out(ByteBuf data, Kcp kcp) {
            UkcpClientChannel ukcpChannel = (UkcpClientChannel) kcp.getUser();
            UkcpClientUdpChannel udpChannel = ukcpChannel.udpChannel;
            udpChannel.unsafe().write(data, udpChannel.voidPromise());
            udpChannel.unsafe().flush();
        }

    }

}