/*
 * Copyright 2015 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.core.ext.UnableToCompleteException;
import com.google.gwt.dev.MinimalRebuildCache;
import com.google.gwt.dev.javac.JsInteropUtil;
import com.google.gwt.dev.jjs.HasSourceInfo;
import com.google.gwt.dev.jjs.ast.CanBeJsNative;
import com.google.gwt.dev.jjs.ast.CanHaveSuppressedWarnings;
import com.google.gwt.dev.jjs.ast.Context;
import com.google.gwt.dev.jjs.ast.HasJsInfo.JsMemberType;
import com.google.gwt.dev.jjs.ast.HasJsName;
import com.google.gwt.dev.jjs.ast.HasType;
import com.google.gwt.dev.jjs.ast.JClassType;
import com.google.gwt.dev.jjs.ast.JConstructor;
import com.google.gwt.dev.jjs.ast.JDeclaredType;
import com.google.gwt.dev.jjs.ast.JDeclaredType.NestedClassDisposition;
import com.google.gwt.dev.jjs.ast.JExpression;
import com.google.gwt.dev.jjs.ast.JExpressionStatement;
import com.google.gwt.dev.jjs.ast.JField;
import com.google.gwt.dev.jjs.ast.JInstanceOf;
import com.google.gwt.dev.jjs.ast.JInterfaceType;
import com.google.gwt.dev.jjs.ast.JMember;
import com.google.gwt.dev.jjs.ast.JMethod;
import com.google.gwt.dev.jjs.ast.JMethodBody;
import com.google.gwt.dev.jjs.ast.JMethodCall;
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.JReferenceType;
import com.google.gwt.dev.jjs.ast.JStatement;
import com.google.gwt.dev.jjs.ast.JType;
import com.google.gwt.dev.jjs.ast.JVisitor;
import com.google.gwt.dev.jjs.ast.js.JsniMethodBody;
import com.google.gwt.dev.js.JsUtils;
import com.google.gwt.dev.js.ast.JsContext;
import com.google.gwt.dev.js.ast.JsFunction;
import com.google.gwt.dev.js.ast.JsNameRef;
import com.google.gwt.dev.js.ast.JsParameter;
import com.google.gwt.dev.js.ast.JsVisitor;
import com.google.gwt.thirdparty.guava.common.base.Preconditions;
import com.google.gwt.thirdparty.guava.common.base.Predicate;
import com.google.gwt.thirdparty.guava.common.collect.FluentIterable;
import com.google.gwt.thirdparty.guava.common.collect.Iterables;
import com.google.gwt.thirdparty.guava.common.collect.LinkedHashMultimap;
import com.google.gwt.thirdparty.guava.common.collect.Multimap;
import com.google.gwt.thirdparty.guava.common.collect.Sets;

import java.util.List;
import java.util.Set;

/**
 * Checks and throws errors for invalid JsInterop constructs.
 */
public class JsInteropRestrictionChecker extends AbstractRestrictionChecker {

  public static void exec(TreeLogger logger, JProgram jprogram,
      MinimalRebuildCache minimalRebuildCache) throws UnableToCompleteException {
    JsInteropRestrictionChecker jsInteropRestrictionChecker =
        new JsInteropRestrictionChecker(jprogram, minimalRebuildCache);
    boolean success = jsInteropRestrictionChecker.checkProgram(logger);
    if (!success) {
      throw new UnableToCompleteException();
    }
  }

  private final JProgram jprogram;
  private final MinimalRebuildCache minimalRebuildCache;
  private boolean wasUnusableByJsWarningReported = false;

  private JsInteropRestrictionChecker(JProgram jprogram,
      MinimalRebuildCache minimalRebuildCache) {
    this.jprogram = jprogram;
    this.minimalRebuildCache = minimalRebuildCache;
  }

  /**
   * Returns true if the constructor method is locally empty (allows calls to init and super
   * constructor).
   */
  private static boolean isConstructorEmpty(final JConstructor constructor) {
    return Iterables.all(constructor.getBody().getStatements(), new Predicate<JStatement>() {
      @Override
      public boolean apply(JStatement statement) {
        JClassType type = constructor.getEnclosingType();
        if (isImplicitSuperCall(statement, type.getSuperClass())) {
          return true;
        }
        if (isInitCall(statement, type)) {
          return true;
        }
        return false;
      }
    });
  }

  private static JMethodCall isMethodCall(JStatement statement) {
    if (!(statement instanceof JExpressionStatement)) {
      return null;
    }
    JExpression expression = ((JExpressionStatement) statement).getExpr();

    return expression instanceof JMethodCall ? (JMethodCall) expression : null;
  }

  private static boolean isInitCall(JStatement statement, JDeclaredType type) {
    JMethodCall methodCall = isMethodCall(statement);

    return methodCall != null
        && methodCall.getTarget() == type.getInitMethod();
  }

