/*
 * Copyright 2014 The Closure Compiler Authors.
 *
 * 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.javascript.jscomp;

import com.google.common.collect.ImmutableMap;
import com.google.javascript.rhino.JSDocInfo;
import com.google.javascript.rhino.JSDocInfo.Visibility;
import com.google.javascript.rhino.Node;
import com.google.javascript.rhino.StaticSourceFile;
import com.google.javascript.rhino.jstype.JSType;
import com.google.javascript.rhino.jstype.ObjectType;
import com.google.javascript.rhino.jstype.PrototypeObjectType;

import javax.annotation.Nullable;

/**
 * Helper functions for computing the visibility of names and properties
 * in JavaScript source code.
 *
 * @author brndn@google.com (Brendan Linn)
 * @see CheckAccessControls
 */
public final class AccessControlUtils {

  /** Non-instantiable. */
  private AccessControlUtils() {}

  /**
   * Returns the effective visibility of the given name. This can differ
   * from the name's declared visibility if the file's {@code @fileoverview}
   * JsDoc specifies a default visibility.
   *
   * @param name The name node to compute effective visibility for.
   * @param var The name to compute effective visibility for.
   * @param fileVisibilityMap A map of {@code @fileoverview} visibility
   *     annotations, used to compute the name's default visibility.
   */
  static Visibility getEffectiveNameVisibility(Node name, Var var,
      ImmutableMap<StaticSourceFile, Visibility> fileVisibilityMap) {
    JSDocInfo jsDocInfo = var.getJSDocInfo();
    Visibility raw = (jsDocInfo == null || jsDocInfo.getVisibility() == null)
        ? Visibility.INHERITED
        : jsDocInfo.getVisibility();
    if (raw != Visibility.INHERITED) {
      return raw;
    }
    Visibility defaultVisibilityForFile =
        fileVisibilityMap.get(var.getSourceFile());
    JSType type = name.getJSType();
    boolean createdFromGoogProvide = (type instanceof PrototypeObjectType
        && ((PrototypeObjectType) type).isAnonymous());
    // Ignore @fileoverview visibility when computing the effective visibility
    // for names created by goog.provide.
    //
    // ProcessClosurePrimitives rewrites goog.provide()s as object literal
    // declarations, but the exact form depends on the ordering of the
    // input files. If goog.provide('a.b') occurs in the inputs before
    // goog.provide('a'), it is rewritten like
    //
    // var a={};a.b={};
    //
    // If the file containing goog.provide('a.b') also declares a @fileoverview
    // visibility, it must not apply to a, as this would make every a.* namespace
    // effectively package-private.
    return (createdFromGoogProvide || defaultVisibilityForFile == null)
        ? raw
        : defaultVisibilityForFile;
  }

  /**
   * Returns the effective visibility of the given property. This can differ
   * from the property's declared visibility if the property is inherited from
   * a superclass, or if the file's {@code @fileoverview} JsDoc specifies
   * a default visibility.
   *
   * @param property The property to compute effective visibility for.
   * @param referenceType The JavaScript type of the property.
   * @param fileVisibilityMap A map of {@code @fileoverview} visibility
   *     annotations, used to compute the property's default visibility.
   * @param codingConvention The coding convention in effect (if any),
   *     used to determine whether the property is private by lexical convention
   *     (example: trailing underscore).
   */
  static Visibility getEffectivePropertyVisibility(
      Node property,
      ObjectType referenceType,
      ImmutableMap<StaticSourceFile, Visibility> fileVisibilityMap,
      @Nullable CodingConvention codingConvention) {
    String propertyName = property.getLastChild().getString();
    StaticSourceFile definingSource = getDefiningSource(
        property, referenceType, propertyName);
    Visibility fileOverviewVisibility = fileVisibilityMap.get(definingSource);
    Node parent = property.getParent();
    boolean isOverride = parent.getJSDocInfo() != null
        && parent.isAssign()
        && parent.getFirstChild() == property;
    ObjectType objectType = getObjectType(
        referenceType, isOverride, propertyName);
    if (isOverride) {
      Visibility overridden = getOverriddenPropertyVisibility(
          objectType, propertyName);
      return getEffectiveVisibilityForOverriddenProperty(
          overridden, fileOverviewVisibility, propertyName, codingConvention);
    } else {
      return getEffectiveVisibilityForNonOverriddenProperty(
          property, objectType, fileOverviewVisibility, codingConvention);
    }
  }

