/*
 * 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.hl7;

import java.nio.charset.CharacterCodingException;
import java.nio.charset.CharsetDecoder;

import org.apache.mina.core.buffer.IoBuffer;
import org.apache.mina.core.session.IoSession;
import org.apache.mina.filter.codec.CumulativeProtocolDecoder;
import org.apache.mina.filter.codec.ProtocolDecoderOutput;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * HL7MLLPDecoder that is aware that a HL7 message can span several buffers. In addition, it avoids rescanning packets
 * by keeping state in the IOSession.
 */
class HL7MLLPDecoder extends CumulativeProtocolDecoder {

    private static final Logger LOG = LoggerFactory.getLogger(HL7MLLPDecoder.class);
    private static final String DECODER_STATE = HL7MLLPDecoder.class.getName() + ".STATE";
    private static final String CHARSET_DECODER = HL7MLLPDecoder.class.getName() + ".charsetdecoder";

    private HL7MLLPConfig config;

    HL7MLLPDecoder(HL7MLLPConfig config) {
        this.config = config;
    }

    @Override
    protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception {

        // Get the state of the current message and
        // Skip what we have already scanned before
        DecoderState state = decoderState(session);
        in.position(state.current());

        LOG.debug("Received data, checking from position {} to {}", in.position(), in.limit());
        boolean messageDecoded = false;

        while (in.hasRemaining()) {

            int previousPosition = in.position();
            byte current = in.get();

            // Check if we are at the end of an HL7 message
            if (current == config.getEndByte2() && state.previous() == config.getEndByte1()) {
                if (state.isStarted()) {
                    // Save the current buffer pointers and reset them to surround the identifier message
                    int currentPosition = in.position();
                    int currentLimit = in.limit();
                    LOG.debug("Message ends at position {} with length {}", previousPosition,
                            previousPosition - state.start() + 1);
                    in.position(state.start());
                    in.limit(currentPosition);
                    LOG.debug("Set start to position {} and limit to {}", in.position(), in.limit());

                    // Now create string or byte[] from this part of the buffer and restore the buffer pointers
                    try {
                        out.write(config.isProduceString()
                                ? parseMessageToString(in.slice(), charsetDecoder(session))
                                : parseMessageToByteArray(in.slice()));
                        messageDecoded = true;
                    } finally {
                        LOG.debug("Resetting to position {} and limit to {}", currentPosition, currentLimit);
                        in.position(currentPosition);
                        in.limit(currentLimit);
                        state.reset();
                    }
                } else {
                    LOG.warn("Ignoring message end at position {} until start byte has been seen.", previousPosition);
                }
            } else {
                // Check if we are at the start of an HL7 message
                if (current == config.getStartByte()) {
                    state.markStart(previousPosition);
                } else {
                    // Remember previous byte in state object because the buffer could
                    // be theoretically exhausted right between the two end bytes
                    state.markPrevious(current);
                }
                messageDecoded = false;
            }
        }

        if (!messageDecoded) {
            // Could not find a complete message in the buffer.
            // Reset to the initial position (just as nothing had been read yet)
            // and return false so that this method is called again with more data.
            LOG.debug("No complete message yet at position {}", in.position());
            state.markCurrent(in.position());
            in.position(0);
        }
        return messageDecoded;
    }

    // Make a defensive byte copy (the buffer will be reused)
    // and omit the start and the two end bytes of the MLLP message
    // returning a byte array
    private Object parseMessageToByteArray(IoBuffer buf) throws CharacterCodingException {
        int len = buf.limit() - 3;
        LOG.debug("Making byte array of length {}", len);
        byte[] dst = new byte[len];
        buf.skip(1); // skip start byte
        buf.get(dst, 0, len);
        buf.skip(2); // skip end bytes

        // Only do this if conversion is enabled
        if (config.isConvertLFtoCR()) {
            LOG.debug("Replacing LF by CR");
            for (int i = 0; i < dst.length; i++) {
                if (dst[i] == (byte) '\n') {
                    dst[i] = (byte) '\r';
                }
            }
        }
        return dst;
    }

    // Make a defensive byte copy (the buffer will be reused)
    // and omit the start and the two end bytes of the MLLP message
    // returning a String
    private Object parseMessageToString(IoBuffer buf, CharsetDecoder decoder) throws CharacterCodingException {
        int len = buf.limit() - 3;
        LOG.debug("Making string of length {} using charset {}", len, decoder.charset());
        buf.skip(1); // skip start byte
        String message = buf.getString(len, decoder);
        buf.skip(2); // skip end bytes

        // Only do this if conversion is enabled
        if (config.isConvertLFtoCR()) {
            LOG.debug("Replacing LF by CR");
            message = message.replace('\n', '\r');
        }
        return message;
    }

    @Override
    public void dispose(IoSession session) throws Exception {
        session.removeAttribute(DECODER_STATE);
        session.removeAttribute(CHARSET_DECODER);
    }

    private CharsetDecoder charsetDecoder(IoSession session) {
        synchronized (session) {
            CharsetDecoder decoder = (CharsetDecoder) session.getAttribute(CHARSET_DECODER);
            if (decoder == null) {
                decoder = config.getCharset().newDecoder()
                        .onMalformedInput(config.getMalformedInputErrorAction())
                        .onUnmappableCharacter(config.getUnmappableCharacterErrorAction());
                session.setAttribute(CHARSET_DECODER, decoder);
            }
            return decoder;
        }
    }

    private DecoderState decoderState(IoSession session) {
        synchronized (session) {
            DecoderState decoderState = (DecoderState) session.getAttribute(DECODER_STATE);
            if (decoderState == null) {
                decoderState = new DecoderState();
                session.setAttribute(DECODER_STATE, decoderState);
            }
            return decoderState;
        }
    }

    /**
     * Holds the state of the decoding process
     */
    private static class DecoderState {
        private int startPos = -1;
        private int currentPos;
        private byte previousByte;

        void reset() {
            startPos = -1;
            currentPos = 0;
            previousByte = 0;
        }

        void markStart(int position) {
            if (isStarted()) {
                LOG.warn("Ignoring message start at position {} before previous message has ended.", position);
            } else {
                startPos = position;
                LOG.debug("Message starts at position {}", startPos);
            }
        }

        void markCurrent(int position) {
            currentPos = position;
        }

        void markPrevious(byte previous) {
            previousByte = previous;
        }

        public int start() {
            return startPos;
        }

        public int current() {
            return currentPos;
        }

        public byte previous() {
            return previousByte;
        }

        public boolean isStarted() {
            return startPos >= 0;
        }
    }
}
