/**
 * 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 org.apache.camel.Consumer;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Processor;
import org.apache.camel.Producer;
import org.apache.camel.api.management.ManagedResource;
import org.apache.camel.impl.DefaultEndpoint;
import org.apache.camel.spi.Metadata;
import org.apache.camel.spi.UriEndpoint;
import org.apache.camel.spi.UriParam;
import org.apache.camel.spi.UriPath;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * The MLLP component is designed to handle the MLLP protocol and provide the functionality required by Healthcare providers to communicate with other systems using the MLLP protocol.
 *
 * <p/>
 * NOTE: MLLP payloads are not logged unless the logging level is set to DEBUG or TRACE to avoid introducing PHI
 * into the log files.  Logging of PHI can be globally disabled by setting the org.apache.camel.mllp.logPHI system
 * property to false.
 * <p/>
 */
@ManagedResource(description = "Mllp Endpoint")
@UriEndpoint(scheme = "mllp", title = "MLLP", syntax = "mllp:hostname:port", consumerClass = MllpTcpServerConsumer.class, label = "mllp")
public class MllpEndpoint extends DefaultEndpoint {
    public static final char START_OF_BLOCK = 0x0b;      // VT (vertical tab)        - decimal 11, octal 013
    public static final char END_OF_BLOCK = 0x1c;        // FS (file separator)      - decimal 28, octal 034
    public static final char END_OF_DATA = 0x0d;         // CR (carriage return)     - decimal 13, octal 015
    public static final int END_OF_STREAM = -1;          //
    public static final char SEGMENT_DELIMITER = 0x0d;   // CR (carriage return)     - decimal 13, octal 015
    public static final char MESSAGE_TERMINATOR = 0x0a;  // LF (line feed, new line) - decimal 10, octal 012

    private static final Logger LOG = LoggerFactory.getLogger(MllpEndpoint.class);

    @UriPath @Metadata(required = "true")
    String hostname;

    @UriPath @Metadata(required = "true")
    int port = -1;

    @UriParam(label = "advanced", defaultValue = "5")
    int backlog = 5;

    @UriParam(label = "timeout", defaultValue = "30000")
    int bindTimeout = 30000;

    @UriParam(label = "timeout", defaultValue = "5000")
    int bindRetryInterval = 5000;

    @UriParam(label = "timeout", defaultValue = "60000")
    int acceptTimeout = 60000;

    @UriParam(label = "timeout", defaultValue = "30000")
    int connectTimeout = 30000;

    @UriParam(label = "timeout", defaultValue = "10000")
    int receiveTimeout = 10000;

    @UriParam(label = "timeout", defaultValue = "-1")
    int maxReceiveTimeouts = -1;

    @UriParam(label = "timeout", defaultValue = "500")
    int readTimeout = 500;

    @UriParam(defaultValue = "true")
    boolean keepAlive = true;

    @UriParam(defaultValue = "true")
    boolean tcpNoDelay = true;

    @UriParam
    boolean reuseAddress;

    @UriParam(label = "advanced")
    Integer receiveBufferSize;

    @UriParam(label = "advanced")
    Integer sendBufferSize;

    @UriParam(defaultValue = "true")
    boolean autoAck = true;

    @UriParam(defaultValue = "true")
    boolean hl7Headers = true;

    @UriParam(defaultValue = "true")
    boolean bufferWrites = true;

    @UriParam(defaultValue = "false")
    boolean validatePayload;

    @UriParam(label = "codec")
    String charsetName;

    public MllpEndpoint(String uri, MllpComponent component) {
        super(uri, component);
    }

    @Override
    public ExchangePattern getExchangePattern() {
        return ExchangePattern.InOut;
    }

    @Override
    public Exchange createExchange(ExchangePattern exchangePattern) {
        Exchange mllpExchange = super.createExchange(exchangePattern);
        setExchangeProperties(mllpExchange);
        return mllpExchange;
    }

    @Override
    public Exchange createExchange(Exchange exchange) {
        Exchange mllpExchange = super.createExchange(exchange);
        setExchangeProperties(mllpExchange);
        return mllpExchange;
    }