  private static boolean isImplicitSuperCall(JStatement statement, JDeclaredType superType) {
    JMethodCall methodCall = isMethodCall(statement);

    return methodCall != null
        && methodCall.isStaticDispatchOnly()
        && methodCall.getTarget().isConstructor()
        && methodCall.getTarget().getEnclosingType() == superType;
  }

  private static boolean isInitEmpty(JDeclaredType type) {
    return type.getInitMethod() == null
        || ((JMethodBody) type.getInitMethod().getBody()).getStatements().isEmpty();
  }

  private void checkJsConstructors(JDeclaredType type) {
    List<JConstructor> jsConstructors = getJsConstructors(type);

    if (type.isJsNative()) {
      return;
    }

    if (jsConstructors.isEmpty()) {
      return;
    }

    if (jsConstructors.size() > 1) {
      logError(type,
          "More than one JsConstructor exists for %s.", getDescription(type));
    }

    final JConstructor jsConstructor = jsConstructors.get(0);

    if (JjsUtils.getPrimaryConstructor(type) != jsConstructor) {
      logError(jsConstructor,
          "Constructor %s can be a JsConstructor only if all constructors in the class are "
          + "delegating to it.", getMemberDescription(jsConstructor));
    }
  }

  private List<JConstructor> getJsConstructors(JDeclaredType type) {
    return FluentIterable
          .from(type.getConstructors())
          .filter(new Predicate<JConstructor>() {
            @Override
            public boolean apply(JConstructor m) {
              return m.isJsConstructor();
            }
          }).toList();
  }

  private void checkJsConstructorSubtype(JDeclaredType type) {
    if (!isJsConstructorSubtype(type)) {
      return;
    }
    if (Iterables.isEmpty(type.getConstructors())) {
      // No constructors in the type; type is not instantiable.
      return;
    }

    if (type.isJsNative()) {
      return;
    }

    JClassType superClass = type.getSuperClass();
    JConstructor superPrimaryConsructor = JjsUtils.getPrimaryConstructor(superClass);
    if (!superClass.isJsNative() && superPrimaryConsructor == null) {
      // Superclass has JsConstructor but does not satisfy the JsConstructor restrictions, no need
      // to report more errors.
      return;
    }

    JConstructor primaryConstructor = JjsUtils.getPrimaryConstructor(type);
    if (primaryConstructor == null) {
      logError(type,
          "Class %s should have only one constructor delegating to the superclass since it is "
              + "subclass of a a type with JsConstructor.", getDescription(type));
      return;
    }

    JConstructor delegatedConstructor =
        JjsUtils.getDelegatedThisOrSuperConstructor(primaryConstructor);

    if (delegatedConstructor.isJsConstructor() ||
        delegatedConstructor == superPrimaryConsructor) {
      return;
    }

    logError(primaryConstructor,
        "Constructor %s can only delegate to super constructor %s since it is a subclass of a "
            + "type with JsConstructor.",
        getDescription(primaryConstructor),
        getDescription(superPrimaryConsructor));
  }

  private void checkMember(JMember member, Multimap<String, JMember> instanceMembersByJsName) {

    if (member.getEnclosingType().isJsNative()) {
      checkMemberOfNativeJsType(member);
    }

    if (member.needsDynamicDispatch()) {
      checkIllegalOverrides(member);
    }

    if (member instanceof JMethod) {
      checkMethodParameters((JMethod) member);
    }

    if (member.isJsOverlay()) {
      checkJsOverlay(member);
      return;
    }

    if (member.canBeReferencedExternally()) {
      checkUnusableByJs(member);
    }

    if (member.getJsMemberType() == JsMemberType.NONE) {
      return;
    }

    if (!checkJsPropertyAccessor(member)) {
      return;
    }

    checkMemberQualifiedJsName(member);

    if (isInstanceJsMember(member)) {
      checkInstanceNameConsistency(instanceMembersByJsName, member);
    }

    if (isStaticJsMember(member)) {
      checkStaticNameCollisions(member);
    }
  }

  private void checkIllegalOverrides(JMember member) {
    if (member instanceof JField) {
      return;
    }

    JMethod method = (JMethod) member;

    if (method.isSynthetic()) {
      // Ignore synthetic methods. These synthetic methods might be accidental overrides or
      // default method implementations, and they forward to the same implementation so it is
      // safe to allow them.
      return;
    }
    for (JMethod overriddenMethod : method.getOverriddenMethods()) {
      if (overriddenMethod.isSynthetic()) {
        // Ignore synthetic methods for a better error message.
        continue;
      }
      if (overriddenMethod.isJsOverlay()) {
        logError(member, "Method '%s' cannot override a JsOverlay method '%s'.",
            JjsUtils.getReadableDescription(method),
            JjsUtils.getReadableDescription(overriddenMethod));
        return;
      }
    }
  }

