/**
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.camel.component.mllp;
import java.io.IOException;
import java.io.InputStream;
import java.net.BindException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;


import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Message;
import org.apache.camel.Processor;
import org.apache.camel.api.management.ManagedOperation;
import org.apache.camel.api.management.ManagedResource;
import org.apache.camel.component.mllp.impl.Hl7Util;
import org.apache.camel.component.mllp.impl.MllpBufferedSocketWriter;
import org.apache.camel.component.mllp.impl.MllpSocketReader;
import org.apache.camel.component.mllp.impl.MllpSocketUtil;
import org.apache.camel.component.mllp.impl.MllpSocketWriter;
import org.apache.camel.converter.IOConverter;
import org.apache.camel.impl.DefaultConsumer;
import org.apache.camel.processor.mllp.Hl7AcknowledgementGenerationException;
import org.apache.camel.processor.mllp.Hl7AcknowledgementGenerator;
import org.apache.camel.util.IOHelper;
import org.slf4j.MDC;

import static org.apache.camel.component.mllp.MllpConstants.MLLP_ACKNOWLEDGEMENT;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_ACKNOWLEDGEMENT_EXCEPTION;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_ACKNOWLEDGEMENT_STRING;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_ACKNOWLEDGEMENT_TYPE;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_AUTO_ACKNOWLEDGE;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_CHARSET;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_CLOSE_CONNECTION_AFTER_SEND;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_CLOSE_CONNECTION_BEFORE_SEND;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_EVENT_TYPE;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_LOCAL_ADDRESS;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_MESSAGE_CONTROL;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_MESSAGE_TYPE;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_PROCESSING_ID;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_RECEIVING_APPLICATION;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_RECEIVING_FACILITY;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_REMOTE_ADDRESS;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_RESET_CONNECTION_AFTER_SEND;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_RESET_CONNECTION_BEFORE_SEND;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_SECURITY;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_SENDING_APPLICATION;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_SENDING_FACILITY;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_TIMESTAMP;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_TRIGGER_EVENT;
import static org.apache.camel.component.mllp.MllpConstants.MLLP_VERSION_ID;
import static org.apache.camel.component.mllp.MllpEndpoint.SEGMENT_DELIMITER;

/**
 * The MLLP consumer.
 */
@ManagedResource(description = "MllpTcpServer Consumer")
public class MllpTcpServerConsumer extends DefaultConsumer {
    public static final int SOCKET_STARTUP_TEST_WAIT = 100;
    public static final int SOCKET_STARTUP_TEST_READ_TIMEOUT = 250;
    ServerSocketThread serverSocketThread;

    List<ClientSocketThread> clientThreads = new LinkedList<>();

    Hl7AcknowledgementGenerator acknowledgementGenerator = new Hl7AcknowledgementGenerator();

    private final MllpEndpoint endpoint;

    public MllpTcpServerConsumer(MllpEndpoint endpoint, Processor processor) {
        super(endpoint, processor);
        log.trace("MllpTcpServerConsumer(endpoint, processor)");
        this.endpoint = endpoint;
    }

    @Override
    protected void doStart() throws Exception {
        log.debug("doStart() - creating acceptor thread");

        startMllpConsumer();

        super.doStart();
    }

    @ManagedOperation(description = "Check server connection")
    public boolean managedCheckConnection() {
        boolean isValid = true;
        try {
            InetSocketAddress socketAddress;
            if (null == endpoint.getHostname()) {
                socketAddress = new InetSocketAddress(endpoint.getPort());
            } else {
                socketAddress = new InetSocketAddress(endpoint.getHostname(), endpoint.getPort());
            }
            Socket checkSocket = new Socket();
            checkSocket.connect(socketAddress, 100);
            checkSocket.close();
        } catch (Exception e) {
            isValid = false;
            log.debug("JMX check connection: {}", e);
        }
        return isValid;
    }

