/*
 * 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 org.jspecify.nullness.Nullable;

/**
 * Helper functions for computing the visibility of names and properties
 * in JavaScript source code.
 *
 * @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 != null && type.isLiteralObject());
    // 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) {
    String propertyName = property.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);
    } else {
      return getEffectiveVisibilityForNonOverriddenProperty(
          property, objectType, fileOverviewVisibility);
    }
  }

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

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

    // Find the lowest property defined on a class with visibility information.
    ObjectType current = isOverride ? referenceType.getImplicitPrototype() : referenceType;
    for (; current != null; current = current.getImplicitPrototype()) {
      JSDocInfo docInfo = current.getOwnPropertyJSDocInfo(propertyName);
      if (docInfo != null && docInfo.getVisibility() != Visibility.INHERITED) {
        return current;
      }
    }
    return null;
  }

  /** Returns the original visibility of an overridden property. */
  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 property
   * inherits the visibility of the property it overrides.
   */
  static Visibility getEffectiveVisibilityForOverriddenProperty(
      Visibility visibility, @Nullable Visibility fileOverviewVisibility, String propertyName) {
    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) {
    String propertyName = getprop.getString();
    Visibility raw = Visibility.INHERITED;
    if (objectType != null) {
      raw = objectType.getOwnPropertyJSDocInfo(propertyName).getVisibility();
    }
    JSType type = getprop.getJSType();
    boolean createdFromGoogProvide = (type != null && type.isLiteralObject());
    // 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;
  }
}
