/*
 * Copyright 2010 Google Inc.
 *
 * 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.google.gwt.dev.jjs.impl;

import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.jjs.SourceInfo;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.JArrayType;
import com.google.gwt.dev.jjs.ast.JCastOperation;
import com.google.gwt.dev.jjs.ast.JClassLiteral;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JDeclarationStatement;
import com.google.gwt.dev.jjs.ast.JEnumField;
import com.google.gwt.dev.jjs.ast.JEnumType;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JFieldRef;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodCall;
import com.google.gwt.dev.jjs.ast.JNewArray;
import com.google.gwt.dev.jjs.ast.JNode;
import com.google.gwt.dev.jjs.ast.JParameter;
import com.google.gwt.dev.jjs.ast.JPrimitiveType;
import com.google.gwt.dev.jjs.ast.JProgram;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.RuntimeConstants;
import com.google.gwt.dev.jjs.ast.js.JsniClassLiteral;
import com.google.gwt.dev.jjs.ast.js.JsniFieldRef;
import com.google.gwt.dev.jjs.ast.js.JsniMethodRef;
import com.google.gwt.thirdparty.guava.common.collect.Lists;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Ordering;
import com.google.gwt.thirdparty.guava.common.collect.Sets;
import com.google.gwt.thirdparty.guava.common.collect.TreeMultimap;

import java.util.Comparator;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;

/**
 * This optimizer replaces enum constants with their ordinal value (a simple
 * int) when possible. We call this process "ordinalization".
 *
 * Generally, this can be done for enums that are only ever referred to by
 * reference, or by their ordinal value. For the specific set of conditions
 * under which ordinalization can proceed, see the notes for the nested class
 * {@link EnumOrdinalizer.CannotBeOrdinalAnalyzer} below.
 *
 * This optimizer modifies enum classes to change their field constants to ints,
 * and to remove initialization of those constants in the clinit method. An
 * ordinalized enum class will not be removed from the AST by this optimizer,
 * but as long as all references to it are replaced, then the enum class itself
 * will be pruned by subsequent optimizer passes. Some enum classes may not be
 * completely removed however. Ordinalization can proceed in cases where there
 * are added static fields or methods in the enum class. In such cases a reduced
 * version of the original enum class can remain in the AST, containing only
 * static fields and methods which aren't part of the enum infrastructure (in
 * which case it will no longer behave as an enum class at all).
 *
 * Regardless of whether an ordinalized enum class ends up being completely
 * pruned away, the AST is expected to be in a coherent and usable state after
 * any pass of this optimizer. Thus, this optimizer should be considered to be
 * stateless.
 *
 * The process is broken up into 2 separate passes over the AST, each
 * implemented as a separate visitor class. The first visitor,
 * {@link EnumOrdinalizer.CannotBeOrdinalAnalyzer} compiles information about
 * each enum class in the AST, and looks for reasons not to ordinalize each
 * enum. Thus, it prepares a "black-list" of enum classes that cannot be
 * ordinalized (and it follows that all enums that don't get entered in the
 * black-list, will be allowed to be ordinalized. The set of conditions which
 * cause an enum to be black-listed are outlined below.
 *
 * If there are enum classes that didn't get black-listed remaining, the
 * subsequent passes of the optimizer will be invoked. The first,
 * {@link ReplaceOrdinalizedEnumTypes}, replaces the type info
 * for each field or expression involved with a target enum constant with an
 * integer. Also replaces access to the field {@code ordinal} and to the method
 * {@code ordinal} with the appropriate ordinal value.
 */
public class EnumOrdinalizer {

  /**
   * A simple Tracker class for compiling lists of enum classes processed by
   * this optimizer. If enabled, the results can be logged as debug output, and
   * the results can be tested after running with a given input.
   */
  public static class Tracker {
    private static final Comparator<SourceInfo> SOURCE_INFO_COMPARATOR =
        new Comparator<SourceInfo>() {
          @Override
          public int compare(SourceInfo s1, SourceInfo s2) {
            int fileNameComp = s1.getFileName().compareTo(s2.getFileName());
            return fileNameComp != 0 ? fileNameComp : s1.getStartLine()
                - s2.getStartLine();
          }
        };

