/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2017 Eric Lafortune @ GuardSquare
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License as published by the Free
 * Software Foundation; either version 2 of the License, or (at your option)
 * any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
 */
package proguard.evaluation;

import proguard.evaluation.value.*;

import java.util.Arrays;

/**
 * This class represents a local variable frame that contains <code>Value</code>
 * objects. Values are generalizations of all values that have been stored in
 * the respective variables.
 *
 * @author Eric Lafortune
 */
public class Variables
{
    private static final TopValue TOP_VALUE = new TopValue();


    protected Value[] values;
    protected int     size;


    /**
     * Creates a new Variables object with a given maximum number of variables.
     */
    public Variables(int size)
    {
        this.values = new Value[size];
        this.size   = size;
    }


    /**
     * Creates a Variables object that is a copy of the given Variables object.
     */
    public Variables(Variables variables)
    {
        // Create the values array.
        this(variables.size);

        // Copy the values.
        initialize(variables);
    }


    /**
     * Resets this Variables object, so that it can be reused.
     */
    public void reset(int size)
    {
        // Is the values array large enough?
        if (values.length < size)
        {
            // Create a new one.
            values = new Value[size];
        }
        else
        {
            // Clear the old variables.
            Arrays.fill(values, 0, this.size, null);
        }

        this.size = size;
    }


    /**
     * Initializes the values of this Variables object with the values of the
     * given Variables object. The other object may have fewer values, in which
     * case the remaining values are left unchanged.
     */
    public void initialize(Variables other)
    {
        if (this.size < other.size)
        {
            throw new IllegalArgumentException("Variable frame is too small ["+this.size+"] compared to other frame ["+other.size+"]");
        }

        // Copy the values.
        System.arraycopy(other.values, 0, this.values, 0, other.size);
    }


    /**
     * Generalizes the values of this Variables object with the values of the
     * given Variables object.
     * @param clearConflictingOtherVariables specifies whether the other
     *                                       variables should be cleared too,
     *                                       in case of conflicts.
     * @return whether the generalization has made any difference.
     */
    public boolean generalize(Variables other,
                              boolean   clearConflictingOtherVariables)
    {
        if (this.size != other.size)
        {
            throw new IllegalArgumentException("Variable frames have different sizes ["+this.size+"] and ["+other.size+"]");
        }

        boolean changed = false;

        for (int index = 0; index < size; index++)
        {
            Value thisValue  = this.values[index];
            Value otherValue = other.values[index];

            // Occasionally, two values of different types might be present
            // in the same variable in a variable frame (corresponding to
            // two local variables that share the same index), at some point
            // outside of their scopes. Don't generalize the variable then,
            // but let it clear instead.
            if (thisValue  != null &&
                otherValue != null &&
                thisValue.computationalType() == otherValue.computationalType())
            {
                Value newValue = thisValue.generalize(otherValue);

                changed = changed || !thisValue.equals(newValue);

                this.values[index] = newValue;
            }
            else
            {
                changed = changed || thisValue != null;

                this.values[index] = null;

                if (clearConflictingOtherVariables)
                {
                    other.values[index] = null;
                }
            }
        }

        return changed;
    }


    /**
     * Returns the number of variables.
     */
    public int size()
    {
        return size;
    }


    /**
     * Gets the Value of the variable with the given index, without disturbing it.
     */
    public Value getValue(int index)
    {
        if (index < 0 ||
            index >= size)
        {
            throw new IndexOutOfBoundsException("Variable index ["+index+"] out of bounds ["+size+"]");
        }

        return values[index];
    }


    /**
     * Stores the given Value at the given variable index.
     */
    public void store(int index, Value value)
    {
        if (index < 0 ||
            index >= size)
        {
            throw new IndexOutOfBoundsException("Variable index ["+index+"] out of bounds ["+size+"]");
        }

        // Store the value.
        values[index] = value;

        // Account for the extra space required by Category 2 values.
        if (value.isCategory2())
        {
            values[index + 1] = TOP_VALUE;
        }
    }


    /**
     * Loads the Value from the variable with the given index.
     */
    public Value load(int index)
    {
        if (index < 0 ||
            index >= size)
        {
            throw new IndexOutOfBoundsException("Variable index ["+index+"] out of bounds ["+size+"]");
        }

        return values[index];
    }


    // Load methods that provide convenient casts to the expected value types.

    /**
     * Loads the IntegerValue from the variable with the given index.
     */
    public IntegerValue iload(int index)
    {
        return load(index).integerValue();
    }


    /**
     * Loads the LongValue from the variable with the given index.
     */
    public LongValue lload(int index)
    {
        return load(index).longValue();
    }


    /**
     * Loads the FloatValue from the variable with the given index.
     */
    public FloatValue fload(int index)
    {
        return load(index).floatValue();
    }


    /**
     * Loads the DoubleValue from the variable with the given index.
     */
    public DoubleValue dload(int index)
    {
        return load(index).doubleValue();
    }


    /**
     * Loads the ReferenceValue from the variable with the given index.
     */
    public ReferenceValue aload(int index)
    {
        return load(index).referenceValue();
    }


    /**
     * Loads the InstructionOffsetValue from the variable with the given index.
     */
    public InstructionOffsetValue oload(int index)
    {
        return load(index).instructionOffsetValue();
    }


    // Implementations for Object.

    public boolean equals(Object object)
    {
        if (object == null ||
            this.getClass() != object.getClass())
        {
            return false;
        }

        Variables other = (Variables)object;

        if (this.size != other.size)
        {
            return false;
        }

        for (int index = 0; index < size; index++)
        {
            Value thisValue  = this.values[index];
            Value otherValue = other.values[index];

            // Occasionally, two values of different types might be
            // present in the same variable in a variable frame
            // (corresponding to two local variables that share the
            // same index), at some point outside of their scopes.
            // We'll ignore these.
            if (thisValue  != null &&
                otherValue != null &&
                thisValue.computationalType() == otherValue.computationalType() &&
                !thisValue.equals(otherValue))
            {
                return false;
            }
        }

        return true;
    }


    public int hashCode()
    {
        int hashCode = size;

        for (int index = 0; index < size; index++)
        {
            Value value = values[index];
            if (value != null)
            {
                hashCode ^= value.hashCode();
            }
        }

        return hashCode;
    }


    public String toString()
    {
        StringBuffer buffer = new StringBuffer();

        for (int index = 0; index < size; index++)
        {
            Value value = values[index];
            buffer = buffer.append('[')
                           .append(value == null ? "empty" : value.toString())
                           .append(']');
        }

        return buffer.toString();
    }
}
