/** 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++;
      }
    }

    return count;
  }
  /**
   * Returns the most specific common superclass or interface of the given classes.
   *
   * @param class1 the first class.
   * @param class2 the second class.
   * @param interfaces specifies whether to look for a superclass or for an interface.
   * @return the common class.
   */
  private Clazz findCommonClass(Clazz class1, Clazz class2, boolean interfaces) {
    // Collect the superclasses or the interfaces of this class.
    Set superClasses1 = new HashSet();
    class1.hierarchyAccept(
        !interfaces, !interfaces, interfaces, false, new ClassCollector(superClasses1));

    int superClasses1Count = superClasses1.size();
    if (superClasses1Count == 0) {
      if (interfaces) {
        return null;
      } else if (class1.getSuperName() != null) {
        throw new IllegalArgumentException(
            "Can't find any super classes of ["
                + class1.getName()
                + "] (not even immediate super class ["
                + class1.getSuperName()
                + "])");
      }
    }

    // Collect the superclasses or the interfaces of the other class.
    Set superClasses2 = new HashSet();
    class2.hierarchyAccept(
        !interfaces, !interfaces, interfaces, false, new ClassCollector(superClasses2));

    int superClasses2Count = superClasses2.size();
    if (superClasses2Count == 0) {
      if (interfaces) {
        return null;
      } else if (class2.getSuperName() != null) {
        throw new IllegalArgumentException(
            "Can't find any super classes of ["
                + class2.getName()
                + "] (not even immediate super class ["
                + class2.getSuperName()
                + "])");
      }
    }

    if (DEBUG) {
      System.out.println(
          "ReferenceValue.generalize this ["
              + class1.getName()
              + "] with other ["
              + class2.getName()
              + "] (interfaces = "
              + interfaces
              + ")");
      System.out.println("  This super classes:  " + superClasses1);
      System.out.println("  Other super classes: " + superClasses2);
    }

    // Find the common superclasses.
    superClasses1.retainAll(superClasses2);

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

    if (interfaces && superClasses1.isEmpty()) {
      return null;
    }

    // Find a class that is a subclass of all common superclasses,
    // or that at least has the maximum number of common superclasses.
    Clazz commonClass = 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 = superClasses1.iterator();
    while (commonSuperClasses.hasNext()) {
      Clazz commonSuperClass = (Clazz) commonSuperClasses.next();

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

    if (commonClass == null) {
      throw new IllegalArgumentException(
          "Can't find common super class of ["
              + class1.getName()
              + "] (with "
              + superClasses1Count
              + " known super classes) and ["
              + class2.getName()
              + "] (with "
              + superClasses2Count
              + " known super classes)");
    }

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

    return commonClass;
  }
  /** Returns the generalization of this ReferenceValue and the given other ReferenceValue. */
  public ReferenceValue generalize(ReferenceValue other) {
    // If both types are identical, the generalization is the same too.
    if (this.equals(other)) {
      return this;
    }

    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));

        int thisSuperClassesCount = thisSuperClasses.size();
        if (thisSuperClassesCount == 0 && thisReferencedClass.getSuperName() != null) {
          throw new IllegalArgumentException(
              "Can't find any super classes of ["
                  + thisType
                  + "] (not even immediate super class ["
                  + thisReferencedClass.getSuperName()
                  + "])");
        }

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

        int otherSuperClassesCount = otherSuperClasses.size();
        if (otherSuperClassesCount == 0 && otherReferencedClass.getSuperName() != null) {
          throw new IllegalArgumentException(
              "Can't find any super classes of ["
                  + otherType
                  + "] (not even immediate super class ["
                  + otherReferencedClass.getSuperName()
                  + "])");
        }

        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 commonClass = 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
                  && commonClass != null
                  && commonClass.getName().compareTo(commonSuperClass.getName()) > 0)) {
            commonClass = commonSuperClass;
            maximumSuperClassCount = superClassCount;
          }
        }

        if (commonClass == null) {
          throw new IllegalArgumentException(
              "Can't find common super class of ["
                  + thisType
                  + "] (with "
                  + thisSuperClassesCount
                  + " known super classes) and ["
                  + otherType
                  + "] (with "
                  + otherSuperClassesCount
                  + " known super classes)");
        }

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

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

        return new ReferenceValue(
            commonDimensionCount == 0
                ? commonClass.getName()
                : ClassUtil.internalArrayTypeFromClassName(
                    commonClass.getName(), commonDimensionCount),
            commonClass,
            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);
  }