    private final Set<String> allEnumsOrdinalized = Sets.newTreeSet();
    private final Set<String> allEnumsVisited = Sets.newTreeSet();
    private final Multimap<String, SourceInfo> enumInfoMap =
        TreeMultimap.create(Ordering.natural(), SOURCE_INFO_COMPARATOR);
    private final List<Set<String>> enumsOrdinalizedPerPass = Lists.newArrayList();
    private final List<Set<String>> enumsVisitedPerPass = Lists.newArrayList();
    private int runCount = -1;

    // use TreeSets, for nice sorted iteration for output
    public Tracker() {
      // add entry for initial pass
      enumsVisitedPerPass.add(new TreeSet<String>());
      enumsOrdinalizedPerPass.add(new TreeSet<String>());
    }

    public void addEnumNotOrdinalizedInfo(String enumName, SourceInfo info) {
      enumInfoMap.put(enumName, info);
    }

    public void addOrdinalized(String ordinalized) {
      enumsOrdinalizedPerPass.get(runCount).add(ordinalized);
      allEnumsOrdinalized.add(ordinalized);
    }

    public void addVisited(String visited) {
      enumsVisitedPerPass.get(runCount).add(visited);
      allEnumsVisited.add(visited);
    }

    public String getInfoString(SourceInfo info) {
      if (info == null) {
        return null;
      }
      return info.getFileName() + ": Line " + info.getStartLine();
    }

    public Set<String> getOrdinalizedNames() {
      return allEnumsOrdinalized;
    }

    public int getNumOrdinalized() {
      return allEnumsOrdinalized.size();
    }

    public int getNumVisited() {
      return allEnumsVisited.size();
    }

    public void incrementRunCount() {
      runCount++;
      enumsVisitedPerPass.add(new TreeSet<String>());
      enumsOrdinalizedPerPass.add(new TreeSet<String>());
    }

    public boolean isOrdinalized(String className) {
      return allEnumsOrdinalized.contains(className);
    }

    public boolean isVisited(String className) {
      return allEnumsVisited.contains(className);
    }

    public void logEnumsNotOrdinalized(TreeLogger logger, TreeLogger.Type logType) {
      if (logger == null) {
        return;
      }

      boolean initialMessageLogged = false;
      for (String enumVisited : allEnumsVisited) {
        if (isOrdinalized(enumVisited)) {
          continue;
        }
        if (!initialMessageLogged) {
          logger = logger.branch(logType, "Enums Not Ordinalized:");
          initialMessageLogged = true;
        }
        TreeLogger subLogger = logger.branch(logType, enumVisited);

        for (SourceInfo info : enumInfoMap.get(enumVisited)) {
          subLogger.branch(logType, getInfoString(info));
        }
      }
    }

    public void logEnumsOrdinalizedPerPass(TreeLogger logger, TreeLogger.Type logType) {
      if (logger == null) {
        return;
      }
      if (allEnumsOrdinalized.size() == 0) {
        return;
      }
      int pass = 0;
      for (Set<String> enumsOrdinalized : enumsOrdinalizedPerPass) {
        pass++;
        if (enumsOrdinalized.size() > 0) {
          TreeLogger subLogger = logger.branch(logType, "Pass " + pass + ": " +
              enumsOrdinalized.size() + " ordinalized");
          for (String enumOrdinalized : enumsOrdinalized) {
            subLogger.branch(logType, enumOrdinalized);
          }
        }
      }
    }

    public void logResultsDetailed(TreeLogger logger, TreeLogger.Type logType) {
      logger = logResultsSummary(logger, logType);
      logEnumsOrdinalizedPerPass(logger, logType);
      logEnumsNotOrdinalized(logger, logType);
    }

    public TreeLogger logResultsSummary(TreeLogger logger, TreeLogger.Type logType) {
      if (logger == null) {
        return null;
      }
      logger = logger.branch(logType, "EnumOrdinalizer Results:");
      logger.branch(logType, (runCount + 1) + " ordinalization passes completed");
      logger.branch(logType, allEnumsOrdinalized.size() + " of " + allEnumsVisited.size()
          + " ordinalized");
      return logger;
    }

