/**
 * 测试
 */
package org.beykery.jkcp;

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.DatagramPacket;
import io.netty.channel.socket.nio.NioDatagramChannel;

import java.net.InetSocketAddress;

/**
 * @author beykery
 */
public abstract class KcpClient implements Output, KcpListerner, Runnable {

    private final NioDatagramChannel channel;
    private final InetSocketAddress addr;
    private int nodelay;
    private int interval = Kcp.IKCP_INTERVAL;
    private int resend;
    private int nc;
    private int sndwnd = Kcp.IKCP_WND_SND;
    private int rcvwnd = Kcp.IKCP_WND_RCV;
    private int mtu = Kcp.IKCP_MTU_DEF;
    private int conv = (int) (Math.random() * Integer.MAX_VALUE);
    private boolean stream;
    private int minRto = Kcp.IKCP_RTO_MIN;
    private long timeout;
    private KcpOnUdp kcp;
    private volatile boolean running;
    private final Object waitLock = new Object();
    private InetSocketAddress remote;
    private NioEventLoopGroup nioEventLoopGroup;

    /**
     * client
     */
    public KcpClient() {
        this(0);
    }

    /**
     * 客户端
     *
     * @param port
     */
    public KcpClient(int port) {
        nioEventLoopGroup = new NioEventLoopGroup();
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.channel(NioDatagramChannel.class);
        bootstrap.group(nioEventLoopGroup);
        bootstrap.handler(new ChannelInitializer<NioDatagramChannel>() {

            @Override
            protected void initChannel(NioDatagramChannel ch) throws Exception {
                ChannelPipeline cp = ch.pipeline();
                cp.addLast(new ChannelInboundHandlerAdapter() {
                    @Override
                    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                        DatagramPacket dp = (DatagramPacket) msg;
                        KcpClient.this.onReceive(dp);
                    }

                    @Override
                    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                        KcpClient.this.handleException(cause, null);
                        KcpClient.this.close();
                    }
                });
            }
        });
        ChannelFuture sync = bootstrap.bind(port).syncUninterruptibly();
        channel = (NioDatagramChannel) sync.channel();
        addr = channel.localAddress();
        Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
            @Override
            public void run() {
                nioEventLoopGroup.shutdownGracefully();
            }
        }));
    }

    /**
     * fastest: ikcp_nodelay(kcp, 1, 20, 2, 1) nodelay: 0:disable(default),
     * 1:enable interval: internal update timer interval in millisec, default is
     * 100ms resend: 0:disable fast resend(default), 1:enable fast resend nc:
     * 0:normal congestion control(default), 1:disable congestion control
     *
     * @param nodelay
     * @param interval
     * @param resend
     * @param nc
     */
    public void noDelay(int nodelay, int interval, int resend, int nc) {
        this.nodelay = nodelay;
        this.interval = interval;
        this.resend = resend;
        this.nc = nc;
    }

    /**
     * set maximum window size: sndwnd=32, rcvwnd=32 by default
     *
     * @param sndwnd
     * @param rcvwnd
     */
    public void wndSize(int sndwnd, int rcvwnd) {
        this.sndwnd = sndwnd;
        this.rcvwnd = rcvwnd;
    }

    /**
     * change MTU size, default is 1400
     *
     * @param mtu
     */
    public void setMtu(int mtu) {
        this.mtu = mtu;
    }

    /**
     * conv
     *
     * @param conv
     */
    public void setConv(int conv) {
        this.conv = conv;
    }

    /**
     * stream mode
     *
     * @param stream
     */
    public void setStream(boolean stream) {
        this.stream = stream;
    }

    public boolean isStream() {
        return stream;
    }

    public void setMinRto(int minRto) {
        this.minRto = minRto;
    }

    public void setTimeout(long timeout) {
        this.timeout = timeout;
    }

    public long getTimeout() {
        return this.timeout;
    }

    /**
     * 固定连接到一个服务器地址,只会处理此地址的消息
     *
     * @param addr
     */
    public void connect(InetSocketAddress addr) {
        this.remote = addr;
        this.channel.connect(addr);
    }

    @Override
    public void out(ByteBuf msg, Kcp kcp, Object user) {
        DatagramPacket temp = new DatagramPacket(msg, (InetSocketAddress) user, this.addr);
        this.channel.writeAndFlush(temp);
    }

    @Override
    public void handleClose(KcpOnUdp kcp) {
        this.close();
    }

    /**
     * 收到服务器消息
     *
     * @param dp
     */
    private void onReceive(DatagramPacket dp) {
        if (this.kcp != null && this.running) {
            this.kcp.input(dp.content());
            synchronized (this.waitLock) {
                this.waitLock.notify();
            }
        } else {
            dp.release();
        }
    }

    /**
     * 关掉
     */
    public void close() {
        if (this.running) {
            this.running = false;
        }
    }

    /**
     * 发送消息
     *
     * @param bb
     */
    public void send(ByteBuf bb) {
        if (this.kcp != null) {
            this.kcp.send(bb);
            synchronized (this.waitLock) {
                this.waitLock.notify();
            }
        }
    }

    /**
     * 开启线程处理kcp状态
     */
    public void start() {
        if (!this.running) {
            this.running = true;
            this.kcp = new KcpOnUdp(this, remote, addr, this);
            this.kcp.noDelay(nodelay, interval, resend, nc);
            this.kcp.wndSize(sndwnd, rcvwnd);
            this.kcp.setTimeout(timeout);
            this.kcp.setMtu(mtu);
            this.kcp.setConv(conv);
            this.kcp.setStream(stream);
            this.kcp.setMinRto(minRto);
            Thread t = new Thread(this);
            t.setName("kcp client thread");
            t.start();
        }
    }

    @Override
    public void run() {
        long start, end;
        while (running) {
            start = System.currentTimeMillis();
            if (kcp.isClosed()) {
                this.running = false;
                continue;
            }
            kcp.update();
            end = System.currentTimeMillis();
            if (end - start < interval) {
                synchronized (waitLock) {
                    try {
                        waitLock.wait(this.interval - end + start);
                    } catch (InterruptedException ex) {
                    }
                }
            }
        }
        this.release();
        nioEventLoopGroup.shutdownGracefully();
        this.channel.close();
    }

    /**
     * 释放内存
     */
    private void release() {
        this.kcp.release();
    }

}