    @ManagedOperation(description = "Starts serverSocket thread and waits for requests")
    public void startMllpConsumer() throws IOException, InterruptedException {
        ServerSocket serverSocket = new ServerSocket();
        if (null != endpoint.receiveBufferSize) {
            serverSocket.setReceiveBufferSize(endpoint.receiveBufferSize);
        }

        serverSocket.setReuseAddress(endpoint.reuseAddress);

        // Accept Timeout
        serverSocket.setSoTimeout(endpoint.acceptTimeout);

        InetSocketAddress socketAddress;
        if (null == endpoint.getHostname()) {
            socketAddress = new InetSocketAddress(endpoint.getPort());
        } else {
            socketAddress = new InetSocketAddress(endpoint.getHostname(), endpoint.getPort());
        }
        long startTicks = System.currentTimeMillis();

        do {
            try {
                serverSocket.bind(socketAddress, endpoint.backlog);
            } catch (BindException bindException) {
                if (System.currentTimeMillis() > startTicks + endpoint.getBindTimeout()) {
                    log.error("Failed to bind to address {} within timeout {}", socketAddress, endpoint.getBindTimeout());
                    throw bindException;
                } else {
                    log.warn("Failed to bind to address {} - retrying in {} milliseconds", socketAddress, endpoint.getBindRetryInterval());
                    Thread.sleep(endpoint.getBindRetryInterval());
                }
            }
        } while (!serverSocket.isBound());

        serverSocketThread = new ServerSocketThread(serverSocket);
        serverSocketThread.start();
    }

    @Override
    protected void doStop() throws Exception {
        log.debug("doStop()");

        stopMllpConsumer();

        super.doStop();
    }

    @ManagedOperation(description = "Stops client threads and serverSocket thread")
    public void stopMllpConsumer() {
        // Close any client sockets that are currently open
        for (ClientSocketThread clientSocketThread: clientThreads) {
            clientSocketThread.interrupt();
        }


        switch (serverSocketThread.getState()) {
        case TERMINATED:
            // This is what we hope for
            break;
        case NEW:
        case RUNNABLE:
        case BLOCKED:
        case WAITING:
        case TIMED_WAITING:
        default:
            serverSocketThread.interrupt();
            break;
        }

        serverSocketThread = null;
    }

    /**
     * Nested Class to handle the ServerSocket.accept requests
     */
    class ServerSocketThread extends Thread {
        ServerSocket serverSocket;

        ServerSocketThread(ServerSocket serverSocket) {
            this.setName(createThreadName(serverSocket));

            this.serverSocket = serverSocket;
        }

        /**
         * Derive a thread name from the class name, the component URI and the connection information.
         * <p/>
         * The String will in the format <class name>[endpoint key] - [local socket address]
         *
         * @return String for thread name
         */
        String createThreadName(ServerSocket serverSocket) {
            // Get the classname without the package.  This is a nested class, so we want the parent class name included
            String fullClassName = this.getClass().getName();
            String className = fullClassName.substring(fullClassName.lastIndexOf('.') + 1);

            // Get the URI without options
            String fullEndpointKey = endpoint.getEndpointKey();
            String endpointKey;
            if (fullEndpointKey.contains("?")) {
                endpointKey = fullEndpointKey.substring(0, fullEndpointKey.indexOf('?'));
            } else {
                endpointKey = fullEndpointKey;
            }

            // Now put it all together
            return String.format("%s[%s] - %s", className, endpointKey, serverSocket.getLocalSocketAddress());
        }