  private void checkJsOverlay(JMember member) {
    if (member.getEnclosingType().isJsoType() || member.isSynthetic()) {
      return;
    }

    String memberDescription = JjsUtils.getReadableDescription(member);

    if (!member.getEnclosingType().isJsNative() && !member.getEnclosingType().isJsFunction()) {
      logError(member,
          "JsOverlay '%s' can only be declared in a native type or a JsFunction interface.",
          memberDescription);
    }

    if (member instanceof JConstructor) {
      logError(member, "JsOverlay method '%s' cannot be a constructor.", memberDescription);
      return;
    }

    if (member.getJsMemberType() != JsMemberType.NONE) {
      logError(member, "JsOverlay method '%s' cannot be nor override a JsProperty or a JsMethod.",
          memberDescription);
      return;
    }

    if (member instanceof JField) {
      JField field = (JField) member;
      if (field.needsDynamicDispatch()) {
        logError(member, "JsOverlay field '%s' can only be static.", memberDescription);
      }
      return;
    }

    JMethod method = (JMethod) member;

    assert method.getOverriddenMethods().isEmpty();

    if (method.getBody() == null
        || (!method.isFinal()
            && !method.getEnclosingType().isFinal()
            && !method.isPrivate()
            && !method.isStatic()
            && !method.isDefaultMethod())) {
      logError(member, "JsOverlay method '%s' cannot be non-final nor native.", memberDescription);
    }
  }

  private void checkSuperDispachToNativeJavaLangObjectMethodOverride() {
    new JVisitor() {
      JClassType superClass;
      @Override
      public boolean visit(JDeclaredType x, Context ctx) {
        superClass = JjsUtils.getNativeSuperClassOrNull(x);
        // Only examine code in non native subclasses of native JsTypes.
        return x instanceof JClassType && superClass != null;
      }

      @Override
      public boolean visit(JMethod x, Context ctx) {
        // Do not report errors from synthetic method bodies, those errors are reported
        // explicitly elsewhere.
        return !x.isSynthetic();
      }

      @Override
      public void endVisit(JMethodCall x, Context ctx) {
        JMethod target = x.getTarget();
        if (!x.isStaticDispatchOnly()) {
          // Not a super call, allow.
          return;
        }

        assert (!target.isStatic());
        // Forbid calling through super when the target is the native implementation because
        // it might not exist in the native supertype at runtime.
        // TODO(rluble): lift this restriction by dispatching through a trampoline. Not that this
        // trampoline is different that the one created for non static dispatches.
        if ((overridesObjectMethod(target) && target.getEnclosingType().isJsNative())
            || target.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
          logError(x, "Cannot use super to call '%s.%s'. 'java.lang.Object' methods in native "
              + "JsTypes cannot be called using super.",
              JjsUtils.getReadableDescription(superClass),
              target.getName());
          return;
        }
      }
    }.accept(jprogram);
  }

  private void checkMemberOfNativeJsType(JMember member) {
    if (member instanceof JMethod && ((JMethod) member).isJsniMethod()) {
      logError(member, "JSNI method %s is not allowed in a native JsType.",
          getMemberDescription(member));
      return;
    }

    if (member.isSynthetic() || member.isJsOverlay()) {
      return;
    }

    if (overridesObjectMethod(member)) {
      if (member.getJsMemberType() != JsMemberType.METHOD
          || !member.getName().equals(member.getJsName())) {
        logError(member,
            "Method %s cannot override a method from 'java.lang.Object' and change its name.",
            getMemberDescription(member));
        return;
      }
    }

    JsMemberType jsMemberType = member.getJsMemberType();
    switch (jsMemberType) {
      case CONSTRUCTOR:
        if (!isConstructorEmpty((JConstructor) member)) {
          logError(member, "Native JsType constructor %s cannot have non-empty method body.",
              getMemberDescription(member));
        }
        break;
      case METHOD:
      case GETTER:
      case SETTER:
      case UNDEFINED_ACCESSOR:
        JMethod method = (JMethod) member;
        if (!method.isAbstract() && method.getBody() != null) {
          logError(member, "Native JsType method %s should be native or abstract.",
              getMemberDescription(member));
        }
        break;
      case PROPERTY:
        JField field = (JField) member;
        if (field.isFinal()) {
          logError(member, "Native JsType field %s cannot be final.",
              getMemberDescription(member));
        } else if (field.hasInitializer()) {
          logError(member, "Native JsType field %s cannot have initializer.",
              getMemberDescription(member));
        }
        break;
      case NONE:
        logError(member, "Native JsType member %s cannot have @JsIgnore.",
            getMemberDescription(member));
        break;
    }
  }

  private boolean overridesObjectMethod(JMember member) {
    if (!(member instanceof JMethod)) {
      return false;
    }

    JMethod method = (JMethod) member;
    for (JMethod overriddenMethod : method.getOverriddenMethods()) {
      if (overriddenMethod.getEnclosingType() == jprogram.getTypeJavaLangObject()) {
        return true;
      }
    }
    return false;
  }

