/*
 * 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.tomcat.util.bcel.classfile;

import java.io.DataInput;
import java.io.IOException;

import org.apache.tomcat.util.bcel.Const;

/**
 * This class represents the constant pool, i.e., a table of constants, of a parsed classfile. It may contain null references, due to the JVM specification that
 * skips an entry after an 8-byte constant (double, long) entry. Those interested in generating constant pools programmatically should see
 * <a href="../generic/ConstantPoolGen.html"> ConstantPoolGen</a>.
 *
 * @see Constant
 */
public class ConstantPool {

    private final Constant[] constantPool;

    /**
     * Reads constants from given input stream.
     *
     * @param input Input stream
     * @throws IOException if an I/O error occurs reading the the InputStream
     * @throws  ClassFormatException If the .class file is not valid
     */
    ConstantPool(final DataInput input) throws IOException, ClassFormatException {
        final int constantPoolCount = input.readUnsignedShort();
        constantPool = new Constant[constantPoolCount];
        /*
         * constantPool[0] is unused by the compiler and may be used freely by the implementation.
         */
        for (int i = 1; i < constantPoolCount; i++) {
            constantPool[i] = Constant.readConstant(input);
            /*
             * Quote from the JVM specification: "All eight byte constants take up two spots in the constant pool. If this is the n'th byte in the constant
             * pool, then the next item will be numbered n+2"
             *
             * Thus we have to increment the index counter.
             */
            if (constantPool[i] != null) {
                byte tag = constantPool[i].getTag();
                if (tag == Const.CONSTANT_Double || tag == Const.CONSTANT_Long) {
                    i++;
                }
            }
        }
    }

    /**
     * Gets constant from constant pool.
     *
     * @param <T> A {@link Constant} subclass
     * @param index Index in constant pool
     * @return Constant value
     * @see Constant
     * @throws ClassFormatException if index is invalid
     */
    @SuppressWarnings("unchecked")
    public <T extends Constant> T getConstant(final int index) throws ClassFormatException {
        return (T) getConstant(index, Constant.class);
    }

    /**
     * Gets constant from constant pool and check whether it has the expected type.
     *
     * @param <T> A {@link Constant} subclass
     * @param index Index in constant pool
     * @param tag   Tag of expected constant, i.e., its type
     * @return Constant value
     * @see Constant
     * @throws ClassFormatException if constant type does not match tag
     */
    public <T extends Constant> T getConstant(final int index, final byte tag) throws ClassFormatException {
        final T c = getConstant(index);
        if (c.getTag() != tag) {
            throw new ClassFormatException("Expected class '" + Const.getConstantName(tag) + "' at index " + index + " and got " + c);
        }
        return c;
    }

    /**
     * Gets constant from constant pool.
     *
     * @param <T> A {@link Constant} subclass
     * @param index Index in constant pool
     * @param castTo The {@link Constant} subclass to cast to.
     * @return Constant value
     * @see Constant
     * @throws ClassFormatException if index is invalid
     */
    public <T extends Constant> T getConstant(final int index, final Class<T> castTo) throws ClassFormatException {
        if (index >= constantPool.length || index < 0) {
            throw new ClassFormatException("Invalid constant pool reference using index: " + index + ". Constant pool size is: " + constantPool.length);
        }
        if (constantPool[index] != null && !castTo.isAssignableFrom(constantPool[index].getClass())) {
            throw new ClassFormatException("Invalid constant pool reference at index: " + index +
                    ". Expected " + castTo + " but was " + constantPool[index].getClass());
        }
        // Previous check ensures this won't throw a ClassCastException
        final T c = castTo.cast(constantPool[index]);
        // the 0th element is always null
        if (c == null && index != 0) {
            final Constant prev = constantPool[index - 1];
            if (prev == null || prev.getTag() != Const.CONSTANT_Double && prev.getTag() != Const.CONSTANT_Long) {
                throw new ClassFormatException("Constant pool at index " + index + " is null.");
            }
        }
        return c;
    }

    /**
     * Gets constant from constant pool and check whether it has the expected type.
     *
     * @param index Index in constant pool
     * @return ConstantInteger value
     * @see ConstantInteger
     * @throws ClassFormatException if constant type does not match tag
     */
    public ConstantInteger getConstantInteger(final int index) {
        return getConstant(index, Const.CONSTANT_Integer);
    }

    /**
     * Gets constant from constant pool and check whether it has the expected type.
     *
     * @param index Index in constant pool
     * @return ConstantUtf8 value
     * @see ConstantUtf8
     * @throws ClassFormatException if constant type does not match tag
     */
    public ConstantUtf8 getConstantUtf8(final int index) throws ClassFormatException {
        return getConstant(index, Const.CONSTANT_Utf8);
    }
}