        /**
         * The main ServerSocket.accept() loop
         * <p/>
         * NOTE:  When a connection is received, the Socket is checked after a brief delay in an attempt to determine
         * if this is a load-balancer probe.  The test is done before the ClientSocketThread is created to avoid creating
         * a large number of short lived threads, which is what can occur if the load balancer polling interval is very
         * short.
         */
        public void run() {
            MDC.put("camel.contextId", endpoint.getCamelContext().getName());

            try {
                while (!isInterrupted()  &&  null != serverSocket && serverSocket.isBound()  &&  !serverSocket.isClosed()) {
                    Socket socket = null;
                    try {
                        socket = serverSocket.accept();
                    } catch (SocketTimeoutException timeoutEx) {
                        // Didn't get a new connection - keep waiting for one
                        log.debug("Timeout waiting for client connection - keep listening");
                        continue;
                    } catch (SocketException socketEx) {
                        // This should happen if the component is closed while the accept call is blocking
                        if (serverSocket.isBound()) {
                            try {
                                serverSocket.close();
                            } catch (Exception ex) {
                                log.debug("Exception encountered closing ServerSocket after SocketException on accept() - ignoring", ex);
                            }
                        }
                        continue;
                    } catch (IOException ioEx) {
                        log.error("Exception encountered accepting connection - closing ServerSocket", ioEx);
                        if (serverSocket.isBound()) {
                            try {
                                serverSocket.close();
                            } catch (Exception ex) {
                                log.debug("Exception encountered closing ServerSocket after exception on accept() - ignoring", ex);
                            }
                        }
                        continue;
                    }

                    try {
                    /* Wait a bit and then check and see if the socket is really there - it could be a load balancer
                     pinging the port
                      */
                        if (socket.isConnected() && !socket.isClosed()) {
                            log.debug("Socket appears to be there - checking for available data in {} milliseconds", SOCKET_STARTUP_TEST_WAIT);
                            Thread.sleep(SOCKET_STARTUP_TEST_WAIT);

                            InputStream inputStream;
                            try {
                                inputStream = socket.getInputStream();
                            } catch (IOException ioEx) {
                                MllpSocketUtil.reset(socket, log, "Failed to retrieve the InputStream for socket after the initial connection was accepted");
                                continue;
                            }

                            if (0 < inputStream.available()) {
                                // Something is there - start the client thread
                                ClientSocketThread clientThread = new ClientSocketThread(socket, null);
                                clientThreads.add(clientThread);
                                clientThread.start();
                                continue;
                            }

                            // The easy check failed - so trigger a blocking read
                            MllpSocketUtil.setSoTimeout(socket, SOCKET_STARTUP_TEST_READ_TIMEOUT, log, "Preparing to check for available data on component startup");
                            try {
                                int tmpByte = inputStream.read();
                                if (-1 == tmpByte) {
                                    log.debug("Check for available data failed - Socket.read() returned END_OF_STREAM");
                                    MllpSocketUtil.close(socket, null, null);
                                } else {
                                    ClientSocketThread clientThread = new ClientSocketThread(socket, tmpByte);
                                    clientThreads.add(clientThread);
                                    clientThread.start();
                                }
                            } catch (SocketTimeoutException timeoutEx) {
                                // No data, but the socket is there
                                String logMessageFormat =
                                    "Check for available data failed - Socket.read() timed-out after {} milliseconds."
                                        + "  No Data - but the socket is there.  Starting ClientSocketThread";
                                log.debug(logMessageFormat, SOCKET_STARTUP_TEST_READ_TIMEOUT);
                                ClientSocketThread clientThread = new ClientSocketThread(socket, null);
                                clientThreads.add(clientThread);
                                clientThread.start();
                            } catch (IOException ioEx) {
                                log.debug("Ignoring IOException encountered when attempting to read a byte - connection was reset");
                                try {
                                    socket.close();
                                } catch (IOException closeEx) {
                                    log.debug("Ignoring IOException encountered when attempting to close the connection after the connection reset was detected", closeEx);
                                }
                            }
                        }
                    } catch (SocketTimeoutException timeoutEx) {
                        // No new clients
                        log.trace("SocketTimeoutException waiting for new connections - no new connections");

                        for (int i = clientThreads.size() - 1; i >= 0; --i) {
                            ClientSocketThread thread = clientThreads.get(i);
                            if (!thread.isAlive()) {
                                clientThreads.remove(i);
                            }
                        }
                    } catch (InterruptedException interruptEx) {
                        log.debug("accept loop interrupted - closing ServerSocket");
                        try {
                            serverSocket.close();
                        } catch (Exception ex) {
                            log.debug("Exception encountered closing ServerSocket after InterruptedException - ignoring", ex);
                        }
                    } catch (Exception ex) {
                        log.error("Exception accepting new connection - retrying", ex);
                    }
                }
            } finally {
                log.debug("ServerSocket.accept loop finished - closing listener");
                if (null != serverSocket && serverSocket.isBound() && !serverSocket.isClosed()) {
                    try {
                        serverSocket.close();
                    } catch (Exception ex) {
                        log.debug("Exception encountered closing ServerSocket after accept loop had exited - ignoring", ex);
                    }
                }
            }
        }

