// Protocol Buffers - Google's data interchange format
// Copyright 2008 Google Inc.
// http://code.google.com/p/protobuf/
//
// 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.hawtbuf.proto;

import java.io.EOFException;
import java.io.FilterInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.fusesource.hawtbuf.Buffer;
import org.fusesource.hawtbuf.BufferInputStream;

/**
 * Reads and decodes protocol message fields.
 * 
 * This class contains two kinds of methods: methods that read specific protocol
 * message constructs and field types (e.g. {@link #readTag()} and
 * {@link #readInt32()}) and methods that read low-level values (e.g.
 * {@link #readRawVarint32()} and {@link #readRawBytes}). If you are reading
 * encoded protocol messages, you should use the former methods, but if you are
 * reading some other format of your own design, use the latter.
 * 
 * @author kenton@google.com Kenton Varda
 */
public final class CodedInputStream extends FilterInputStream {

    private int lastTag = 0;
    private int limit = Integer.MAX_VALUE;
    private int pos;
    private BufferInputStream bis;
    
    public CodedInputStream(InputStream in) {
        super(in);
        if( in.getClass() == BufferInputStream.class ) {
            bis = (BufferInputStream)in;
        }
    }

    public CodedInputStream(Buffer data) {
        this(new BufferInputStream(data));
        limit = data.length;
    }

    public CodedInputStream(byte[] data) {
        this(new BufferInputStream(data));
        limit = data.length;
    }

    /**
     * Attempt to read a field tag, returning zero if we have reached EOF.
     * Protocol message parsers use this to read tags, since a protocol message
     * may legally end wherever a tag occurs, and zero is not a valid tag
     * number.
     */
    public int readTag() throws IOException {
        if( pos >= limit ) {
            lastTag=0;
            return 0;
        }
        try {
            lastTag = readRawVarint32();
            if (lastTag == 0) {
                // If we actually read zero, that's not a valid tag.
                throw InvalidProtocolBufferException.invalidTag();
            }
            return lastTag;
        } catch (EOFException e) {
            lastTag=0;
            return 0;
        }
    }

    
    /**
     * Verifies that the last call to readTag() returned the given tag value.
     * This is used to verify that a nested group ended with the correct end
     * tag.
     * 
     * @throws InvalidProtocolBufferException
     *             {@code value} does not match the last tag.
     */
    public void checkLastTagWas(int value) throws InvalidProtocolBufferException {
        if (lastTag != value) {
            throw InvalidProtocolBufferException.invalidEndTag();
        }
    }

    /**
     * Reads and discards a single field, given its tag value.
     * 
     * @return {@code false} if the tag is an endgroup tag, in which case
     *         nothing is skipped. Otherwise, returns {@code true}.
     */
    public boolean skipField(int tag) throws IOException {
        switch (WireFormat.getTagWireType(tag)) {
        case WireFormat.WIRETYPE_VARINT:
            readInt32();
            return true;
        case WireFormat.WIRETYPE_FIXED64:
            readRawLittleEndian64();
            return true;
        case WireFormat.WIRETYPE_LENGTH_DELIMITED:
            skipRawBytes(readRawVarint32());
            return true;
        case WireFormat.WIRETYPE_START_GROUP:
            skipMessage();
            checkLastTagWas(WireFormat.makeTag(WireFormat.getTagFieldNumber(tag), WireFormat.WIRETYPE_END_GROUP));
            return true;
        case WireFormat.WIRETYPE_END_GROUP:
            return false;
        case WireFormat.WIRETYPE_FIXED32:
            readRawLittleEndian32();
            return true;
        default:
            throw InvalidProtocolBufferException.invalidWireType();
        }
    }

    /**
     * Reads and discards an entire message. This will read either until EOF or
     * until an endgroup tag, whichever comes first.
     */
    public void skipMessage() throws IOException {
        while (true) {
            int tag = readTag();
            if (tag == 0 || !skipField(tag))
                return;
        }
    }

    // -----------------------------------------------------------------

    /** Read a {@code double} field value from the stream. */
    public double readDouble() throws IOException {
        return Double.longBitsToDouble(readRawLittleEndian64());
    }