  private void checkMethodParameters(JMethod method) {
    if (method.isSynthetic()) {
      return;
    }

    boolean hasOptionalParameters = false;
    for (JParameter parameter : method.getParams()) {
      if (parameter.isOptional()) {
        if (parameter.getType().isPrimitiveType()) {
          logError(method, "JsOptional parameter '%s' in method %s cannot be of primitive type.",
              parameter.getName(), getMemberDescription(method));
        }
        hasOptionalParameters = true;
        continue;
      }
      if (hasOptionalParameters && !parameter.isVarargs()) {
        logError(method, "JsOptional parameter '%s' in method %s cannot precede parameters "
            + "that are not optional.", parameter.getName(), getMemberDescription(method));
        break;
      }
    }

    if (hasOptionalParameters
        && method.getJsMemberType() != JsMemberType.CONSTRUCTOR
        && method.getJsMemberType() != JsMemberType.METHOD
        && !method.isOrOverridesJsFunctionMethod()) {
      logError(method, "Method %s has JsOptional parameters and is not a JsMethod, "
          + "a JsConstructor or a JsFunction method.", getMemberDescription(method));
    }

    if (method.isJsMethodVarargs()) {
      checkJsVarargs(method);
    }

    // Check that parameters that are declared JsOptional in overridden methods remain JsOptional.
    for (JMethod overriddenMethod : method.getOverriddenMethods()) {
      for (int i = 0; i < overriddenMethod.getParams().size(); i++) {
        if (overriddenMethod.getParams().get(i).isOptional()) {
          if (!method.getParams().get(i).isOptional()) {
            logError(method, "Method %s should declare parameter '%s' as JsOptional",
                getMemberDescription(method), method.getParams().get(i).getName());
            return;
          }
          break;
        }
      }
    }
  }

  private void checkJsVarargs(final JMethod method) {
    if (!method.isJsniMethod()) {
      return;
    }
    final JsFunction function = ((JsniMethodBody) method.getBody()).getFunc();
    final JsParameter varargParameter = Iterables.getLast(function.getParameters());
    new JsVisitor() {
      @Override
      public void endVisit(JsNameRef x, JsContext ctx) {
        if (x.getName() == varargParameter.getName()) {
          logError(x, "Cannot access vararg parameter '%s' from JSNI in JsMethod %s."
              + " Use 'arguments' instead.", x.getIdent(),
              getMemberDescription(method));
        }
      }
    }.accept(function);
  }

  private boolean checkJsPropertyAccessor(JMember member) {
    JsMemberType memberType = member.getJsMemberType();

    if (member.getJsName().equals(JsInteropUtil.INVALID_JSNAME)) {
      assert memberType.isPropertyAccessor();
      logError(
          member,
          "JsProperty %s should either follow Java Bean naming conventions or provide a name.",
          getMemberDescription(member));
      return false;
    }

    switch (memberType) {
      case UNDEFINED_ACCESSOR:
        logError(member, "JsProperty %s should have a correct setter or getter signature.",
            getMemberDescription(member));
        break;
      case GETTER:
        if (member.getType() != JPrimitiveType.BOOLEAN && member.getName().startsWith("is")) {
          logError(member, "JsProperty %s cannot have a non-boolean return.",
              getMemberDescription(member));
        }
        break;
      case SETTER:
        if (((JMethod) member).getParams().get(0).isVarargs()) {
          logError(member, "JsProperty %s cannot have a vararg parameter.",
              getMemberDescription(member));
        }
        break;
    }

    if (memberType.isPropertyAccessor() && member.isStatic() && !member.isJsNative()) {
        logError(member, "Static property accessor '%s' can only be native.",
            JjsUtils.getReadableDescription(member));
    }

    return true;
  }

  private void checkMemberQualifiedJsName(JMember member) {
    if (member instanceof JConstructor) {
      // Constructors always inherit their name and namespace from the enclosing type.
      // The corresponding checks are done for the type separately.
      return;
    }

    checkJsName(member);

    if (member.getJsNamespace().equals(member.getEnclosingType().getQualifiedJsName())) {
      // Namespace set by the enclosing type has already been checked.
      return;
    }

    if (member.needsDynamicDispatch()) {
      logError(member, "Instance member %s cannot declare a namespace.",
          getMemberDescription(member));
      return;
    }

    checkJsNamespace(member);
  }

  private <T extends HasJsName & HasSourceInfo & CanBeJsNative> void checkJsName(T item) {
    if (item.getJsName().isEmpty()) {
      logError(item, "%s cannot have an empty name.", getDescription(item));
    } else if ((item.isJsNative() && !JsUtils.isValidJsQualifiedName(item.getJsName()))
        || (!item.isJsNative() && !JsUtils.isValidJsIdentifier(item.getJsName()))) {
      // Allow qualified names in the name field for JsPackage.GLOBAL native items for future
      // compatibility
      logError(item, "%s has invalid name '%s'.", getDescription(item), item.getJsName());
    }
  }