  /**
   * Returns the source file in which the given property is defined,
   * or null if it is not known.
   */
  @Nullable private static StaticSourceFile getDefiningSource(
      Node getprop, @Nullable ObjectType referenceType, String propertyName) {
    if (referenceType != null) {
      Node propDefNode = referenceType.getPropertyNode(propertyName);
      if (propDefNode != null) {
        return propDefNode.getStaticSourceFile();
      }
    }
    return getprop.getStaticSourceFile();
  }

  /**
   * Returns the lowest property defined on a class with visibility information.
   */
  @Nullable private static ObjectType getObjectType(
      @Nullable ObjectType referenceType,
      boolean isOverride,
      String propertyName) {
    if (referenceType == null) {
      return null;
    }

    ObjectType objectType = isOverride
        ? referenceType.getImplicitPrototype()
        : referenceType;
    for (; objectType != null;
        objectType = objectType.getImplicitPrototype()) {
      JSDocInfo docInfo = objectType.getOwnPropertyJSDocInfo(propertyName);
      if (docInfo != null
          && docInfo.getVisibility() != Visibility.INHERITED) {
        return objectType;
      }
    }
    return null;
  }

  /**
   * Returns the original visibility of an overridden property.
   */
  private static Visibility getOverriddenPropertyVisibility(
      ObjectType objectType, String propertyName) {
    return objectType != null
        ? objectType.getOwnPropertyJSDocInfo(propertyName).getVisibility()
        : Visibility.INHERITED;
  }

  /**
   * Returns the effective visibility of the given overridden property.
   * An overridden propertiy inherits the visibility of the property it
   * overrides.
   */
  private static Visibility getEffectiveVisibilityForOverriddenProperty(
      Visibility visibility,
      @Nullable Visibility fileOverviewVisibility,
      String propertyName,
      @Nullable CodingConvention codingConvention) {
    if (codingConvention != null && codingConvention.isPrivate(propertyName)) {
      return Visibility.PRIVATE;
    }
    return (fileOverviewVisibility != null
        && visibility == Visibility.INHERITED)
        ? fileOverviewVisibility
        : visibility;
  }

  /**
   * Returns the effective visibility of the given non-overridden property.
   * Non-overridden properties without an explicit visibility annotation
   * receive the default visibility declared in the file's {@code @fileoverview}
   * block, if one exists.
   */
  private static Visibility getEffectiveVisibilityForNonOverriddenProperty(
      Node getprop,
      ObjectType objectType,
      @Nullable Visibility fileOverviewVisibility,
      @Nullable CodingConvention codingConvention) {
    String propertyName = getprop.getLastChild().getString();
    if (codingConvention != null && codingConvention.isPrivate(propertyName)) {
      return Visibility.PRIVATE;
    }
    Visibility raw = Visibility.INHERITED;
    if (objectType != null) {
      raw = objectType.getOwnPropertyJSDocInfo(propertyName).getVisibility();
    }
    JSType type = getprop.getJSType();
    boolean createdFromGoogProvide = (type instanceof PrototypeObjectType
        && ((PrototypeObjectType) type).isAnonymous());
    // Ignore @fileoverview visibility when computing the effective visibility
    // for properties created by goog.provide.
    //
    // ProcessClosurePrimitives rewrites goog.provide()s as object literal
    // declarations, but the exact form depends on the ordering of the
    // input files. If goog.provide('a.b.c') occurs in the inputs before
    // goog.provide('a'), it is rewritten like
    //
    // var a={};a.b={}a.b.c={};
    //
    // If the file containing goog.provide('a.b.c') also declares
    // a @fileoverview visibility, it must not apply to b, as this would make
    // every a.b.* namespace effectively package-private.
    return (raw != Visibility.INHERITED
        || fileOverviewVisibility == null
        || createdFromGoogProvide)
        ? raw
        : fileOverviewVisibility;
  }
}
