/*
 * ProGuard -- shrinking, optimization, obfuscation, and preverification
 *             of Java bytecode.
 *
 * Copyright (c) 2002-2007 Eric Lafortune (eric@graphics.cornell.edu)
 *
 * 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.value;

import proguard.classfile.*;
import proguard.classfile.util.ClassUtil;
import proguard.classfile.visitor.ClassCollector;

import java.util.*;

/**
 * This class represents a partially evaluated reference value. It has a type
 * and a flag that indicates whether the value could be <code>null</code>. If
 * the type is <code>null</code>, the value is <code>null</code>.
 *
 * @author Eric Lafortune
 */
public class ReferenceValue extends Category1Value
{
    private static final boolean DEBUG = false;


    private final String  type;
    private final Clazz   referencedClass;
    private final boolean mayBeNull;


    /**
     * Creates a new ReferenceValue.
     */
    public ReferenceValue(String  type,
                          Clazz   referencedClass,
                          boolean mayBeNull)
    {
        this.type            = type;
        this.referencedClass = referencedClass;
        this.mayBeNull       = mayBeNull;
    }


    /**
     * Returns the type.
     */
    public String getType()
    {
        return type;
    }


    /**
     * Returns the class that is referenced by the type.
     */
    public Clazz getReferencedClass()
    {
        return referencedClass;
    }


    // Basic unary methods.

    /**
     * Returns whether the type is <code>null</code>.
     */
    public int isNull()
    {
        return type == null ? ALWAYS :
               mayBeNull    ? MAYBE  :
                              NEVER;
    }


    /**
     * Returns whether the type is an instance of the given type.
     */
    public int instanceOf(String otherType, Clazz otherReferencedClass)
    {
        String thisType = this.type;

        // If this type is null, it is never an instance of any class.
        if (thisType == null)
        {
            return NEVER;
        }

        // Start taking into account the type dimensions.
        int thisDimensionCount   = ClassUtil.internalArrayTypeDimensionCount(thisType);
        int otherDimensionCount  = ClassUtil.internalArrayTypeDimensionCount(otherType);
        int commonDimensionCount = Math.min(thisDimensionCount, otherDimensionCount);

        // Strip any common array prefixes.
        thisType  = thisType.substring(commonDimensionCount);
        otherType = otherType.substring(commonDimensionCount);

        // If either stripped type is a primitive type, we can tell right away.
        if (commonDimensionCount > 0 &&
            (ClassUtil.isInternalPrimitiveType(thisType.charAt(0)) ||
             ClassUtil.isInternalPrimitiveType(otherType.charAt(0))))
        {
            return !thisType.equals(otherType) ? NEVER :
                   mayBeNull                   ? MAYBE :
                                                 ALWAYS;
        }

        // Strip the class type prefix and suffix of this type, if any.
        if (thisDimensionCount == commonDimensionCount)
        {
            thisType = ClassUtil.internalClassNameFromClassType(thisType);
        }

        // Strip the class type prefix and suffix of the other type, if any.
        if (otherDimensionCount == commonDimensionCount)
        {
            otherType = ClassUtil.internalClassNameFromClassType(otherType);
        }

        // If this type is an array type, and the other type is not
        // java.lang.Object, java.lang.Cloneable, or java.io.Serializable,
        // this type can never be an instance.
        if (thisDimensionCount > otherDimensionCount &&
            !ClassUtil.isInternalArrayInterfaceName(otherType))
        {
            return NEVER;
        }

        // If the other type is an array type, and this type is not
        // java.lang.Object, java.lang.Cloneable, or java.io.Serializable,
        // this type can never be an instance.
        if (thisDimensionCount < otherDimensionCount &&
            !ClassUtil.isInternalArrayInterfaceName(thisType))
        {
            return NEVER;
        }

        // If this type may be null, it might not be an instance of any class.
        if (mayBeNull)
        {
            return MAYBE;
        }

        // If this type is equal to the other type, or if the other type is
        // java.lang.Object, this type is always an instance.
        if (thisType.equals(otherType) ||
            ClassConstants.INTERNAL_NAME_JAVA_LANG_OBJECT.equals(otherType))
        {
            return ALWAYS;
        }

        // If this type is an array type, it's ok.
        if (thisDimensionCount > otherDimensionCount)
        {
            return ALWAYS;
        }

        // If the other type is an array type, it might be ok.
        if (thisDimensionCount < otherDimensionCount)
        {
            return MAYBE;
        }

        // If the value extends the type, we're sure.
        return referencedClass      != null &&
               otherReferencedClass != null &&
               referencedClass.extendsOrImplements(otherReferencedClass) ?
                   ALWAYS :
                   MAYBE;
    }


    /**
     * Returns the length of the array, assuming this type is an array.
     */
    public IntegerValue arrayLength(ValueFactory valueFactory)
    {
        return valueFactory.createIntegerValue();
    }