  private <T extends HasJsName & HasSourceInfo & CanBeJsNative> void checkJsNamespace(T item) {
    if (JsInteropUtil.isGlobal(item.getJsNamespace())) {
      return;
    }
    if (JsInteropUtil.isWindow(item.getJsNamespace())) {
      if (item.isJsNative()) {
        return;
      }
      logError(item, "'%s' can only be used as a namespace of native types and members.",
          item.getJsNamespace());
    } else if (item.getJsNamespace().isEmpty()) {
      logError(item, "%s cannot have an empty namespace.", getDescription(item));
    } else if (!JsUtils.isValidJsQualifiedName(item.getJsNamespace())) {
      logError(item, "%s has invalid namespace '%s'.", getDescription(item), item.getJsNamespace());
    }
  }

  private void checkInstanceNameConsistency(
      Multimap<String, JMember> instanceMembersByJsName, JMember member) {
    checkOverrideConsistency(member);

    if (member.isJsNative()) {
      return;
    }

    String name = member.getJsName();

    Set<JMember> potentiallyCollidingMembers =
        Sets.newLinkedHashSet(instanceMembersByJsName.get(name));

    // Remove self.
    boolean removed = potentiallyCollidingMembers.remove(member);
    Preconditions.checkState(removed);

    // Remove native members as they don't cause collisions.
    Iterables.removeIf(potentiallyCollidingMembers,
        new Predicate<JMember>() {
          @Override
          public boolean apply(JMember member) {
            return member.isJsNative();
          }
        });

    if (potentiallyCollidingMembers.isEmpty()) {
      // No colliding members.
      return;
    }

    JMember potentiallyCollidingMember = potentiallyCollidingMembers.iterator().next();
    if (potentiallyCollidingMembers.size() == 1
         && isJsPropertyAccessorPair(member, potentiallyCollidingMember)) {
      if (!checkPropertyConsistency(member, potentiallyCollidingMember)) {
        // Remove colliding member to avoid duplicate error messages.
        instanceMembersByJsName.get(name).remove(member);
      }
      return;
    }

    logError(member,
        "%s and %s cannot both use the same JavaScript name '%s'.",
        getMemberDescription(member),
        getMemberDescription(potentiallyCollidingMember),
        member.getJsName());

    // Remove colliding member avoid duplicate error messages.
    instanceMembersByJsName.get(name).remove(member);
  }

  private boolean isJsPropertyAccessorPair(JMember thisMember, JMember thatMember) {
    return (thisMember.getJsMemberType() == JsMemberType.GETTER
            && thatMember.getJsMemberType() == JsMemberType.SETTER)
        || (thatMember.getJsMemberType() == JsMemberType.GETTER
            && thisMember.getJsMemberType() == JsMemberType.SETTER);
  }

  private void checkStaticNameCollisions(JMember member) {
    if (member.isJsNative()) {
      return;
    }
    // TODO(rluble): Add static property consistency check here if static property accessors are
    // ever allowed.
    String currentGlobalNameDescription =
        minimalRebuildCache.addExportedGlobalName(member.getQualifiedJsName(),
            JjsUtils.getReadableDescription(member), member.getEnclosingType().getName());

    if (currentGlobalNameDescription == null) {
      return;
    }
    logError(member, "%s cannot be exported because the global name '%s' is already taken by '%s'.",
        getMemberDescription(member), member.getQualifiedJsName(), currentGlobalNameDescription);
  }

  private boolean checkPropertyConsistency(JMember member, JMember otherMember) {
    JMember setter = member.getJsMemberType() == JsMemberType.SETTER ? member : otherMember;
    JMember getter = member.getJsMemberType() == JsMemberType.GETTER ? member : otherMember;
    if (setter != null && getter != null) {
      List<JParameter> setterParams = ((JMethod) setter).getParams();
      if (isSameType(getter.getType(), setterParams.get(0).getType())) {
        logError(member, "JsProperty setter %s and getter %s cannot have inconsistent types.",
            getMemberDescription(setter), getMemberDescription(getter));
        return false;
      }
    }
    return true;
  }

  /**
   * Returns true if {@code thisType} is the same type as {@code thatType}.
   */
  private boolean isSameType(JType thisType, JType thatType) {
    // NOTE: The comparison here is made by signature instead of reference equality because under
    // incremental compilation this types might be reference only and hence not unique.
    return !thisType.getJavahSignatureName().equals(thatType.getJavahSignatureName());
  }