    public void maybeDumpAST(JProgram program, int stage) {
      AstDumper.maybeDumpAST(program, NAME + "_" + (runCount + 1) + "_" + stage);
    }
  }

  /**
   * A visitor which keeps track of the enums which cannot be ordinalized. It
   * does this by keeping track of a "black-list" for ordinals which violate the
   * conditions for ordinalization, below.
   *
   * An enum cannot be ordinalized, if it:
   * <ul>
   * <li>is implicitly upcast.</li>
   * <li>is implicitly cast to from a nullType.</li>
   * <li>is implicitly cast to or from a javaScriptObject type.</li>
   * <li>is explicitly cast to another type (or vice-versa).</li>
   * <li>is tested in an instanceof expression.</li>
   * <li>it's class literal is used explicitly.</li>
   * <li>has any field referenced, except for:</li>
   * <ul>
   * <li>static fields.</li>
   * <li>Enum.ordinal.</li>
   * </ul>
   * <li>has any method called, except for:</li>
   * <ul>
   * <li>ordinal().</li>
   * <li>Enum.ordinal().</li>
   * <li>Enum() super constructor.</li>
   * <li>Enum.createValueOfMap().</li>
   * <li>static methods other than valueOf().</li>
   * </ul>
   * </ul>
   *
   * This visitor extends the ImplicitUpcastAnalyzer, which encapsulates all the
   * conditions where implicit upcasting can occur in an AST. The rest of the
   * logic for checking ordinalizability is encapsulated in this sub-class.
   *
   * It also keeps track of all enums encountered, so we can know if we need to
   * continue with the other visitors of the optimizer after this visitor runs.
   *
   * We make special allowances not to check any code statements that appear
   * within the ClassLiteralHolder class, which can contain a reference to all
   * enum class literals in the program, even after ordinalization occurs.
   *
   * Also, we ignore visiting the getClass() method for any enum subclass, since
   * it will also cause a visit to the enum's class literal, and we don't
   * necessarily want to prevent ordinalization in that case.
   *
   * Special checking is needed to detect a class literal reference that occurs
   * within a JSNI method body. We don't get a visit to JClassLiteral in that
   * case, so we need to inspect visits to JsniFieldRef for the possibility it
   * might be a reference to a class literal.
   *
   * We also skip any checking in a method call to Enum.createValueOfMap(),
   * since this is generated for any enum class initially within the extra
   * enumClass$Map class, and this call contains an implicit upcast in the
   * method call args (as well as a reference to the static enumClass$VALUES
   * field), which we want to ignore. The enumClass$Map class will not get
   * pruned as long as the enumClass is not ordinalized, and so we need to
   * ignore it's presence in the consideration for whether an enum class is
   * ordinalizable.
   */
  private class CannotBeOrdinalAnalyzer extends ImplicitUpcastAnalyzer {
    public CannotBeOrdinalAnalyzer(JProgram program) {
      super(program);
    }

    @Override
    public void endVisit(JCastOperation x, Context ctx) {
      // check for explicit cast (check both directions)
      blackListIfEnumCast(x.getExpr().getType(), x.getCastType(), x.getSourceInfo());
      blackListIfEnumCast(x.getCastType(), x.getExpr().getType(), x.getSourceInfo());
    }

    @Override
    public void endVisit(JClassLiteral x, Context ctx) {
      /*
       * Check for references to an enum's class literal. We need to black-list
       * classes in this case, since there could be a call to
       * Enum.valueOf(someEnum.class,"name"), etc.
       *
       * Note: we won't get here for class literals that occur in the
       * ClassLiteralHolder class, or within the getClass method of an enum
       * class (see comments above).
       */
      blackListIfEnum(x.getRefType(), x.getSourceInfo());
    }