        @Override
        public void interrupt() {
            super.interrupt();
            if (null != serverSocket) {
                if (serverSocket.isBound()) {
                    try {
                        serverSocket.close();
                    } catch (IOException ioEx) {
                        log.warn("Exception encountered closing ServerSocket in interrupt() method - ignoring", ioEx);
                    }
                }
            }
        }
    }

    /**
     * Nested Class reads the Socket
     */
    class ClientSocketThread extends Thread {
        final Socket clientSocket;
        final MllpSocketReader mllpSocketReader;
        final MllpSocketWriter mllpSocketWriter;

        Integer initialByte;

        ClientSocketThread(Socket clientSocket, Integer initialByte) throws IOException {
            this.initialByte = initialByte;
            this.setName(createThreadName(clientSocket));
            this.clientSocket = clientSocket;
            this.clientSocket.setKeepAlive(endpoint.keepAlive);
            this.clientSocket.setTcpNoDelay(endpoint.tcpNoDelay);
            if (null != endpoint.receiveBufferSize) {
                this.clientSocket.setReceiveBufferSize(endpoint.receiveBufferSize);
            }
            if (null != endpoint.sendBufferSize) {
                this.clientSocket.setSendBufferSize(endpoint.sendBufferSize);
            }
            this.clientSocket.setReuseAddress(endpoint.reuseAddress);
            this.clientSocket.setSoLinger(false, -1);

            // Initial Read Timeout
            MllpSocketUtil.setSoTimeout(clientSocket, endpoint.receiveTimeout, log, "Constructing ClientSocketThread");

            mllpSocketReader = new MllpSocketReader(this.clientSocket, endpoint.receiveTimeout, endpoint.readTimeout, false);
            if (endpoint.bufferWrites) {
                mllpSocketWriter = new MllpBufferedSocketWriter(this.clientSocket, true);
            } else {
                mllpSocketWriter = new MllpSocketWriter(this.clientSocket, true);
            }
        }

        /**
         * derive a thread name from the class name, the component URI and the connection information
         * <p/>
         * The String will in the format <class name>[endpoint key] - [local socket address] -> [remote socket address]
         *
         * @return the thread name
         */
        String createThreadName(Socket socket) {
            // Get the classname without the package.  This is a nested class, so we want the parent class name included
            String fullClassName = this.getClass().getName();
            String className = fullClassName.substring(fullClassName.lastIndexOf('.') + 1);

            // Get the URI without options
            String fullEndpointKey = endpoint.getEndpointKey();
            String endpointKey;
            if (fullEndpointKey.contains("?")) {
                endpointKey = fullEndpointKey.substring(0, fullEndpointKey.indexOf('?'));
            } else {
                endpointKey = fullEndpointKey;
            }

            // Now put it all together
            return String.format("%s[%s] - %s -> %s", className, endpointKey, socket.getLocalSocketAddress(), socket.getRemoteSocketAddress());
        }