  private void checkOverrideConsistency(JMember member) {
    if (member instanceof JMethod) {
      String jsName = member.getJsName();
      for (JMethod overridenMethod : ((JMethod) member).getOverriddenMethods()) {
        String parentName = overridenMethod.getJsName();
        if (parentName == null) {
          continue;
        }

        if (!parentName.equals(jsName)) {
          logError(
              member,
              "%s cannot be assigned a different JavaScript name than the method it overrides.",
              getMemberDescription(member));
          break;
        }

        if (overridenMethod.getJsMemberType() != member.getJsMemberType()) {
          // Overrides can not change JsMethod to JsProperty nor vice versa.
          logError(
              member,
              "%s %s cannot override %s %s.",
              member.getJsMemberType() == JsMemberType.METHOD ? "JsMethod" : "JsProperty",
              getMemberDescription(member),
              overridenMethod.getJsMemberType() == JsMemberType.METHOD ? "JsMethod" : "JsProperty",
              getMemberDescription(overridenMethod));
        }
      }
    }
  }

  private void checkStaticJsPropertyCalls() {
    new JVisitor() {
      @Override
      public boolean visit(JMethod x, Context ctx) {
        // Skip unnecessary synthetic override, as they will not be generated.
        return !JjsUtils.isJsMemberUnnecessaryAccidentalOverride(x);
      }

      @Override
      public void endVisit(JMethodCall x, Context ctx) {
        JMethod target = x.getTarget();
        if (x.isStaticDispatchOnly() && target.getJsMemberType().isPropertyAccessor()) {
          logError(x, "Cannot call property accessor %s via super.",
              getMemberDescription(target));
        }
      }
    }.accept(jprogram);
  }

  private void checkInstanceOfNativeJsTypesOrJsFunctionImplementations() {
    new JVisitor() {
      @Override
      public boolean visit(JInstanceOf x, Context ctx) {
        JReferenceType type = x.getTestType();
        if (type.isJsNative() && type instanceof JInterfaceType) {
          logError(x, "Cannot do instanceof against native JsType interface '%s'.",
              JjsUtils.getReadableDescription(type));
        } else if (type.isJsFunctionImplementation()) {
          logError(x, "Cannot do instanceof against JsFunction implementation '%s'.",
              JjsUtils.getReadableDescription(type));
        }
        return true;
      }
    }.accept(jprogram);
  }

  private boolean checkJsType(JDeclaredType type) {
    // Java (at least up to Java 8) does not allow to annotate anonymous classes or lambdas; if
    // it ever becomes possible we should emit an error.
    assert type.getClassDisposition() != NestedClassDisposition.ANONYMOUS
        && type.getClassDisposition() != NestedClassDisposition.LAMBDA;

    if  (type.getClassDisposition() == NestedClassDisposition.LOCAL) {
      logError("Local class '%s' cannot be a JsType.", type);
      return false;
    }

    return true;
  }

  private boolean checkNativeJsType(JDeclaredType type) {
    if (type.isEnumOrSubclass() != null) {
      logError("Enum '%s' cannot be a native JsType.", type);
      return false;
    }

    if (type.getClassDisposition() == NestedClassDisposition.INNER) {
      logError("Non static inner class '%s' cannot be a native JsType.", type);
      return false;
    }

    JClassType superClass = type.getSuperClass();
    if (superClass != null && superClass != jprogram.getTypeJavaLangObject() &&
        !superClass.isJsNative()) {
      logError("Native JsType '%s' can only extend native JsType classes.", type);
    }

    for (JInterfaceType interfaceType : type.getImplements()) {
      if (!interfaceType.isJsNative()) {
        logError(type, "Native JsType '%s' can only %s native JsType interfaces.",
            getDescription(type),
            type instanceof JInterfaceType ? "extend" : "implement");
      }
    }

    if (!isInitEmpty(type)) {
      logError("Native JsType '%s' cannot have initializer.", type);
    }

    return true;
  }

  private void checkMemberOfJsFunction(JMember member) {
    if (member.getJsMemberType() != JsMemberType.NONE) {
      logError(member,
          "JsFunction interface member '%s' cannot be JsMethod nor JsProperty.",
          JjsUtils.getReadableDescription(member));
    }

    if (member.isJsOverlay() || member.isSynthetic()) {
      return;
    }

    if (member instanceof JMethod && ((JMethod) member).isOrOverridesJsFunctionMethod()) {
      return;
    }

    logError(member, "JsFunction interface '%s' cannot declare non-JsOverlay member '%s'.",
        JjsUtils.getReadableDescription(member.getEnclosingType()),
        JjsUtils.getReadableDescription(member));
  }

  private void checkJsFunction(JDeclaredType type) {
    if (type.getImplements().size() > 0) {
      logError("JsFunction '%s' cannot extend other interfaces.", type);
    }

    if (type.isJsType()) {
      logError("'%s' cannot be both a JsFunction and a JsType at the same time.", type);
      return;
    }

    // Functional interface restriction already enforced by JSORestrictionChecker. It is safe
    // to assume here that there is a single abstract method.
    for (JMember member : type.getMembers()) {
      checkMemberOfJsFunction(member);
    }
  }