    @Override
    public void endVisit(JClassType x, Context ctx) {
      // keep track of all enum classes visited
      JEnumType enumClass = x.isEnumOrSubclass();
      if (enumClass == null) {
        return;
      }

      enumsVisited.add(enumClass);

      // don't need to re-ordinalize a previously ordinalized enum
      if (enumClass.isOrdinalized()
          || enumClass.canBeReferencedExternally()) {
        addToBlackList(enumClass, x.getSourceInfo());
      }
    }

    @Override
    public void endVisit(JFieldRef x, Context ctx) {
      // don't need to check Enum.ordinal
      if (x.getField() == enumOrdinalField) {
        return;
      }

      if (x.getInstance() != null) {
        // check any instance field reference other than ordinal
        blackListIfEnumExpression(x.getInstance());
      }
    }

    @Override
    public void endVisit(JInstanceOf x, Context ctx) {
      // If any instanceof tests haven't been optimized out, black list.
      blackListIfEnum(x.getExpr().getType(), x.getSourceInfo());
      // TODO (jbrosenberg): Investigate further whether ordinalization can be
      // allowed in this case.
      blackListIfEnum(x.getTestType(), x.getSourceInfo());
    }

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      // exempt calls to certain methods on the Enum super class
      if (x.getTarget() == enumCreateValueOfMapMethod || x.getTarget() == enumSuperConstructor
          || x.getTarget() == enumOrdinalMethod) {
        return;
      }

      // any other method on an enum class should cause it to be black-listed
      if (x.getInstance() != null) {
        blackListIfEnumExpression(x.getInstance());
      } else if (x.getTarget().isStatic()) {
        // black-list static method calls on an enum class only for valueOf()
        // and values()
        JMethod target = x.getTarget();
        maybeBlackListDueToStaticCall(x.getSourceInfo(), target);
      }

      if (x.getTarget().isJsniMethod()) {
        // Black list enum types declared in parameters of native functions.
        for (JParameter parameter :x.getTarget().getParams()) {
          blackListIfEnum(parameter.getType(), x.getSourceInfo());
        }
      }

      // defer to ImplicitUpcastAnalyzer to check method call args & params
      super.endVisit(x, ctx);
    }

    @Override
    public void endVisit(JsniClassLiteral x, Context ctx) {
      /*
       * Check for references to an enum's class literal. We need to black-list
       * classes in this case, since there could be a call to
       * Enum.valueOf(someEnum.class,"name"), etc.
       *
       * Note: we won't get here for class literals that occur in the
       * ClassLiteralHolder class, or within the getClass method of an enum
       * class (see comments above).
       */
      blackListIfEnum(x.getRefType(), x.getSourceInfo());
    }

    @Override
    public void endVisit(JsniFieldRef x, Context ctx) {
      /*
       * Can't do the same thing as for JFieldRef, all JsniFieldRefs are cast to
       * JavaScriptObjects. Need to check both the field type and the type of
       * the instance or enclosing class referencing the field.
       */

      // check the field type
      blackListIfEnum(x.getField().getType(), x.getSourceInfo());

      // check the referrer
      if (x.getInstance() != null) {
        blackListIfEnumExpression(x.getInstance());
      } else {
        blackListIfEnum(x.getField().getEnclosingType(), x.getSourceInfo());
      }
    }

    @Override
    public void endVisit(JsniMethodRef x, Context ctx) {
      // no enum methods are exempted if occur within a JsniMethodRef
      if (x.getInstance() != null) {
        blackListIfEnumExpression(x.getInstance());
      } else if (x.getTarget().isStatic()) {
        maybeBlackListDueToStaticCall(x.getSourceInfo(), x.getTarget());
      }

      // Black list enums returned to JSNI.
      blackListIfEnum(x.getTarget().getType(), x.getSourceInfo());

      // defer to ImplicitUpcastAnalyzer to check method call args & params
      super.endVisit(x, ctx);
    }

    @Override
    public boolean visit(JNewArray x, Context ctx) {
      // Do not visit the implicit Class literal.

      if (x.getDimensionExpressions() != null) {
        accept(x.getDimensionExpressions());
      }

      if (x.getInitializers() != null) {
        accept(x.getInitializers());
      }
      return false;
    }