    /**
     * Returns the value of the array at the given index, assuming this type
     * is an array.
     */
    public Value arrayLoad(IntegerValue integerValue, ValueFactory valueFactory)
    {
        return
            type == null                         ? ValueFactory.REFERENCE_VALUE_NULL                        :
            !ClassUtil.isInternalArrayType(type) ? ValueFactory.REFERENCE_VALUE_JAVA_LANG_OBJECT_MAYBE_NULL :
                                                   valueFactory.createValue(type.substring(1),
                                                                            referencedClass,
                                                                            true);
    }


    // Basic binary methods.

    /**
     * Returns the generalization of this ReferenceValue and the given other
     * ReferenceValue.
     */
    public ReferenceValue generalize(ReferenceValue other)
    {
        String thisType  = this.type;
        String otherType = other.type;

        // If both types are nul, the generalization is null too.
        if (thisType == null && otherType == null)
        {
            return ValueFactory.REFERENCE_VALUE_NULL;
        }

        // If this type is null, the generalization is the other type, maybe null.
        if (thisType == null)
        {
            return other.generalizeMayBeNull(true);
        }

        // If the other type is null, the generalization is this type, maybe null.
        if (otherType == null)
        {
            return this.generalizeMayBeNull(true);
        }

        boolean mayBeNull = this.mayBeNull || other.mayBeNull;

        // If the two types are equal, the generalization remains the same, maybe null.
        if (thisType.equals(otherType))
        {
            return this.generalizeMayBeNull(mayBeNull);
        }

        // Start taking into account the type dimensions.
        int thisDimensionCount   = ClassUtil.internalArrayTypeDimensionCount(thisType);
        int otherDimensionCount  = ClassUtil.internalArrayTypeDimensionCount(otherType);
        int commonDimensionCount = Math.min(thisDimensionCount, otherDimensionCount);

        if (thisDimensionCount == otherDimensionCount)
        {
            // See if we can take into account the referenced classes.
            Clazz thisReferencedClass  = this.referencedClass;
            Clazz otherReferencedClass = other.referencedClass;

            if (thisReferencedClass  != null &&
                otherReferencedClass != null)
            {
                if (thisReferencedClass.extendsOrImplements(otherReferencedClass))
                {
                    return other.generalizeMayBeNull(mayBeNull);
                }

                if (otherReferencedClass.extendsOrImplements(thisReferencedClass))
                {
                    return this.generalizeMayBeNull(mayBeNull);
                }

                // Collect the superclasses and interfaces of this class.
                Set thisSuperClasses = new HashSet();
                thisReferencedClass.hierarchyAccept(false, true, true, false,
                                                    new ClassCollector(thisSuperClasses));

                // Collect the superclasses and interfaces of the other class.
                Set otherSuperClasses = new HashSet();
                otherReferencedClass.hierarchyAccept(false, true, true, false,
                                                     new ClassCollector(otherSuperClasses));

                if (DEBUG)
                {
                    System.out.println("ReferenceValue.generalize this ["+thisReferencedClass.getName()+"] with other ["+otherReferencedClass.getName()+"]");
                    System.out.println("  This super classes:  "+thisSuperClasses);
                    System.out.println("  Other super classes: "+otherSuperClasses);
                }

                // Find the common superclasses.
                thisSuperClasses.retainAll(otherSuperClasses);

                if (DEBUG)
                {
                    System.out.println("  Common super classes: "+thisSuperClasses);
                }

                // Find a class that is a subclass of all common superclasses,
                // or that at least has the maximum number of common superclasses.
                Clazz commonClazz = null;

                int maximumSuperClassCount = -1;

                // Go over all common superclasses to find it. In case of
                // multiple subclasses, keep the lowest one alphabetically,
                // in order to ensure that the choice is deterministic.
                Iterator commonSuperClasses = thisSuperClasses.iterator();
                while (commonSuperClasses.hasNext())
                {
                    Clazz commonSuperClass = (Clazz)commonSuperClasses.next();

                    int superClassCount = superClassCount(commonSuperClass, thisSuperClasses);
                    if (maximumSuperClassCount < superClassCount ||
                        (maximumSuperClassCount == superClassCount &&
                         commonClazz != null                       &&
                         commonClazz.getName().compareTo(commonSuperClass.getName()) > 0))
                    {
                        commonClazz            = commonSuperClass;
                        maximumSuperClassCount = superClassCount;
                    }
                }

                if (commonClazz == null)
                {
                    throw new IllegalArgumentException("Can't find common super class of ["+thisType+"] and ["+otherType+"]");
                }

                if (DEBUG)
                {
                    System.out.println("  Best common class: ["+commonClazz.getName()+"]");
                }

                // TODO: Handle more difficult cases, with multiple global subclasses.

                return new ReferenceValue(commonDimensionCount == 0 ?
                                              commonClazz.getName() :
                                              ClassUtil.internalArrayTypeFromClassName(commonClazz.getName(),
                                                                                       commonDimensionCount),
                                          commonClazz,
                                          mayBeNull);
            }
        }
        else if (thisDimensionCount > otherDimensionCount)
        {
            // See if the other type is an interface type of arrays.
            if (ClassUtil.isInternalArrayInterfaceName(ClassUtil.internalClassNameFromClassType(otherType)))
            {
                return other.generalizeMayBeNull(mayBeNull);
            }
        }
        else if (thisDimensionCount < otherDimensionCount)
        {
            // See if this type is an interface type of arrays.
            if (ClassUtil.isInternalArrayInterfaceName(ClassUtil.internalClassNameFromClassType(thisType)))
            {
                return this.generalizeMayBeNull(mayBeNull);
            }
        }

        // Reduce the common dimension count if either type is an array of
        // primitives type of this dimension.
        if (commonDimensionCount > 0 &&
            (ClassUtil.isInternalPrimitiveType(otherType.charAt(commonDimensionCount))) ||
             ClassUtil.isInternalPrimitiveType(thisType.charAt(commonDimensionCount)))
        {
            commonDimensionCount--;
        }

        // Fall back on a basic Object or array of Objects type.
        return commonDimensionCount == 0 ?
            mayBeNull ?
                ValueFactory.REFERENCE_VALUE_JAVA_LANG_OBJECT_MAYBE_NULL :
                ValueFactory.REFERENCE_VALUE_JAVA_LANG_OBJECT_NOT_NULL   :
            new ReferenceValue(ClassUtil.internalArrayTypeFromClassName(ClassConstants.INTERNAL_NAME_JAVA_LANG_OBJECT,
                                                                        commonDimensionCount),
                               null,
                               mayBeNull);
    }