  private void checkMemberOfJsFunctionImplementation(JMember member) {
    if (member.getJsMemberType() != JsMemberType.NONE) {
      logError(member,
          "JsFunction implementation member '%s' cannot be JsMethod nor JsProperty.",
          JjsUtils.getReadableDescription(member));
    }

    if (!(member instanceof JMethod)) {
      return;
    }

    JMethod method = (JMethod) member;
    if (method.isOrOverridesJsFunctionMethod()
        || method.isSynthetic()
        || method.getOverriddenMethods().isEmpty()) {
      return;
    }

    // Methods that are not effectively static dispatch are disallowed. In this case these
    // could only be overridable methods of java.lang.Object, i.e. toString, hashCode and equals.
    logError(method, "JsFunction implementation '%s' cannot implement method '%s'.",
        JjsUtils.getReadableDescription(member.getEnclosingType()),
        JjsUtils.getReadableDescription(method));
  }

  private void checkJsFunctionImplementation(JDeclaredType type) {
    if (!type.isFinal()) {
      logError("JsFunction implementation '%s' must be final.",
          type);
    }

    if (type.getImplements().size() != 1) {
      logError("JsFunction implementation '%s' cannot implement more than one interface.",
          type);
    }

    if (type.getSuperClass() != jprogram.getTypeJavaLangObject()) {
      logError("JsFunction implementation '%s' cannot extend a class.", type);
    }

    if (type.isJsType()) {
      logError("'%s' cannot be both a JsFunction implementation and a JsType at the same time.",
          type);
      return;
    }
    for (JMember member : type.getMembers()) {
      checkMemberOfJsFunctionImplementation(member);
    }
  }

  private void checkJsFunctionSubtype(JDeclaredType type) {
    for (JInterfaceType superInterface : type.getImplements()) {
      if (superInterface.isJsFunction()) {
        logError(type, "'%s' cannot extend JsFunction '%s'.",
            JjsUtils.getReadableDescription(type), JjsUtils.getReadableDescription(superInterface));
      }
    }
  }

  private boolean checkProgram(TreeLogger logger) {
    for (JDeclaredType type : jprogram.getModuleDeclaredTypes()) {
      checkType(type);
    }
    checkStaticJsPropertyCalls();
    checkInstanceOfNativeJsTypesOrJsFunctionImplementations();
    checkSuperDispachToNativeJavaLangObjectMethodOverride();
    if (wasUnusableByJsWarningReported) {
      logSuggestion(
          "Suppress \"[unusable-by-js]\" warnings by adding a "
              + "`@SuppressWarnings(\"unusable-by-js\")` annotation to the corresponding member.");
    }

    boolean hasErrors = reportErrorsAndWarnings(logger);
    return !hasErrors;
  }

  private boolean isJsConstructorSubtype(JDeclaredType type) {
    JClassType superClass = type.getSuperClass();
    if (superClass == null) {
      return false;
    }

    if (JjsUtils.getJsConstructor(superClass) != null) {
      return true;
    }
    return isJsConstructorSubtype(superClass);
  }

  private static boolean isSubclassOfNativeClass(JDeclaredType type) {
    return JjsUtils.getNativeSuperClassOrNull(type) != null;
  }

  private void checkJsNameOnType(JDeclaredType type) {
    if (!type.getJsName().equals("*") && !type.getJsName().equals("?")) {
      checkJsName(type);
      return;
    }

    if (!type.isJsNative()
        || !(type instanceof JInterfaceType)
        || !JsInteropUtil.isGlobal(type.getJsNamespace())) {
      logError(type,
          "'%s' can only be used as a name for native interfaces in the global namespace.",
          type.getJsName());
    }
  }

  private void checkType(JDeclaredType type) {
    minimalRebuildCache.removeExportedNames(type.getName());

    if (type.isJsType()) {
      if (!checkJsType(type)) {
        return;
      }
      checkJsNameOnType(type);
      checkJsNamespace(type);
    }

    if (type.isJsNative()) {
      if (!checkNativeJsType(type)) {
        return;
      }
    } else if (isSubclassOfNativeClass(type)) {
      checkSubclassOfNativeClass(type);
    }

    if (type.isJsFunction()) {
      checkJsFunction(type);
    } else if (type.isJsFunctionImplementation()) {
      checkJsFunctionImplementation(type);
    } else {
      checkJsFunctionSubtype(type);
      checkJsConstructors(type);
      checkJsConstructorSubtype(type);
    }

    Multimap<String, JMember> instanceJsNames = collectInstanceMembersByJsNames(type);
    for (JMember member : type.getMembers()) {
      checkMember(member, instanceJsNames);
    }
  }

  private void checkSubclassOfNativeClass(JDeclaredType type) {
    assert (type instanceof JClassType);
    for (JMethod method : type.getMethods()) {
      if (!overridesObjectMethod(method) || !method.isSynthetic()) {
        continue;
      }
      // Only look at synthetic (accidental) overrides.
      for (JMethod overridenMethod : method.getOverriddenMethods()) {
        if (overridenMethod.getEnclosingType() instanceof JInterfaceType
            && overridenMethod.getJsMemberType() != JsMemberType.METHOD) {
          logError(
              type,
              "Native JsType subclass %s can not implement interface %s that declares method '%s' "
                  + "inherited from java.lang.Object.",
              getDescription(type),
              getDescription(overridenMethod.getEnclosingType()),
              overridenMethod.getName());
        }
      }
    }
  }

