package org.bdware.sc.conn;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.*;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bdware.sc.codec.LengthFieldBasedFrameCodec;
import org.bdware.sc.get.GetMessage;

import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ServiceServer extends Thread {
    public static final ExecutorService executor =
            new ThreadPoolExecutor(
                    8,
                    15,
                    60,
                    TimeUnit.SECONDS,
                    new SynchronousQueue<>());
    private static final String TAG = "ServiceServer";
    private static final Logger LOGGER = LogManager.getLogger(ServiceServer.class);
    private final MsgHandler handler;
    public AtomicInteger mainPort;
    boolean ready;

    public ServiceServer(MsgHandler handler, int startPort) {
        mainPort = new AtomicInteger(startPort);
        ready = false;
        this.handler = handler;
        this.start();
    }

    public int getPort() {
        while (!ready) {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return mainPort.get();
    }

    public void run() {
        EventLoopGroup bossGroup = new NioEventLoopGroup(1);
        EventLoopGroup workerGroup = new NioEventLoopGroup();

        ServerBootstrap b = new ServerBootstrap();
        b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT);
        b.group(bossGroup, workerGroup)
                .channel(NioServerSocketChannel.class)
                .option(ChannelOption.SO_BACKLOG, 100)
                .option(ChannelOption.SO_REUSEADDR, false)
                .childHandler(
                        new ChannelInitializer<SocketChannel>() {
                            @Override
                            protected void initChannel(SocketChannel arg0) {
                                arg0.pipeline()
                                        .addLast(new LengthFieldBasedFrameCodec())
                                        .addLast(new AsyncInboundHandler(ServiceServer.this));
                            }
                        });
        Channel channel;
        while (true) {
            try {
                int port = mainPort.get();
                ChannelFuture temp1 = b.bind(port);
                temp1.sync();
                channel = temp1.channel();
                ready = true;
                break;
            } catch (Exception e) {
                //e.printStackTrace();
                LOGGER.debug("port already in used:" + mainPort);
                mainPort.getAndIncrement();
            }
        }
        //TODO MUST use System.out to ensure output
        System.out.println("ServiceServer mainPort " + mainPort);
        try {
            channel.closeFuture().await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void handle(GetMessage message, ResultCallback rc) {
        try {
            handler.handle(message, rc);
        } catch (Throwable e) {
            ByteArrayOutputStream bo = new ByteArrayOutputStream();
            e.printStackTrace(new PrintStream(bo));
            rc.onResult("{ \"msg\":\"" + bo + "\"}");
        }
    }

    static class AsyncInboundHandler extends SimpleChannelInboundHandler<ByteBuf> {
        ServiceServer serviceServer;

        public AsyncInboundHandler(ServiceServer serviceServer) {
            this.serviceServer = serviceServer;
        }

        @Override
        protected void channelRead0(ChannelHandlerContext ctx, ByteBuf msg) throws Exception {
            ObjectInputStream obj = new ObjectInputStream(new ByteBufInputStream(msg));
            final long id = obj.readLong();
            GetMessage getMsg = (GetMessage) obj.readObject();
            serviceServer.handle(
                    getMsg,
                    new ResultCallback() {
                        @Override
                        public void onResult(String response) {
                            try {
                                writeToChannel(ctx, id, response);
                            } catch (Exception e) {
                                e.printStackTrace();
                            }
                        }
                    });
        }

        private void writeToChannel(ChannelHandlerContext ctx, long id, String response)
                throws Exception {
            ByteBuf buf = Unpooled.buffer();
            ObjectOutputStream out = new ObjectOutputStream(new ByteBufOutputStream(buf));
            out.writeLong(id);
            out.writeObject(response);
            synchronized (ctx) {
                ctx.writeAndFlush(buf);
            }
        }
    }
}