        @Override
        public void run() {
            int receiveTimeoutCounter = 0;
            MDC.put("camel.contextId", endpoint.getCamelContext().getName());

            while (!isInterrupted()  &&  null != clientSocket  &&  clientSocket.isConnected()  &&  !clientSocket.isClosed()) {
                byte[] hl7MessageBytes = null;

                log.debug("Checking for data ....");
                try {
                    hl7MessageBytes = mllpSocketReader.readEnvelopedPayload(initialByte);
                    if (hl7MessageBytes == null) {
                        // No data received - check for max timeouts
                        if (endpoint.maxReceiveTimeouts > 0 && ++receiveTimeoutCounter >= endpoint.maxReceiveTimeouts) {
                            String reasonMessage = String.format("Idle Client after %d receive timeouts [%d-milliseconds] - resetting connection", receiveTimeoutCounter, endpoint.receiveTimeout);
                            MllpSocketUtil.reset(clientSocket, log, reasonMessage);
                        }
                        continue;
                    }
                } catch (MllpException mllpEx) {
                    Exchange exchange = endpoint.createExchange(ExchangePattern.InOut);
                    exchange.setException(mllpEx);
                    log.warn("Exception encountered reading payload - sending exception to route", mllpEx);
                    try {
                        getProcessor().process(exchange);
                    } catch (Exception e) {
                        log.error("Exception encountered processing exchange with exception encounter reading payload", e);
                    }
                    continue;
                } finally {
                    initialByte = null;
                }

                // Send the message on for processing and wait for the response
                log.debug("Populating the exchange with received message");
                Exchange exchange = endpoint.createExchange(ExchangePattern.InOut);
                try {
                    createUoW(exchange);
                    Message message = exchange.getIn();
                    message.setBody(hl7MessageBytes, byte[].class);

                    message.setHeader(MLLP_LOCAL_ADDRESS, clientSocket.getLocalAddress().toString());
                    message.setHeader(MLLP_REMOTE_ADDRESS, clientSocket.getRemoteSocketAddress());
                    message.setHeader(MLLP_AUTO_ACKNOWLEDGE, endpoint.autoAck);

                    if (endpoint.validatePayload) {
                        String exceptionMessage = Hl7Util.generateInvalidPayloadExceptionMessage(hl7MessageBytes);
                        if (exceptionMessage != null) {
                            exchange.setException(new MllpInvalidMessageException(exceptionMessage, hl7MessageBytes));
                        }
                    }
                    populateHl7DataHeaders(exchange, message, hl7MessageBytes);

                    log.debug("Calling processor");
                    try {
                        getProcessor().process(exchange);
                        sendAcknowledgement(hl7MessageBytes, exchange);
                    } catch (RuntimeException runtimeEx) {
                        throw runtimeEx;
                    } catch (Exception ex) {
                        log.error("Unexpected exception processing exchange", ex);
                    }
                } catch (Exception uowEx) {
                    // TODO:  Handle this correctly
                    exchange.setException(uowEx);
                    log.warn("Exception encountered creating Unit of Work - sending exception to route", uowEx);
                    try {
                        getProcessor().process(exchange);
                    } catch (Exception e) {
                        log.error("Exception encountered processing exchange with exception encountered createing Unit of Work", e);
                    }
                    continue;
                } finally {
                    if (exchange != null) {
                        doneUoW(exchange);
                    }
                }


            }

            log.debug("ClientSocketThread exiting");
        }

