/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * 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 com.pojavdx.dex;

import com.pojavdx.dex.util.ByteInput;

/**
 * Pull parser for encoded values.
 */
public final class EncodedValueReader {
    public static final int ENCODED_BYTE = 0x00;
    public static final int ENCODED_SHORT = 0x02;
    public static final int ENCODED_CHAR = 0x03;
    public static final int ENCODED_INT = 0x04;
    public static final int ENCODED_LONG = 0x06;
    public static final int ENCODED_FLOAT = 0x10;
    public static final int ENCODED_DOUBLE = 0x11;
    public static final int ENCODED_METHOD_TYPE = 0x15;
    public static final int ENCODED_METHOD_HANDLE = 0x16;
    public static final int ENCODED_STRING = 0x17;
    public static final int ENCODED_TYPE = 0x18;
    public static final int ENCODED_FIELD = 0x19;
    public static final int ENCODED_ENUM = 0x1b;
    public static final int ENCODED_METHOD = 0x1a;
    public static final int ENCODED_ARRAY = 0x1c;
    public static final int ENCODED_ANNOTATION = 0x1d;
    public static final int ENCODED_NULL = 0x1e;
    public static final int ENCODED_BOOLEAN = 0x1f;

    /** placeholder type if the type is not yet known */
    private static final int MUST_READ = -1;

    protected final ByteInput in;
    private int type = MUST_READ;
    private int annotationType;
    private int arg;

    public EncodedValueReader(ByteInput in) {
        this.in = in;
    }

    public EncodedValueReader(EncodedValue in) {
        this(in.asByteInput());
    }

    /**
     * Creates a new encoded value reader whose only value is the specified
     * known type. This is useful for encoded values without a type prefix,
     * such as class_def_item's encoded_array or annotation_item's
     * encoded_annotation.
     */
    public EncodedValueReader(ByteInput in, int knownType) {
        this.in = in;
        this.type = knownType;
    }

    public EncodedValueReader(EncodedValue in, int knownType) {
        this(in.asByteInput(), knownType);
    }

    /**
     * Returns the type of the next value to read.
     */
    public int peek() {
        if (type == MUST_READ) {
            int argAndType = in.readByte() & 0xff;
            type = argAndType & 0x1f;
            arg = (argAndType & 0xe0) >> 5;
        }
        return type;
    }

    /**
     * Begins reading the elements of an array, returning the array's size. The
     * caller must follow up by calling a read method for each element in the
     * array. For example, this reads a byte array: <pre>   {@code
     *   int arraySize = readArray();
     *   for (int i = 0, i < arraySize; i++) {
     *     readByte();
     *   }
     * }</pre>
     */
    public int readArray() {
        checkType(ENCODED_ARRAY);
        type = MUST_READ;
        return Leb128.readUnsignedLeb128(in);
    }

    /**
     * Begins reading the fields of an annotation, returning the number of
     * fields. The caller must follow up by making alternating calls to {@link
     * #readAnnotationName()} and another read method. For example, this reads
     * an annotation whose fields are all bytes: <pre>   {@code
     *   int fieldCount = readAnnotation();
     *   int annotationType = getAnnotationType();
     *   for (int i = 0; i < fieldCount; i++) {
     *       readAnnotationName();
     *       readByte();
     *   }
     * }</pre>
     */
    public int readAnnotation() {
        checkType(ENCODED_ANNOTATION);
        type = MUST_READ;
        annotationType = Leb128.readUnsignedLeb128(in);
        return Leb128.readUnsignedLeb128(in);
    }

    /**
     * Returns the type of the annotation just returned by {@link
     * #readAnnotation()}. This method's value is undefined unless the most
     * recent call was to {@link #readAnnotation()}.
     */
    public int getAnnotationType() {
        return annotationType;
    }

    public int readAnnotationName() {
        return Leb128.readUnsignedLeb128(in);
    }

    public byte readByte() {
        checkType(ENCODED_BYTE);
        type = MUST_READ;
        return (byte) EncodedValueCodec.readSignedInt(in, arg);
    }

    public short readShort() {
        checkType(ENCODED_SHORT);
        type = MUST_READ;
        return (short) EncodedValueCodec.readSignedInt(in, arg);
    }

    public char readChar() {
        checkType(ENCODED_CHAR);
        type = MUST_READ;
        return (char) EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readInt() {
        checkType(ENCODED_INT);
        type = MUST_READ;
        return EncodedValueCodec.readSignedInt(in, arg);
    }

    public long readLong() {
        checkType(ENCODED_LONG);
        type = MUST_READ;
        return EncodedValueCodec.readSignedLong(in, arg);
    }

    public float readFloat() {
        checkType(ENCODED_FLOAT);
        type = MUST_READ;
        return Float.intBitsToFloat(EncodedValueCodec.readUnsignedInt(in, arg, true));
    }

    public double readDouble() {
        checkType(ENCODED_DOUBLE);
        type = MUST_READ;
        return Double.longBitsToDouble(EncodedValueCodec.readUnsignedLong(in, arg, true));
    }

    public int readMethodType() {
        checkType(ENCODED_METHOD_TYPE);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readMethodHandle() {
        checkType(ENCODED_METHOD_HANDLE);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readString() {
        checkType(ENCODED_STRING);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readType() {
        checkType(ENCODED_TYPE);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readField() {
        checkType(ENCODED_FIELD);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readEnum() {
        checkType(ENCODED_ENUM);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public int readMethod() {
        checkType(ENCODED_METHOD);
        type = MUST_READ;
        return EncodedValueCodec.readUnsignedInt(in, arg, false);
    }

    public void readNull() {
        checkType(ENCODED_NULL);
        type = MUST_READ;
    }

    public boolean readBoolean() {
        checkType(ENCODED_BOOLEAN);
        type = MUST_READ;
        return arg != 0;
    }

    /**
     * Skips a single value, including its nested values if it is an array or
     * annotation.
     */
    public void skipValue() {
        switch (peek()) {
        case ENCODED_BYTE:
            readByte();
            break;
        case ENCODED_SHORT:
            readShort();
            break;
        case ENCODED_CHAR:
            readChar();
            break;
        case ENCODED_INT:
            readInt();
            break;
        case ENCODED_LONG:
            readLong();
            break;
        case ENCODED_FLOAT:
            readFloat();
            break;
        case ENCODED_DOUBLE:
            readDouble();
            break;
        case ENCODED_METHOD_TYPE:
            readMethodType();
            break;
        case ENCODED_METHOD_HANDLE:
            readMethodHandle();
            break;
        case ENCODED_STRING:
            readString();
            break;
        case ENCODED_TYPE:
            readType();
            break;
        case ENCODED_FIELD:
            readField();
            break;
        case ENCODED_ENUM:
            readEnum();
            break;
        case ENCODED_METHOD:
            readMethod();
            break;
        case ENCODED_ARRAY:
            for (int i = 0, size = readArray(); i < size; i++) {
                skipValue();
            }
            break;
        case ENCODED_ANNOTATION:
            for (int i = 0, size = readAnnotation(); i < size; i++) {
                readAnnotationName();
                skipValue();
            }
            break;
        case ENCODED_NULL:
            readNull();
            break;
        case ENCODED_BOOLEAN:
            readBoolean();
            break;
        default:
            throw new DexException("Unexpected type: " + Integer.toHexString(type));
        }
    }

    private void checkType(int expected) {
        if (peek() != expected) {
            throw new IllegalStateException(
                    String.format("Expected %x but was %x", expected, peek()));
        }
    }
}