    private void setExchangeProperties(Exchange mllpExchange) {
        if (charsetName != null) {
            mllpExchange.setProperty(Exchange.CHARSET_NAME, charsetName);
        }
    }

    public Producer createProducer() throws Exception {
        LOG.trace("({}).createProducer()", this.getEndpointKey());
        return new MllpTcpClientProducer(this);
    }

    public Consumer createConsumer(Processor processor) throws Exception {
        LOG.trace("({}).createConsumer(processor)", this.getEndpointKey());
        Consumer consumer = new MllpTcpServerConsumer(this, processor);
        configureConsumer(consumer);
        return consumer;
    }

    @Override
    public boolean isSynchronous() {
        return true;
    }

    public boolean isSingleton() {
        return true;
    }

    public String getCharsetName() {
        return charsetName;
    }

    /**
     * Set the CamelCharsetName property on the exchange
     *
     * @param charsetName the charset
     */
    public void setCharsetName(String charsetName) {
        this.charsetName = charsetName;
    }

    public String getHostname() {
        return hostname;
    }

    /**
     * Hostname or IP for connection for the TCP connection.
     *
     * The default value is null, which means any local IP address
     *
     * @param hostname Hostname or IP
     */
    public void setHostname(String hostname) {
        this.hostname = hostname;
    }

    public int getPort() {
        return port;
    }

    /**
     * Port number for the TCP connection
     *
     * @param port TCP port
     */
    public void setPort(int port) {
        this.port = port;
    }

    public int getBacklog() {
        return backlog;
    }

    /**
     * The maximum queue length for incoming connection indications (a request to connect) is set to the backlog parameter. If a connection indication arrives when the queue is full, the connection
     * is refused.
     */
    public void setBacklog(int backlog) {
        this.backlog = backlog;
    }

    public int getBindTimeout() {
        return bindTimeout;
    }

    /**
     * TCP Server Only - The number of milliseconds to retry binding to a server port
     */
    public void setBindTimeout(int bindTimeout) {
        this.bindTimeout = bindTimeout;
    }

    public int getBindRetryInterval() {
        return bindRetryInterval;
    }

    /**
     * TCP Server Only - The number of milliseconds to wait between bind attempts
     */
    public void setBindRetryInterval(int bindRetryInterval) {
        this.bindRetryInterval = bindRetryInterval;
    }

    public int getAcceptTimeout() {
        return acceptTimeout;
    }

    /**
     * Timeout (in milliseconds) while waiting for a TCP connection
     * <p/>
     * TCP Server Only
     *
     * @param acceptTimeout timeout in milliseconds
     */
    public void setAcceptTimeout(int acceptTimeout) {
        this.acceptTimeout = acceptTimeout;
    }

    public int getConnectTimeout() {
        return connectTimeout;
    }

    /**
     * Timeout (in milliseconds) for establishing for a TCP connection
     * <p/>
     * TCP Client only
     *
     * @param connectTimeout timeout in milliseconds
     */
    public void setConnectTimeout(int connectTimeout) {
        this.connectTimeout = connectTimeout;
    }

    public int getReceiveTimeout() {
        return receiveTimeout;
    }

    /**
     * The SO_TIMEOUT value (in milliseconds) used when waiting for the start of an MLLP frame
     *
     * @param receiveTimeout timeout in milliseconds
     */
    public void setReceiveTimeout(int receiveTimeout) {
        this.receiveTimeout = receiveTimeout;
    }

    public int getMaxReceiveTimeouts() {
        return maxReceiveTimeouts;
    }

    /**
     * The maximum number of timeouts (specified by receiveTimeout) allowed before the TCP Connection will be reset.
     *
     * @param maxReceiveTimeouts maximum number of receiveTimeouts
     */
    public void setMaxReceiveTimeouts(int maxReceiveTimeouts) {
        this.maxReceiveTimeouts = maxReceiveTimeouts;
    }

    public int getReadTimeout() {
        return readTimeout;
    }

    /**
     * The SO_TIMEOUT value (in milliseconds) used after the start of an MLLP frame has been received
     *
     * @param readTimeout timeout in milliseconds
     */
    public void setReadTimeout(int readTimeout) {
        this.readTimeout = readTimeout;
    }

