package com.pwt.netty.client;

import cn.hutool.core.thread.NamedThreadFactory;
import com.pwt.StationService;
import com.pwt.netty.MessageSender;
import com.pwt.netty.client.dispatcher.KeepaliveHandler;
import com.pwt.netty.common.constants.ExchangeWorkState;
import com.pwt.netty.message2.Assist;
import io.netty.bootstrap.Bootstrap;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.concurrent.DefaultThreadFactory;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

import static com.pwt.netty.MessageSender.MSG_ASSIST;

/**
 * 扫码器客户端，用于连接并接收扫码器消息
 *
 * @author zk
 */

@Slf4j
public class CodeScannerClient {

    /**
     * 扫码器消息头
     */
    private static final String START_PREFIX = "start";

    private final String host;

    private final int port;

    private final AtomicBoolean shouldStart = new AtomicBoolean(false);

    private final AtomicBoolean shouldRetry = new AtomicBoolean(false);

    private final AtomicInteger currentState = new AtomicInteger(ClientConstants.CLIENT_STATUS_OFF);

    private final AtomicInteger failConnectedTime = new AtomicInteger(0);

    private NioEventLoopGroup eventLoopGroup;
    private Channel channel;

    private Bootstrap bootstrap;

    private StationService stationService;

    @Autowired
    public void setStationService(StationService stationService) {
        this.stationService = stationService;
    }

    public CodeScannerClient(String hostname, int port) {
        this.host = hostname;
        this.port = port;
    }

    public void start() throws Exception {
        if (shouldStart.compareAndSet(false, true)) {
            shouldRetry.set(true);
            startInternal();
        }
    }

    private void startInternal() {
        initBootStrap();
        connect();
    }

    private void initBootStrap() {
        bootstrap = new Bootstrap();
        eventLoopGroup = new NioEventLoopGroup(1, new DefaultThreadFactory("CodeScannerClientWorker", true));

        LoggingHandler loggingHandler = new LoggingHandler(LogLevel.DEBUG);

        bootstrap.group(eventLoopGroup)
            .channel(NioSocketChannel.class)
            .handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel channel) {
                    channel.pipeline()
                        .addLast("condeScanner-logging", loggingHandler)
                        .addLast("codeScanner-idle", new IdleStateHandler(10, 0, 0, TimeUnit.SECONDS))
                        .addLast("codeScanner-keepalive", new KeepaliveHandler())
                        .addLast(new StringDecoder())
                        .addLast("condeScanner-clientHandler", new SimpleChannelInboundHandler<String>() {

                            @Override
                            public void channelInactive(ChannelHandlerContext ctx) throws Exception {
                                if (log.isInfoEnabled()) {
                                    log.info("[CodeScannerClientHandler] Client handler inactive, remote address: {}",
                                        ctx.channel().remoteAddress());
                                }
                            }

                            @Override
                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                                if (log.isWarnEnabled()) {
                                    log.warn("[CodeScannerClientHandler] Client exception caught", cause);
                                }
                                ctx.close();
                            }

                            @Override
                            public void channelUnregistered(ChannelHandlerContext ctx) throws Exception {
                                if (log.isInfoEnabled()) {
                                    log.info("[CodeScannerClientHandler] Client channel unregistered, remote address: {}", ctx.channel().remoteAddress());
                                }
                                currentState.set(ClientConstants.CLIENT_STATUS_OFF);
                                disconnectCallback.run();
                            }


                            @Override
                            public void channelActive(ChannelHandlerContext ctx) throws Exception {
                                currentState.set(ClientConstants.CLIENT_STATUS_STARTED);
                            }

                            @Override
                            protected void channelRead0(ChannelHandlerContext channelHandlerContext, String msg) throws Exception {
                                if(stationService.getExchangeState() != ExchangeWorkState.IDLE.getValue()){
                                    return;
                                }
                                if (msg.startsWith(START_PREFIX)) {
                                    Assist assist = new Assist();
                                    assist.setText("扫码成功！");
                                    assist.setMedia("http://localhost:8080/static/" + 17 + ".mp3");
                                    MessageSender.send(MSG_ASSIST, assist);
                                    Thread.sleep(2000);
                                    // TODO 取消报警【扫码枪非法的扫描信息】
                                    stationService.startExchange();
                                } else {
                                    // TODO 报警【扫码枪非法的扫描信息】
                                }
                            }
                        });
                }
            });
    }

    private void connect() {
        ChannelFuture future = bootstrap.connect(host, port);
        future.addListener(new ChannelFutureListener() {
            @Override
            public void operationComplete(ChannelFuture channelFuture) throws Exception {
                if (channelFuture.cause() != null) {
                    if (log.isWarnEnabled()) {
                        log.info("[CodeScannerClient] Could not connect to server <{}:{}> after {} times", host, port, failConnectedTime.get(),
                            channelFuture.cause());
                    }
                    channel = null;
                    failConnectedTime.incrementAndGet();
                } else {
                    channel = channelFuture.channel();
                    failConnectedTime.set(0);
                    if (log.isInfoEnabled()) {
                        log.info("[CodeScannerClient] Successfully connect to server <{}:{}>", host, port);
                    }
                }
            }
        });
    }

    public void stop() throws Exception {
        if (shouldStart.compareAndSet(true, false)) {
            stopClientIfStarted();
        }
    }

    private void stopClientIfStarted() {
        // Stop retrying for connection.
        shouldRetry.set(false);

        while (currentState.get() == ClientConstants.CLIENT_STATUS_STARTING) {
            try {
                Thread.sleep(200);
            } catch (Exception ex) {
                // Ignore.
            }
        }
        cleanUp();

        if (log.isInfoEnabled()) {
            log.info("[CodeScannerClient] client stopped");
        }
    }

    private void cleanUp() {
        if (channel != null) {
            channel.close();
            channel = null;
        }
        if (eventLoopGroup != null) {
            eventLoopGroup.shutdownGracefully();
        }
    }

    @SuppressWarnings("PMD.ThreadPoolCreationRule")
    private static final ScheduledExecutorService SCHEDULER = Executors.newScheduledThreadPool(1,
        new NamedThreadFactory("code-scanner-client-scheduler", true));

    private static final long RECONNECT_DELAY_MS = 1000;


    private static final long MAX_RECONNECT_DELAY_MS = 1000;

    private final Runnable disconnectCallback = new Runnable() {
        @Override
        public void run() {
            if (!shouldRetry.get()) {
                return;
            }
            long time = Math.min(RECONNECT_DELAY_MS * failConnectedTime.get(), MAX_RECONNECT_DELAY_MS);
            SCHEDULER.schedule(() -> {
                if (shouldRetry.get()) {
                    if (log.isInfoEnabled()) {
                        log.info("[CodeScannerClient] Reconnection to server <{}:{}>", host, port);
                    }
                    try {
                        connect();
                    } catch (Exception e) {
                        if (log.isWarnEnabled()) {
                            log.warn("[CodeScannerClient] Failed to reconnect to server", e);
                        }
                    }
                }
            }, time, TimeUnit.MILLISECONDS);
//            cleanUp();
        }
    };
}