    /**
     * Returns if the number of superclasses of the given class in the given
     * set of classes.
     */
    private int superClassCount(Clazz subClass, Set classes)
    {
        int count = 0;

        Iterator iterator = classes.iterator();

        while (iterator.hasNext())
        {
            Clazz clazz = (Clazz)iterator.next();
            if (subClass.extendsOrImplements(clazz))
            {
                count++;
            }
        }

        //System.out.println("ReferenceValue.superClassCount: ["+subClass.getName()+"]: "+count);

        return count;
    }


    /**
     * Returns whether this ReferenceValue is equal to the given other
     * ReferenceValue.
     * @return <code>NEVER</code>, <code>MAYBE</code>, or <code>ALWAYS</code>.
     */
    public int equal(ReferenceValue other)
    {
        return this.type  == null && other.type == null ? ALWAYS : MAYBE;
    }


    // Derived unary methods.

    /**
     * Returns whether this ReferenceValue is not <code>null</code>.
     * @return <code>NEVER</code>, <code>MAYBE</code>, or <code>ALWAYS</code>.
     */
    public final int isNotNull()
    {
        return -isNull();
    }


    /**
     * Returns the generalization of this ReferenceValue and the given other
     * ReferenceValue.
     */
    private ReferenceValue generalizeMayBeNull(boolean mayBeNull)
    {
        return this.mayBeNull || !mayBeNull ?
            this :
            new ReferenceValue(this.type, this.referencedClass, true);
    }


    // Derived binary methods.

    /**
     * Returns whether this ReferenceValue and the given ReferenceValue are different.
     * @return <code>NEVER</code>, <code>MAYBE</code>, or <code>ALWAYS</code>.
     */
    public final int notEqual(ReferenceValue other)
    {
        return -equal(other);
    }


    // Implementations for Value.

    public final ReferenceValue referenceValue()
    {
        return this;
    }

    public final Value generalize(Value other)
    {
        return this.generalize(other.referenceValue());
    }

    public boolean isSpecific()
    {
        return type == null;
    }

    public final int computationalType()
    {
        return TYPE_REFERENCE;
    }

    public final String internalType()
    {
        return
            type == null                        ? ClassConstants.INTERNAL_TYPE_JAVA_LANG_OBJECT :
            ClassUtil.isInternalArrayType(type) ? type                                          :
                                                  ClassConstants.INTERNAL_TYPE_CLASS_START +
                                                  type +
                                                  ClassConstants.INTERNAL_TYPE_CLASS_END;
    }


    // Implementations for Object.

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

        ReferenceValue other = (ReferenceValue)object;
        return this.type == null ? other.type == null :
                                   (this.mayBeNull == other.mayBeNull &&
                                    this.type.equals(other.type));
    }


    public int hashCode()
    {
        return this.getClass().hashCode() ^
               (type == null ? 0 : type.hashCode() ^ (mayBeNull ? 0 : 1));
    }


    public String toString()
    {
        return "a:" + (type == null ?
            "null" :
            type + (referencedClass == null ? "?" : "") + (mayBeNull ? "" : "!"));
    }
}
