package featurea.modbus.tcp;

import featurea.modbus.ModbusSlave;
import featurea.modbus.encap.EncapMessageParser;
import featurea.modbus.encap.EncapRequestHandler;
import featurea.modbus.util.BaseMessageParser;
import featurea.modbus.util.BaseRequestHandler;
import featurea.modbus.util.ModbusInitException;
import featurea.modbus.util.ModbusUtils;
import featurea.modbus.xa.XaMessageParser;
import featurea.modbus.xa.XaRequestHandler;
import featurea.sero.messaging.MessageControl;
import featurea.sero.messaging.TestableTransport;

import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class TcpSlave extends ModbusSlave {

    final boolean encapsulated;
    final ExecutorService executorService;
    final List<TcpConnectionHandler> connectionHandlers = new ArrayList<>();
    // Configuration fields
    private final int port;
    // Runtime fields.
    private ServerSocket serverSocket;

    public TcpSlave(boolean encapsulated) {
        this(ModbusUtils.TCP_PORT, encapsulated);
    }

    public TcpSlave(int port, boolean encapsulated) {
        this.port = port;
        this.encapsulated = encapsulated;
        executorService = Executors.newCachedThreadPool();
    }

    @Override
    public void start() throws ModbusInitException {
        try {
            serverSocket = new ServerSocket(port);

            Socket socket;
            while (true) {
                socket = serverSocket.accept();
                TcpConnectionHandler handler = new TcpConnectionHandler(socket);
                executorService.execute(handler);
                synchronized (connectionHandlers) {
                    connectionHandlers.add(handler);
                }
            }
        } catch (IOException e) {
            throw new ModbusInitException(e);
        }
    }

    @Override
    public void stop() {
        // Close the socket first to prevent new messages.
        try {
            serverSocket.close();
        } catch (IOException e) {
            getExceptionHandler().receivedException(e);
        }

        synchronized (connectionHandlers) {
            for (TcpConnectionHandler connectionHandler : connectionHandlers) {
                connectionHandler.kill();
            }
            connectionHandlers.clear();
        }

        // Now close the executor service.
        executorService.shutdown();
        try {
            executorService.awaitTermination(3, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            getExceptionHandler().receivedException(e);
        }
    }

    class TcpConnectionHandler implements Runnable {
        private final Socket socket;
        private TestableTransport transport;
        private MessageControl messageControl;

        TcpConnectionHandler(Socket socket) throws ModbusInitException {
            this.socket = socket;
            try {
                transport = new TestableTransport(socket.getInputStream(), socket.getOutputStream());
            } catch (IOException e) {
                throw new ModbusInitException(e);
            }
        }

        @Override
        public void run() {
            BaseMessageParser messageParser;
            BaseRequestHandler requestHandler;

            if (encapsulated) {
                messageParser = new EncapMessageParser(false);
                requestHandler = new EncapRequestHandler(TcpSlave.this);
            } else {
                messageParser = new XaMessageParser(false);
                requestHandler = new XaRequestHandler(TcpSlave.this);
            }

            messageControl = new MessageControl();
            messageControl.setExceptionHandler(getExceptionHandler());

            try {
                messageControl.start(transport, messageParser, requestHandler, null);
                executorService.execute(transport);
            } catch (IOException e) {
                getExceptionHandler().receivedException(new ModbusInitException(e));
            }

            // Monitor the socket to detect when it gets closed.
            while (true) {
                try {
                    transport.testInputStream();
                } catch (IOException e) {
                    break;
                }

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    // no op
                }
            }

            messageControl.close();
            kill();
            synchronized (connectionHandlers) {
                connectionHandlers.remove(this);
            }
        }

        void kill() {
            try {
                socket.close();
            } catch (IOException e) {
                getExceptionHandler().receivedException(new ModbusInitException(e));
            }
        }
    }
}