    @Override
    public boolean visit(JClassType x, Context ctx) {
      /*
       * Don't want to visit the large ClassLiteralHolder class, it doesn't
       * contain references to actual usage of enum class literals. It's also a
       * time savings to not traverse this class.
       */
      if (x == classLiteralHolderType) {
        return false;
      }
      return true;
    }

    @Override
    public boolean visit(JMethod x, Context ctx) {
      /*
       * Don't want to visit the generated getClass() method on an enum, since
       * it contains a reference to an enum's class literal that we don't want
       * to consider. Make sure this is not a user overloaded version (i.e.
       * check that it has no params).
       */
      if (x.getEnclosingType().isEnumOrSubclass() != null && x.getName().equals("getClass")
          && (x.getOriginalParamTypes() == null || x.getOriginalParamTypes().size() == 0)) {
        return false;
      }

      // defer to parent method on ImplicitCastAnalyzer
      return super.visit(x, ctx);
    }

    @Override
    public boolean visit(JMethodCall x, Context ctx) {
      /*
       * Skip all calls to Enum.createValueOfMap, since they'd get falsely
       * flagged for implicitly upcasting an array of an enum class, in the
       * arg passing. This method is only used by the enumClass$Map class to
       * support Enum.valueOf(Class,String), which if reachable already prevents
       * the enum from being ordinalized. Once ordinalization proceeds,
       * this $Map class should be pruned.
       */
      if (x.getTarget() == enumCreateValueOfMapMethod) {
        return false;
      }
      // ok to visit
      return true;
    }

    /*
     * Override for the method called from ImplicitUpcastAnalyzer, which will be
     * called for any implicit upcast.
     */
    @Override
    protected void processImplicitUpcast(JType fromType, JType destType, SourceInfo info) {
      if (fromType.isNullType()) {
        // handle case where a nullType is cast to an enum
        blackListIfEnum(destType, info);
      } else if (fromType == javaScriptObjectType) {
        // handle case where a javaScriptObject is cast to an enum
        blackListIfEnum(destType, info);
      } else {
        blackListIfEnumCast(fromType, destType, info);
      }
    }

    private void addToBlackList(JEnumType enumType, SourceInfo info) {
      ordinalizationBlackList.add(enumType);

      if (tracker != null) {
        tracker.addEnumNotOrdinalizedInfo(enumType.getName(), info);
      }
    }

    private void blackListIfEnum(JType maybeEnum, SourceInfo info) {
      JEnumType actualEnum = maybeEnum.isEnumOrSubclass();
      if (actualEnum != null) {
        addToBlackList(actualEnum, info);
      }
    }

    private void blackListIfEnumCast(JType maybeEnum, JType destType, SourceInfo info) {
      JEnumType actualEnum = maybeEnum.isEnumOrSubclass();
      JEnumType actualDestType = destType.isEnumOrSubclass();
      if (actualEnum != null) {
        if (actualDestType != actualEnum) {
          addToBlackList(actualEnum, info);
        }
        return;
      }

      // check JArrayTypes of enums
      actualEnum = getEnumTypeFromArrayLeafType(maybeEnum);
      actualDestType = getEnumTypeFromArrayLeafType(destType);
      if (actualEnum != null) {
        if (actualDestType != actualEnum) {
          addToBlackList(actualEnum, info);
        }
      }
    }

    private void blackListIfEnumExpression(JExpression instance) {
      if (instance != null) {
        blackListIfEnum(instance.getType(), instance.getSourceInfo());
      }
    }