        private void sendAcknowledgement(byte[] originalHl7MessageBytes, Exchange exchange) {
            log.info("sendAcknowledgement");

            // Check BEFORE_SEND Properties
            if (exchange.getProperty(MLLP_RESET_CONNECTION_BEFORE_SEND, boolean.class)) {
                String reasonMessage = String.format("Exchange property %s is %b", MLLP_RESET_CONNECTION_BEFORE_SEND,  exchange.getProperty(MLLP_RESET_CONNECTION_BEFORE_SEND, boolean.class));
                MllpSocketUtil.reset(clientSocket, log, reasonMessage);
                return;
            } else if (exchange.getProperty(MLLP_CLOSE_CONNECTION_BEFORE_SEND, boolean.class)) {
                String reasonMessage = String.format("Exchange property %s is %b", MLLP_CLOSE_CONNECTION_BEFORE_SEND,  exchange.getProperty(MLLP_CLOSE_CONNECTION_BEFORE_SEND, boolean.class));
                MllpSocketUtil.close(clientSocket, log, reasonMessage);
                return;
            }

            // Find the acknowledgement body
            // TODO:  Enhance this to say whether or not the acknowledgment is missing or just of an un-convertible type
            byte[] acknowledgementMessageBytes = exchange.getProperty(MLLP_ACKNOWLEDGEMENT, byte[].class);
            String acknowledgementMessageType = null;
            if (null == acknowledgementMessageBytes) {
                boolean autoAck = exchange.getProperty(MLLP_AUTO_ACKNOWLEDGE, true, boolean.class);
                if (!autoAck) {
                    exchange.setException(new MllpInvalidAcknowledgementException("Automatic Acknowledgement is disabled and the "
                            + MLLP_ACKNOWLEDGEMENT + " exchange property is null or cannot be converted to byte[]", originalHl7MessageBytes, acknowledgementMessageBytes));
                    return;
                }

                String acknowledgmentTypeProperty = exchange.getProperty(MLLP_ACKNOWLEDGEMENT_TYPE, String.class);
                try {
                    if (null == acknowledgmentTypeProperty) {
                        if (null == exchange.getException()) {
                            acknowledgementMessageType = "AA";
                            acknowledgementMessageBytes = acknowledgementGenerator.generateApplicationAcceptAcknowledgementMessage(originalHl7MessageBytes);
                        } else {
                            acknowledgementMessageType = "AE";
                            acknowledgementMessageBytes = acknowledgementGenerator.generateApplicationErrorAcknowledgementMessage(originalHl7MessageBytes);
                        }
                    } else {
                        switch (acknowledgmentTypeProperty) {
                        case "AA":
                            acknowledgementMessageType = "AA";
                            acknowledgementMessageBytes = acknowledgementGenerator.generateApplicationAcceptAcknowledgementMessage(originalHl7MessageBytes);
                            break;
                        case "AE":
                            acknowledgementMessageType = "AE";
                            acknowledgementMessageBytes = acknowledgementGenerator.generateApplicationErrorAcknowledgementMessage(originalHl7MessageBytes);
                            break;
                        case "AR":
                            acknowledgementMessageType = "AR";
                            acknowledgementMessageBytes = acknowledgementGenerator.generateApplicationRejectAcknowledgementMessage(originalHl7MessageBytes);
                            break;
                        default:
                            exchange.setException(new Hl7AcknowledgementGenerationException("Unsupported acknowledgment type: " + acknowledgmentTypeProperty));
                            return;
                        }
                    }
                } catch (Hl7AcknowledgementGenerationException ackGenerationException) {
                    exchange.setProperty(MLLP_ACKNOWLEDGEMENT_EXCEPTION, ackGenerationException);
                    exchange.setException(ackGenerationException);
                }
            } else {
                final byte bM = 77;
                final byte bS = 83;
                final byte bA = 65;
                final byte bE = 69;
                final byte bR = 82;

                final byte fieldSeparator = originalHl7MessageBytes[3];
                // Acknowledgment is specified in exchange property - determine the acknowledgement type
                for (int i = 0; i < originalHl7MessageBytes.length; ++i) {
                    if (SEGMENT_DELIMITER == i) {
                        if (i + 7 < originalHl7MessageBytes.length // Make sure we don't run off the end of the message
                                && bM == originalHl7MessageBytes[i + 1] && bS == originalHl7MessageBytes[i + 2]
                                && bA == originalHl7MessageBytes[i + 3] && fieldSeparator == originalHl7MessageBytes[i + 4]) {
                            if (fieldSeparator != originalHl7MessageBytes[i + 7]) {
                                log.warn("MSA-1 is longer than 2-bytes - ignoring trailing bytes");
                            }
                            // Found MSA - pull acknowledgement bytes
                            byte[] acknowledgmentTypeBytes = new byte[2];
                            acknowledgmentTypeBytes[0] = originalHl7MessageBytes[i + 5];
                            acknowledgmentTypeBytes[1] = originalHl7MessageBytes[i + 6];
                            try {
                                acknowledgementMessageType = IOConverter.toString(acknowledgmentTypeBytes, exchange);
                            } catch (IOException ioEx) {
                                throw new RuntimeException("Failed to convert acknowledgement message to string", ioEx);
                            }

                            // Verify it's a valid acknowledgement code
                            if (bA != acknowledgmentTypeBytes[0]) {
                                switch (acknowledgementMessageBytes[1]) {
                                case bA:
                                case bR:
                                case bE:
                                    break;
                                default:
                                    log.warn("Invalid acknowledgement type [" + acknowledgementMessageType + "] found in message - should be AA, AE or AR");
                                }
                            }

                            // if the MLLP_ACKNOWLEDGEMENT_TYPE property is set on the exchange, make sure it matches
                            String acknowledgementTypeProperty = exchange.getProperty(MLLP_ACKNOWLEDGEMENT_TYPE, String.class);
                            if (null != acknowledgementTypeProperty && !acknowledgementTypeProperty.equals(acknowledgementMessageType)) {
                                log.warn("Acknowledgement type found in message [" + acknowledgementMessageType + "] does not match "
                                        + MLLP_ACKNOWLEDGEMENT_TYPE + " exchange property value [" + acknowledgementTypeProperty + "] - using value found in message");
                            }
                        }
                    }
                }
            }

            Message message;
            if (exchange.hasOut()) {
                message = exchange.getOut();
            } else {
                message = exchange.getIn();
            }
            message.setHeader(MLLP_ACKNOWLEDGEMENT, acknowledgementMessageBytes);
            // TODO:  Use the charset of the exchange
            message.setHeader(MLLP_ACKNOWLEDGEMENT_STRING, new String(acknowledgementMessageBytes));
            message.setHeader(MLLP_ACKNOWLEDGEMENT_TYPE, acknowledgementMessageType);

            // Send the acknowledgement
            log.debug("Sending Acknowledgement: {}", MllpComponent.covertBytesToPrintFriendlyString(acknowledgementMessageBytes));
            try {
                mllpSocketWriter.writeEnvelopedPayload(originalHl7MessageBytes, acknowledgementMessageBytes);
            } catch (MllpException mllpEx) {
                log.error("MLLP Acknowledgement failure: {}", mllpEx);
                MllpAcknowledgementDeliveryException deliveryException = new MllpAcknowledgementDeliveryException(originalHl7MessageBytes, acknowledgementMessageBytes, mllpEx);
                exchange.setProperty(MLLP_ACKNOWLEDGEMENT_EXCEPTION, deliveryException);
                exchange.setException(deliveryException);
            }

            // Check AFTER_SEND Properties
            if (exchange.getProperty(MLLP_RESET_CONNECTION_AFTER_SEND, boolean.class)) {
                String reasonMessage = String.format("Exchange property %s is %b", MLLP_RESET_CONNECTION_AFTER_SEND,  exchange.getProperty(MLLP_RESET_CONNECTION_AFTER_SEND, boolean.class));
                MllpSocketUtil.reset(clientSocket, log, reasonMessage);
                return;
            } else if (exchange.getProperty(MLLP_CLOSE_CONNECTION_AFTER_SEND, boolean.class)) {
                String reasonMessage = String.format("Exchange property %s is %b", MLLP_CLOSE_CONNECTION_AFTER_SEND,  exchange.getProperty(MLLP_CLOSE_CONNECTION_AFTER_SEND, boolean.class));
                MllpSocketUtil.reset(clientSocket, log, reasonMessage);
            }
        }

