/*
 * Copyright (C) 2007 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.android.dx.cf.code;

import com.android.dx.rop.cst.CstUtf8;
import com.android.dx.rop.type.Type;
import com.android.dx.rop.code.LocalItem;
import com.android.dx.util.FixedSizeList;

/**
 * List of "local variable" entries, which are the contents of
 * {@code LocalVariableTable} and {@code LocalVariableTypeTable}
 * attributes, as well as combinations of the two.
 */
public final class LocalVariableList extends FixedSizeList {
    /** {@code non-null;} zero-size instance */
    public static final LocalVariableList EMPTY = new LocalVariableList(0);

    /**
     * Returns an instance which is the concatenation of the two given
     * instances. The result is immutable.
     *
     * @param list1 {@code non-null;} first instance
     * @param list2 {@code non-null;} second instance
     * @return {@code non-null;} combined instance
     */
    public static LocalVariableList concat(LocalVariableList list1,
                                           LocalVariableList list2) {
        if (list1 == EMPTY) {
            // easy case
            return list2;
        }

        int sz1 = list1.size();
        int sz2 = list2.size();
        LocalVariableList result = new LocalVariableList(sz1 + sz2);

        for (int i = 0; i < sz1; i++) {
            result.set(i, list1.get(i));
        }

        for (int i = 0; i < sz2; i++) {
            result.set(sz1 + i, list2.get(i));
        }

        result.setImmutable();
        return result;
    }

    /**
     * Returns an instance which is the result of merging the two
     * given instances, where one instance should have only type
     * descriptors and the other only type signatures. The merged
     * result is identical to the one with descriptors, except that
     * any element whose {name, index, start, length} matches an
     * element in the signature list gets augmented with the
     * corresponding signature. The result is immutable.
     *
     * @param descriptorList {@code non-null;} list with descriptors
     * @param signatureList {@code non-null;} list with signatures
     * @return {@code non-null;} the merged result
     */
    public static LocalVariableList mergeDescriptorsAndSignatures(
            LocalVariableList descriptorList,
            LocalVariableList signatureList) {
        int descriptorSize = descriptorList.size();
        LocalVariableList result = new LocalVariableList(descriptorSize);

        for (int i = 0; i < descriptorSize; i++) {
            Item item = descriptorList.get(i);
            Item signatureItem = signatureList.itemToLocal(item);
            if (signatureItem != null) {
                CstUtf8 signature = signatureItem.getSignature();
                item = item.withSignature(signature);
            }
            result.set(i, item);
        }

        result.setImmutable();
        return result;
    }

    /**
     * Constructs an instance.
     *
     * @param count the number of elements to be in the list
     */
    public LocalVariableList(int count) {
        super(count);
    }

    /**
     * Gets the indicated item.
     *
     * @param n {@code >= 0;} which item
     * @return {@code null-ok;} the indicated item
     */
    public Item get(int n) {
        return (Item) get0(n);
    }

    /**
     * Sets the item at the given index.
     *
     * @param n {@code >= 0, < size();} which element
     * @param item {@code non-null;} the item
     */
    public void set(int n, Item item) {
        if (item == null) {
            throw new NullPointerException("item == null");
        }

        set0(n, item);
    }

    /**
     * Sets the item at the given index.
     *
     * <p><b>Note:</b> At least one of {@code descriptor} or
     * {@code signature} must be passed as non-null.</p>
     *
     * @param n {@code >= 0, < size();} which element
     * @param startPc {@code >= 0;} the start pc of this variable's scope
     * @param length {@code >= 0;} the length (in bytecodes) of this variable's
     * scope
     * @param name {@code non-null;} the variable's name
     * @param descriptor {@code null-ok;} the variable's type descriptor
     * @param signature {@code null-ok;} the variable's type signature
     * @param index {@code >= 0;} the variable's local index
     */
    public void set(int n, int startPc, int length, CstUtf8 name,
            CstUtf8 descriptor, CstUtf8 signature, int index) {
        set0(n, new Item(startPc, length, name, descriptor, signature, index));
    }

    /**
     * Gets the local variable information in this instance which matches
     * the given {@link com.android.dx.cf.code.LocalVariableList.Item}
     * in all respects but the type descriptor and signature, if any.
     *
     * @param item {@code non-null;} local variable information to match
     * @return {@code null-ok;} the corresponding local variable information stored
     * in this instance, or {@code null} if there is no matching
     * information
     */
    public Item itemToLocal(Item item) {
        int sz = size();

        for (int i = 0; i < sz; i++) {
            Item one = (Item) get0(i);

            if ((one != null) && one.matchesAllButType(item)) {
                return one;
            }
        }

        return null;
    }