  private void checkUnusableByJs(JMember member) {
    if (member instanceof JMethod) {
      JMethod method = (JMethod) member;
      if (method.isSynthetic() && !method.isSyntheticAccidentalOverride()) {
        // Do not emit warnings for synthetic methods unless they are accidental overrides.
        return;
      }

      logIfUnusableByJs(member, "Return type of", member);

      for (JParameter parameter : method.getParams()) {
        String prefix = String.format("Type of parameter '%s' in", parameter.getName());
        logIfUnusableByJs(parameter, prefix, member);
      }
    } else {
      // it's a field
      logIfUnusableByJs(member, "Type of", member);
    }
  }

  private <T extends HasType & CanHaveSuppressedWarnings> void logIfUnusableByJs(
      T hasType, String prefix, JMember x) {
    if (hasType.getType().canBeReferencedExternally()) {
      return;
    }
    if (isUnusableByJsSuppressed(x.getEnclosingType()) || isUnusableByJsSuppressed(x)
        || isUnusableByJsSuppressed(hasType)) {
      return;
    }
    logWarning(x, "[unusable-by-js] %s %s is not usable by but exposed to JavaScript.", prefix,
        getMemberDescription(x));
    wasUnusableByJsWarningReported = true;
  }

  private Multimap<String, JMember> collectInstanceMembersByJsNames(JDeclaredType type) {
    if (type == null) {
      return LinkedHashMultimap.create();
    }

    Multimap<String, JMember> instanceMembersByJsName =
        collectInstanceMembersByJsNames(type.getSuperClass());
    for (JMember member : type.getMembers()) {
      if (isInstanceJsMember(member)) {
        addMember(instanceMembersByJsName, member);
      }
    }
    return instanceMembersByJsName;
  }

  private static void addMember(
      Multimap<String, JMember> instanceMembersByJsName, final JMember member) {
    String name = member.getJsName();
    Iterables.removeIf(instanceMembersByJsName.get(name),
        new Predicate<JMember>() {
          @Override
          public boolean apply(JMember m) {
            return overrides(member, m);
          }
        });

    instanceMembersByJsName.put(name, member);
  }

  private boolean isInstanceJsMember(JMember method) {
    return method.needsDynamicDispatch() && method.getJsMemberType() != JsMemberType.NONE
        && !isSyntheticBridgeMethod(method);
  }

  private boolean isSyntheticBridgeMethod(JMember member) {
    if (!(member instanceof JMethod)) {
      return false;
    }
    // A name slot taken up by a synthetic method, such as a bridge method for a generic method,
    // is not the fault of the user and so should not be reported as an error. JS generation
    // should take responsibility for ensuring that only the correct method version (in this
    // particular set of colliding method names) is exported. Forwarding synthetic methods
    // (such as an accidental override forwarding method that occurs when a JsType interface
    // starts exposing a method in class B that is only ever implemented in its parent class A)
    // though should be checked since they are exported and do take up an name slot.
    return member.isSynthetic() && !((JMethod) member).isForwarding();
  }

  private boolean isStaticJsMember(JMember member) {
    return !member.needsDynamicDispatch() && member.getJsMemberType() != JsMemberType.NONE;
  }

  private static boolean overrides(JMember member, JMember potentiallyOverriddenMember) {
    if (member instanceof JField || potentiallyOverriddenMember instanceof JField) {
      return false;
    }
    JMethod method = (JMethod) member;
    if (method.getOverriddenMethods().contains(potentiallyOverriddenMember)) {
      return true;
    }

    // Consider methods that have the same name and parameter signature to be overrides.
    // GWT models overrides similar to the JVM (not Java) in the sense that for a method to override
    // another they must have identical signatures (includes parameters and return type).
    // Methods that only differ in return types are Java overrides and need to be considered so
    // for instance name collision checking.
    JMethod potentiallyOverriddenMethod = (JMethod) potentiallyOverriddenMember;

    // TODO(goktug): make this more precise to handle package visibilities.
    boolean visibilitiesMatchesForOverride =
        !method.isPackagePrivate() && !method.isPrivate()
        && !potentiallyOverriddenMethod.isPackagePrivate()
        && !potentiallyOverriddenMethod.isPrivate();

    return visibilitiesMatchesForOverride
        && method.getJsniSignature(false, false)
               .equals(potentiallyOverriddenMethod.getJsniSignature(false, false));
  }

  private boolean isUnusableByJsSuppressed(CanHaveSuppressedWarnings x) {
    return x.getSuppressedWarnings() != null &&
        x.getSuppressedWarnings().contains(JsInteropUtil.UNUSABLE_BY_JS);
  }
}