        private void populateHl7DataHeaders(Exchange exchange, Message message, byte[] hl7MessageBytes) {
            if (hl7MessageBytes == null ||  hl7MessageBytes.length < 8) {
                // Not enough data to populate anything - just return
                return;
            }
            // Find the end of the MSH and indexes of the fields in the MSH to populate message headers
            final byte fieldSeparator = hl7MessageBytes[3];
            int endOfMSH = -1;
            List<Integer> fieldSeparatorIndexes = new ArrayList<>(10);  // We should have at least 10 fields

            for (int i = 0; i < hl7MessageBytes.length; ++i) {
                if (fieldSeparator == hl7MessageBytes[i]) {
                    fieldSeparatorIndexes.add(i);
                } else if (SEGMENT_DELIMITER == hl7MessageBytes[i]) {
                    // If the MSH Segment doesn't have a trailing field separator, add one so the field can be extracted into a header
                    if (fieldSeparator != hl7MessageBytes[i - 1]) {
                        fieldSeparatorIndexes.add(i);
                    }
                    endOfMSH = i;
                    break;
                }
            }

            String messageBodyForDebugging = new String(hl7MessageBytes);
            if (-1 == endOfMSH) {
                // TODO:  May want to throw some sort of an Exception here
                log.error("Population of message headers failed - unable to find the end of the MSH segment");
            } else if (endpoint.hl7Headers) {
                log.debug("Populating the HL7 message headers");
                Charset charset = Charset.forName(IOHelper.getCharsetName(exchange));

                for (int i = 2; i < fieldSeparatorIndexes.size(); ++i) {
                    int startingFieldSeparatorIndex = fieldSeparatorIndexes.get(i - 1);
                    int endingFieldSeparatorIndex = fieldSeparatorIndexes.get(i);

                    // Only populate the header if there's data in the HL7 field
                    if (endingFieldSeparatorIndex - startingFieldSeparatorIndex > 1) {
                        String headerName = null;
                        switch (i) {
                        case 2: // MSH-3
                            headerName = MLLP_SENDING_APPLICATION;
                            break;
                        case 3: // MSH-4
                            headerName = MLLP_SENDING_FACILITY;
                            break;
                        case 4: // MSH-5
                            headerName = MLLP_RECEIVING_APPLICATION;
                            break;
                        case 5: // MSH-6
                            headerName = MLLP_RECEIVING_FACILITY;
                            break;
                        case 6: // MSH-7
                            headerName = MLLP_TIMESTAMP;
                            break;
                        case 7: // MSH-8
                            headerName = MLLP_SECURITY;
                            break;
                        case 8: // MSH-9
                            headerName = MLLP_MESSAGE_TYPE;
                            break;
                        case 9: // MSH-10
                            headerName = MLLP_MESSAGE_CONTROL;
                            break;
                        case 10: // MSH-11
                            headerName = MLLP_PROCESSING_ID;
                            break;
                        case 11: // MSH-12
                            headerName = MLLP_VERSION_ID;
                            break;
                        case 17: // MSH-18
                            headerName = MLLP_CHARSET;
                            break;
                        default:
                            // Not processing this field
                            continue;
                        }

                        String headerValue = new String(hl7MessageBytes, startingFieldSeparatorIndex + 1,
                                endingFieldSeparatorIndex - startingFieldSeparatorIndex - 1,
                                charset);
                        message.setHeader(headerName, headerValue);

                        // For MSH-9, set a couple more headers
                        if (i == 8) {
                            // final byte componentSeparator = hl7MessageBytes[4];
                            String componentSeparator = new String(hl7MessageBytes, 4, 1, charset);
                            String[] components = headerValue.split(String.format("\\Q%s\\E", componentSeparator), 3);
                            message.setHeader(MLLP_EVENT_TYPE, components[0]);
                            if (2 <= components.length) {
                                message.setHeader(MLLP_TRIGGER_EVENT, components[1]);
                            }
                        }
                    }
                }
            } else {
                log.trace("HL7 Message headers disabled");
            }

        }

        @Override
        public void interrupt() {
            if (null != clientSocket  &&  clientSocket.isConnected()  && !clientSocket.isClosed()) {
                MllpSocketUtil.close(clientSocket, log, this.getClass().getSimpleName() + " interrupted");
            }
            super.interrupt();
        }
    }
}