    /**
     * Gets the local variable information associated with a given address
     * and local index, if any. <b>Note:</b> In standard classfiles, a
     * variable's start point is listed as the address of the instruction
     * <i>just past</i> the one that sets the variable.
     *
     * @param pc {@code >= 0;} the address to look up
     * @param index {@code >= 0;} the local variable index
     * @return {@code null-ok;} the associated local variable information, or
     * {@code null} if none is known
     */
    public Item pcAndIndexToLocal(int pc, int index) {
        int sz = size();

        for (int i = 0; i < sz; i++) {
            Item one = (Item) get0(i);

            if ((one != null) && one.matchesPcAndIndex(pc, index)) {
                return one;
            }
        }

        return null;
    }

    /**
     * Item in a local variable table.
     */
    public static class Item {
        /** {@code >= 0;} the start pc of this variable's scope */
        private final int startPc;

        /** {@code >= 0;} the length (in bytecodes) of this variable's scope */
        private final int length;

        /** {@code non-null;} the variable's name */
        private final CstUtf8 name;

        /** {@code null-ok;} the variable's type descriptor */
        private final CstUtf8 descriptor;

        /** {@code null-ok;} the variable's type signature */
        private final CstUtf8 signature;

        /** {@code >= 0;} the variable's local index */
        private final int index;

        /**
         * Constructs an instance.
         *
         * <p><b>Note:</b> At least one of {@code descriptor} or
         * {@code signature} must be passed as non-null.</p>
         *
         * @param startPc {@code >= 0;} the start pc of this variable's scope
         * @param length {@code >= 0;} the length (in bytecodes) of this variable's
         * scope
         * @param name {@code non-null;} the variable's name
         * @param descriptor {@code null-ok;} the variable's type descriptor
         * @param signature {@code null-ok;} the variable's type signature
         * @param index {@code >= 0;} the variable's local index
         */
        public Item(int startPc, int length, CstUtf8 name,
                CstUtf8 descriptor, CstUtf8 signature, int index) {
            if (startPc < 0) {
                throw new IllegalArgumentException("startPc < 0");
            }

            if (length < 0) {
                throw new IllegalArgumentException("length < 0");
            }

            if (name == null) {
                throw new NullPointerException("name == null");
            }

            if ((descriptor == null) && (signature == null)) {
                throw new NullPointerException(
                        "(descriptor == null) && (signature == null)");
            }

            if (index < 0) {
                throw new IllegalArgumentException("index < 0");
            }

            this.startPc = startPc;
            this.length = length;
            this.name = name;
            this.descriptor = descriptor;
            this.signature = signature;
            this.index = index;
        }

        /**
         * Gets the start pc of this variable's scope.
         *
         * @return {@code >= 0;} the start pc of this variable's scope
         */
        public int getStartPc() {
            return startPc;
        }

        /**
         * Gets the length (in bytecodes) of this variable's scope.
         *
         * @return {@code >= 0;} the length (in bytecodes) of this variable's scope
         */
        public int getLength() {
            return length;
        }

        /**
         * Gets the variable's type descriptor.
         *
         * @return {@code null-ok;} the variable's type descriptor
         */
        public CstUtf8 getDescriptor() {
            return descriptor;
        }

        /**
         * Gets the variable's LocalItem, a (name, signature) tuple
         *
         * @return {@code null-ok;} the variable's type descriptor
         */
        public LocalItem getLocalItem() {
            return LocalItem.make(name, signature);
        }

        /**
         * Gets the variable's type signature. Private because if you need this,
         * you want getLocalItem() instead.
         *
         * @return {@code null-ok;} the variable's type signature
         */
        private CstUtf8 getSignature() {
            return signature;
        }

        /**
         * Gets the variable's local index.
         *
         * @return {@code >= 0;} the variable's local index
         */
        public int getIndex() {
            return index;
        }

        /**
         * Gets the variable's type descriptor. This is a convenient shorthand
         * for {@code Type.intern(getDescriptor().getString())}.
         *
         * @return {@code non-null;} the variable's type
         */
        public Type getType() {
            return Type.intern(descriptor.getString());
        }

        /**
         * Constructs and returns an instance which is identical to this
         * one, except that the signature is changed to the given value.
         *
         * @param newSignature {@code non-null;} the new signature
         * @return {@code non-null;} an appropriately-constructed instance
         */
        public Item withSignature(CstUtf8 newSignature) {
            return new Item(startPc, length, name, descriptor, newSignature,
                    index);
        }

        /**
         * Gets whether this instance matches (describes) the given
         * address and index.
         *
         * @param pc {@code >= 0;} the address in question
         * @param index {@code >= 0;} the local variable index in question
         * @return {@code true} iff this instance matches {@code pc}
         * and {@code index}
         */
        public boolean matchesPcAndIndex(int pc, int index) {
            return (index == this.index) &&
                (pc >= startPc) &&
                (pc < (startPc + length));
        }

        /**
         * Gets whether this instance matches (describes) the given
         * other instance exactly in all fields except type descriptor and
         * type signature.
         *
         * @param other {@code non-null;} the instance to compare to
         * @return {@code true} iff this instance matches
         */
        public boolean matchesAllButType(Item other) {
            return (startPc == other.startPc)
                && (length == other.length)
                && (index == other.index)
                && name.equals(other.name);
        }
    }
}