    public boolean isKeepAlive() {
        return keepAlive;
    }

    /**
     * Enable/disable the SO_KEEPALIVE socket option.
     *
     * @param keepAlive enable SO_KEEPALIVE when true; otherwise disable SO_KEEPALIVE
     */
    public void setKeepAlive(boolean keepAlive) {
        this.keepAlive = keepAlive;
    }

    public boolean isTcpNoDelay() {
        return tcpNoDelay;
    }

    /**
     * Enable/disable the TCP_NODELAY socket option.
     *
     * @param tcpNoDelay enable TCP_NODELAY when true; otherwise disable TCP_NODELAY
     */
    public void setTcpNoDelay(boolean tcpNoDelay) {
        this.tcpNoDelay = tcpNoDelay;
    }

    public boolean isReuseAddress() {
        return reuseAddress;
    }

    /**
     * Enable/disable the SO_REUSEADDR socket option.
     *
     * @param reuseAddress enable SO_REUSEADDR when true; otherwise disable SO_REUSEADDR
     */
    public void setReuseAddress(boolean reuseAddress) {
        this.reuseAddress = reuseAddress;
    }

    public int getReceiveBufferSize() {
        return receiveBufferSize;
    }

    /**
     * Sets the SO_RCVBUF option to the specified value (in bytes)
     *
     * @param receiveBufferSize the SO_RCVBUF option value.  If null, the system default is used
     */
    public void setReceiveBufferSize(Integer receiveBufferSize) {
        this.receiveBufferSize = receiveBufferSize;
    }

    public int getSendBufferSize() {
        return sendBufferSize;
    }

    /**
     * Sets the SO_SNDBUF option to the specified value (in bytes)
     *
     * @param sendBufferSize the SO_SNDBUF option value.  If null, the system default is used
     */
    public void setSendBufferSize(Integer sendBufferSize) {
        this.sendBufferSize = sendBufferSize;
    }

    public boolean isAutoAck() {
        return autoAck;
    }

    /**
     * Enable/Disable the automatic generation of a MLLP Acknowledgement
     *
     * MLLP Consumers only
     *
     * @param autoAck enabled if true, otherwise disabled
     */
    public void setAutoAck(boolean autoAck) {
        this.autoAck = autoAck;
    }

    public boolean isHl7Headers() {
        return hl7Headers;
    }

    /**
     * Enable/Disable the automatic generation of message headers from the HL7 Message
     *
     * MLLP Consumers only
     *
     * @param hl7Headers enabled if true, otherwise disabled
     */
    public void setHl7Headers(boolean hl7Headers) {
        this.hl7Headers = hl7Headers;
    }

    public boolean isValidatePayload() {
        return validatePayload;
    }

    /**
     * Enable/Disable the validation of HL7 Payloads
     *
     * If enabled, HL7 Payloads received from external systems will be validated (see Hl7Util.generateInvalidPayloadExceptionMessage for details on the validation).
     * If and invalid payload is detected, a MllpInvalidMessageException (for consumers) or a MllpInvalidAcknowledgementException will be thrown.
     *
     * @param validatePayload enabled if true, otherwise disabled
     */
    public void setValidatePayload(boolean validatePayload) {
        this.validatePayload = validatePayload;
    }

    public boolean isBufferWrites() {
        return bufferWrites;
    }

    /**
     * Enable/Disable the validation of HL7 Payloads
     *
     * If enabled, MLLP Payloads are buffered and written to the external system in a single write(byte[]) operation.
     * If disabled, the MLLP payload will not be buffered, and three write operations will be used.  The first operation
     * will write the MLLP start-of-block character {0x0b (ASCII VT)}, the second operation will write the HL7 payload, and the
     * third operation will writh the MLLP end-of-block character and the MLLP end-of-data character {[0x1c, 0x0d] (ASCII [FS, CR])}.
     *
     * @param bufferWrites enabled if true, otherwise disabled
     */
    public void setBufferWrites(boolean bufferWrites) {
        this.bufferWrites = bufferWrites;
    }
}