    /** Read a {@code float} field value from the stream. */
    public float readFloat() throws IOException {
        return Float.intBitsToFloat(readRawLittleEndian32());
    }

    /** Read a {@code uint64} field value from the stream. */
    public long readUInt64() throws IOException {
        return readRawVarint64();
    }

    /** Read an {@code int64} field value from the stream. */
    public long readInt64() throws IOException {
        return readRawVarint64();
    }

    /** Read an {@code int32} field value from the stream. */
    public int readInt32() throws IOException {
        return readRawVarint32();
    }

    /** Read a {@code fixed64} field value from the stream. */
    public long readFixed64() throws IOException {
        return readRawLittleEndian64();
    }

    /** Read a {@code fixed32} field value from the stream. */
    public int readFixed32() throws IOException {
        return readRawLittleEndian32();
    }

    /** Read a {@code bool} field value from the stream. */
    public boolean readBool() throws IOException {
        return readRawVarint32() != 0;
    }

    /** Read a {@code string} field value from the stream. */
    public String readString() throws IOException {
        int size = readRawVarint32();
        Buffer data = readRawBytes(size);
        return new String(data.data, data.offset, data.length, "UTF-8");
    }

    /** Read a {@code bytes} field value from the stream. */
    public Buffer readBytes() throws IOException {
        int size = readRawVarint32();
        return readRawBytes(size);
    }

    /** Read a {@code uint32} field value from the stream. */
    public int readUInt32() throws IOException {
        return readRawVarint32();
    }

    /**
     * Read an enum field value from the stream. Caller is responsible for
     * converting the numeric value to an actual enum.
     */
    public int readEnum() throws IOException {
        return readRawVarint32();
    }

    /** Read an {@code sfixed32} field value from the stream. */
    public int readSFixed32() throws IOException {
        return readRawLittleEndian32();
    }

    /** Read an {@code sfixed64} field value from the stream. */
    public long readSFixed64() throws IOException {
        return readRawLittleEndian64();
    }

    /** Read an {@code sint32} field value from the stream. */
    public int readSInt32() throws IOException {
        return decodeZigZag32(readRawVarint32());
    }

    /** Read an {@code sint64} field value from the stream. */
    public long readSInt64() throws IOException {
        return decodeZigZag64(readRawVarint64());
    }

    // =================================================================

    /**
     * Read a raw Varint from the stream. If larger than 32 bits, discard the
     * upper bits.
     */
    public int readRawVarint32() throws IOException {
        byte tmp = readRawByte();
        if (tmp >= 0) {
            return tmp;
        }
        int result = tmp & 0x7f;
        if ((tmp = readRawByte()) >= 0) {
            result |= tmp << 7;
        } else {
            result |= (tmp & 0x7f) << 7;
            if ((tmp = readRawByte()) >= 0) {
                result |= tmp << 14;
            } else {
                result |= (tmp & 0x7f) << 14;
                if ((tmp = readRawByte()) >= 0) {
                    result |= tmp << 21;
                } else {
                    result |= (tmp & 0x7f) << 21;
                    result |= (tmp = readRawByte()) << 28;
                    if (tmp < 0) {
                        // Discard upper 32 bits.
                        for (int i = 0; i < 5; i++) {
                            if (readRawByte() >= 0)
                                return result;
                        }
                        throw InvalidProtocolBufferException.malformedVarint();
                    }
                }
            }
        }
        return result;
    }

    /** Read a raw Varint from the stream. */
    public long readRawVarint64() throws IOException {
        int shift = 0;
        long result = 0;
        while (shift < 64) {
            byte b = readRawByte();
            result |= (long) (b & 0x7F) << shift;
            if ((b & 0x80) == 0)
                return result;
            shift += 7;
        }
        throw InvalidProtocolBufferException.malformedVarint();
    }

    /** Read a 32-bit little-endian integer from the stream. */
    public int readRawLittleEndian32() throws IOException {
        byte b1 = readRawByte();
        byte b2 = readRawByte();
        byte b3 = readRawByte();
        byte b4 = readRawByte();
        return (((int) b1 & 0xff)) | (((int) b2 & 0xff) << 8) | (((int) b3 & 0xff) << 16) | (((int) b4 & 0xff) << 24);
    }