    /**
     * Blacklist the enum if there is a call to  MyEnum.valueOf().
     */
    private void maybeBlackListDueToStaticCall(SourceInfo info, JMethod target) {
      if (target.getEnclosingType().isEnumOrSubclass() != null &&
          (target.getName().equals("valueOf"))) {
        blackListIfEnum(target.getEnclosingType(), info);
      }
    }
  }

  /**
   * A visitor which replaces enum types with an integer.
   *
   * It sub-classes TypeRemapper, which encapsulates all the locations for a
   * settable type. The overridden remap() method will be called in each
   * instance, and it will test whether the type is a candidate for replacement,
   * and if so, return the new type to set (JPrimitiveType.INT).
   * <p>
   * Any reference to an enum field constant in an expression is replaced with
   * integer.
   * <p>
   * This will also explicitly replace an enum's field constants with its
   * ordinal int values; also replaces access to the ordinal field and class to
   * the ordinal() method with the corresponding int value.
   */
  private class ReplaceOrdinalizedEnumTypes extends TypeRemapper {
    public ReplaceOrdinalizedEnumTypes(OptimizerContext optimizerCtx) {
      super(optimizerCtx);
    }

    @Override
    public boolean visit(JClassType x, Context ctx) {
      if (canBeOrdinal(x)) {
        /*
         * Remove any static impl mappings for any methods in an ordinal enum
         * class. An ordinalized enum will no longer have an instance passed as
         * the first argument for a static impl (it will just be an int). This
         * is needed to preserve proper assumptions about static impls by other
         * optimizers (e.g. we might need to insert a clinit, when it wouldn't
         * be needed if a method call still had a static impl target).
         */
        for (JMethod method : x.getMethods()) {
          program.removeStaticImplMapping(method);
        }
      }
      return true;
    }

    @Override
    public void endVisit(JFieldRef x, Context ctx) {
      super.endVisit(x, ctx);
      JField field = x.getField();
      if (field instanceof JEnumField && canBeOrdinal(field.getEnclosingType())
          && !ctx.isLvalue()) {
        int ordinal = ((JEnumField) field).ordinal();
        ctx.replaceMe(program.getLiteralInt(ordinal));
        return;
      }
      maybeReplaceOrdinalAccess(x.getInstance(), field, ctx);
    }

    @Override
    public void endVisit(JMethodCall x, Context ctx) {
      super.endVisit(x, ctx);
      maybeReplaceOrdinalAccess(x.getInstance(),x.getTarget(), ctx);
    }

    @Override
    public void endVisit(JDeclarationStatement x, Context ctx) {
      super.endVisit(x, ctx);
      if (!(x.getVariableRef().getTarget() instanceof JEnumField)) {
        return;
      }

      JEnumField enumField = (JEnumField) x.getVariableRef().getTarget();
      if (!(canBeOrdinal(enumField.getEnclosingType()))) {
        return;
      }

      // Also replace the declaration statement so that the enum instances are not constructed;
      // eventually the field will be pruned but the declaration statement will only completely
      // disappear if there are no side effects (constructing a new instance counts as side
      // effects).
      ctx.replaceMe(new JDeclarationStatement(x.getSourceInfo(), x.getVariableRef(),
          program.getLiteralInt(enumField.ordinal())));
    }

    /**
     * Remap enum types with JPrimitiveType.INT. Also handle case for arrays,
     * replace enum leaftype with JPrimitive.INT. This is an override
     * implementation called from TypeRemapper.
     */
    @Override
    protected JType remap(JType type) {
      JType remappedType = getOrdinalizedType(type);
      if (remappedType == null) {
        return type;
      }
      return remappedType;
    }

    private boolean canBeOrdinal(JType type) {
      JType uType = type.getUnderlyingType();
      return uType instanceof JEnumType && !ordinalizationBlackList.contains(uType);
    }

    private JType getOrdinalizedType(JType type) {
      if (canBeOrdinal(type)) {
        return JPrimitiveType.INT;
      }

      JType uType = type.getUnderlyingType();
      if (!(uType instanceof JArrayType)) {
        return null;
      }

      JArrayType aType = (JArrayType) uType;
      JType leafType = aType.getLeafType();
      if (canBeOrdinal(leafType)) {
        JArrayType newAType = program.getOrCreateArrayType(JPrimitiveType.INT, aType.getDims());
        return !type.canBeNull() ? newAType.strengthenToNonNull() : newAType;
      }

      return null;
    }

    private void maybeReplaceOrdinalAccess(JExpression instance, JNode member, Context ctx) {
      if (member  != enumOrdinalField && member != enumOrdinalMethod) {
        return;
      }

      JType instanceType = instance.getType();
      // Access to x.ordinal and x.ordinal() need to be replaced by x if the type was ordinalized.
      // Ordinalized types in this stage can also appear as primitive ints.
      if (instanceType == JPrimitiveType.INT || canBeOrdinal(instanceType)) {
        ctx.replaceMe(instance);
      }
    }
  }

  private static final String NAME = EnumOrdinalizer.class.getSimpleName();

  private static Tracker tracker = null;

  private static boolean trackerEnabled =
      (System.getProperty("gwt.enableEnumOrdinalizerTracking") != null);

  public static void enableTracker() {
    trackerEnabled = true;
  }

  public static int exec(JProgram program, OptimizerContext optimizerCtx) {
    startTracker();
    return new EnumOrdinalizer(program).execImpl(optimizerCtx);
  }

  public static Tracker getTracker() {
    return tracker;
  }

  public static void resetTracker() {
    if (tracker != null) {
      tracker = null;
      startTracker();
    }
  }

  private static void startTracker() {
    if (trackerEnabled && tracker == null) {
      tracker = new Tracker();
    }
  }

  private final JType classLiteralHolderType;
  private final JMethod enumCreateValueOfMapMethod;
  private final JField enumOrdinalField;
  private final JMethod enumOrdinalMethod;
  private final JMethod enumSuperConstructor;
  private final Set<JEnumType> enumsVisited = Sets.newHashSet();
  private final JType javaScriptObjectType;
  private final Set<JEnumType> ordinalizationBlackList = Sets.newHashSet();
  private final JProgram program;

  public EnumOrdinalizer(JProgram program) {
    this.program = program;
    this.classLiteralHolderType = program.getTypeClassLiteralHolder();
    this.javaScriptObjectType = program.getJavaScriptObject();
    this.enumOrdinalField = program.getIndexedField(RuntimeConstants.ENUM_ORDINAL);
    this.enumCreateValueOfMapMethod =
        program.getIndexedMethod(RuntimeConstants.ENUM_CREATE_VALUE_OF_MAP);
    this.enumOrdinalMethod = program.getIndexedMethod(RuntimeConstants.ENUM_ORDINAL);
    this.enumSuperConstructor = program.getIndexedMethod(RuntimeConstants.ENUM_ENUM);
  }

  private int execImpl(OptimizerContext optimizerCtx) {
    try (OptimizerStats stats = OptimizerStats.optimization(NAME)) {

      if (tracker != null) {
        tracker.incrementRunCount();
        tracker.maybeDumpAST(program, 0);
      }

      // Create black list of enum refs which can't be converted to an ordinal ref
      CannotBeOrdinalAnalyzer ordinalAnalyzer = new CannotBeOrdinalAnalyzer(program);
      ordinalAnalyzer.accept(program);

      // Bail if we don't need to do any ordinalization
      if (enumsVisited.size() == ordinalizationBlackList.size()) {
        // Update tracker stats
        if (tracker != null) {
          for (JEnumType type : enumsVisited) {
            tracker.addVisited(type.getName());
          }
        }
        return stats.getNumMods();
      }

      // Replace enum type refs
      ReplaceOrdinalizedEnumTypes replaceEnums = new ReplaceOrdinalizedEnumTypes(optimizerCtx);
      replaceEnums.accept(program);
      stats.recordModified(replaceEnums.getNumMods());

      if (tracker != null) {
        tracker.maybeDumpAST(program, 1);
      }

      // Update enums ordinalized, and tracker stats
      for (JEnumType type : enumsVisited) {
        if (tracker != null) {
          tracker.addVisited(type.getName());
        }
        if (!ordinalizationBlackList.contains(type)) {
          if (tracker != null) {
            tracker.addOrdinalized(type.getName());
          }
          type.setOrdinalized();
        }
      }
      JavaAstVerifier.assertProgramIsConsistent(program);
      optimizerCtx.incOptimizationStep();

      return stats.getNumMods();
    }
  }

  private JEnumType getEnumTypeFromArrayLeafType(JType type) {
    type = type.getUnderlyingType();
    if (type instanceof JArrayType) {
      type = ((JArrayType) type).getLeafType();
      return type.isEnumOrSubclass();
    }
    return null;
  }
}
