/**
 * Copyright (C) 2010-2012, FuseSource Corp.  All rights reserved.
 *
 *     http://fusesource.com
 *
 * Licensed 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.fusesource.mqtt.codec;

import org.fusesource.hawtbuf.Buffer;
import org.fusesource.hawtdispatch.transport.AbstractProtocolCodec;
import org.fusesource.hawtdispatch.util.BufferPools;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * <p>
 * </p>
 *
 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
 */
public class MQTTProtocolCodec extends AbstractProtocolCodec {

    private static final BufferPools BUFFER_POOLS = new BufferPools();

    private int maxMessageLength = 1024*1024*100;

    public MQTTProtocolCodec() {
        this.bufferPools = BUFFER_POOLS;
    }

    public int getMaxMessageLength() {
        return maxMessageLength;
    }

    public void setMaxMessageLength(int maxMessageLength) {
        this.maxMessageLength = maxMessageLength;
    }

    @Override
    protected void encode(Object value) throws IOException {
        MQTTFrame frame = (MQTTFrame) value;
        nextWriteBuffer.write(frame.header());

        int remaining = 0;
        for(Buffer buffer : frame.buffers) {
            remaining += buffer.length;
        }
        do {
            byte digit = (byte) (remaining & 0x7F);
            remaining >>>= 7;
            if (remaining > 0) {
                digit |= 0x80;
            }
            nextWriteBuffer.write(digit);
        } while (remaining > 0);
        for(Buffer buffer : frame.buffers) {
            nextWriteBuffer.write(buffer.data, buffer.offset, buffer.length);
        }
    }

    @Override
    protected Action initialDecodeAction() {
        return readHeader;
    }

    private final Action readHeader = new Action() {
        public MQTTFrame apply() throws IOException {
            int length = readLength();
            if( length >= 0 ) {
                if( length > maxMessageLength) {
                    throw new IOException("The maximum message length was exceeded");
                }
                byte header = readBuffer.get(readStart);
                readStart = readEnd;
                if( length > 0 ) {
                    nextDecodeAction = readBody(header, length);
                } else {
                    return new MQTTFrame().header(header);
                }
            }
            return null;
        }
    };

    private int readLength() throws IOException {
        readEnd = readStart+2; // Header is at least 2 bytes..
        int limit = readBuffer.position();
        int length = 0;
        int multiplier = 1;
        byte digit;

        while (readEnd-1 < limit) {
            // last byte is part of the encoded length..
            digit = readBuffer.get(readEnd-1);
            length += (digit & 0x7F) * multiplier;
            if( (digit & 0x80) == 0 ) {
                return length;
            }

            // length extends out one more byte..
            multiplier <<= 7;
            readEnd++;
        }
        return -1;
    }

    Action readBody(final byte header, final int length) {
        return new Action() {
            public MQTTFrame apply() throws IOException {
                int limit = readBuffer.position();
                if ((limit - readStart) < length) {
                    readEnd = limit;
                    return null;
                } else {
                    Buffer body = new Buffer(readBuffer.array(), readStart, length);
                    readEnd = readStart = readStart + length;
                    nextDecodeAction = readHeader;
                    return new MQTTFrame(body).header(header);
                }
            }
        };
    }
}