    /** Read a 64-bit little-endian integer from the stream. */
    public long readRawLittleEndian64() throws IOException {
        byte b1 = readRawByte();
        byte b2 = readRawByte();
        byte b3 = readRawByte();
        byte b4 = readRawByte();
        byte b5 = readRawByte();
        byte b6 = readRawByte();
        byte b7 = readRawByte();
        byte b8 = readRawByte();
        return (((long) b1 & 0xff)) | (((long) b2 & 0xff) << 8) | (((long) b3 & 0xff) << 16) | (((long) b4 & 0xff) << 24) | (((long) b5 & 0xff) << 32) | (((long) b6 & 0xff) << 40) | (((long) b7 & 0xff) << 48) | (((long) b8 & 0xff) << 56);
    }

    /**
     * Decode a ZigZag-encoded 32-bit value. ZigZag encodes signed integers into
     * values that can be efficiently encoded with varint. (Otherwise, negative
     * values must be sign-extended to 64 bits to be varint encoded, thus always
     * taking 10 bytes on the wire.)
     * 
     * @param n
     *            An unsigned 32-bit integer, stored in a signed int because
     *            Java has no explicit unsigned support.
     * @return A signed 32-bit integer.
     */
    public static int decodeZigZag32(int n) {
        return (n >>> 1) ^ -(n & 1);
    }

    /**
     * Decode a ZigZag-encoded 64-bit value. ZigZag encodes signed integers into
     * values that can be efficiently encoded with varint. (Otherwise, negative
     * values must be sign-extended to 64 bits to be varint encoded, thus always
     * taking 10 bytes on the wire.)
     * 
     * @param n
     *            An unsigned 64-bit integer, stored in a signed int because
     *            Java has no explicit unsigned support.
     * @return A signed 64-bit integer.
     */
    public static long decodeZigZag64(long n) {
        return (n >>> 1) ^ -(n & 1);
    }   

    /**
     * Read one byte from the input.
     * 
     * @throws InvalidProtocolBufferException
     *             The end of the stream or the current limit was reached.
     */
    public byte readRawByte() throws IOException {
        if( pos >= limit ) {
            throw new EOFException();
        }
        int rc = in.read();
        if( rc < 0 ) {
            throw new EOFException();
        }
        pos++;
        return (byte)( rc & 0xFF); 
    }

    /**
     * Read a fixed size of bytes from the input.
     * 
     * @throws InvalidProtocolBufferException
     *             The end of the stream or the current limit was reached.
     */
    public Buffer readRawBytes(int size) throws IOException {
        if( size == 0) {
            return new Buffer(new byte[]{});
        }
        if( this.pos+size > limit ) {
            throw new EOFException();
        }
        
        // If the underlying stream is a ByteArrayInputStream
        // then we can avoid an array copy.
        if( bis!=null ) {
            Buffer rc = bis.readBuffer(size);
            if( rc==null || rc.getLength() < size ) {
                throw new EOFException();
            }
            this.pos += rc.getLength();
            return rc;
        }

        // Otherwise we, have to do it the old fasioned way
        byte[] rc = new byte[size];
        int c;
        int pos=0;
        while( pos < size ) {
            c = in.read(rc, pos, size-pos);
            if( c < 0 ) {
                throw new EOFException();
            }
            this.pos += c;
            pos += c;
        }
        
        return new Buffer(rc);
    }

    /**
     * Reads and discards {@code size} bytes.
     * 
     * @throws InvalidProtocolBufferException
     *             The end of the stream or the current limit was reached.
     */
    public void skipRawBytes(int size) throws IOException {
        int pos = 0;
        while (pos < size) {
            int n = (int) in.skip(size - pos);
            pos += n;
        }
    }

    public int pushLimit(int limit) {
        int rc = this.limit;
        this.limit = pos+limit;
        return rc;
    }

    public void popLimit(int limit) {
        this.limit = limit;
    }
  
}
