// Copyright 2014 The Bazel Authors. All rights reserved.
//
// 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.devtools.build.docgen;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.collect.ImmutableList.toImmutableList;

import com.google.common.base.Preconditions;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.flogger.GoogleLogger;
import com.google.devtools.build.docgen.StarlarkDocumentationProcessor.Category;
import com.google.devtools.build.docgen.annot.GlobalMethodDocs;
import com.google.devtools.build.docgen.annot.GlobalMethodDocs.Environment;
import com.google.devtools.build.docgen.annot.StarlarkConstructor;
import com.google.devtools.build.docgen.starlark.AnnotStarlarkBuiltinDoc;
import com.google.devtools.build.docgen.starlark.AnnotStarlarkConstructorMethodDoc;
import com.google.devtools.build.docgen.starlark.AnnotStarlarkOrdinaryMethodDoc;
import com.google.devtools.build.docgen.starlark.StardocProtoFunctionDoc;
import com.google.devtools.build.docgen.starlark.StardocProtoProviderDocPage;
import com.google.devtools.build.docgen.starlark.StardocProtoStructDocPage;
import com.google.devtools.build.docgen.starlark.StarlarkDocExpander;
import com.google.devtools.build.docgen.starlark.StarlarkDocPage;
import com.google.devtools.build.docgen.starlark.StarlarkGlobalsDoc;
import com.google.devtools.build.docgen.starlark.TypeParser;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.AspectInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.MacroInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.ModuleExtensionInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.ModuleInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.ProviderInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.RepositoryRuleInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.RuleInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.StarlarkFunctionInfo;
import com.google.devtools.build.lib.starlarkdocextract.StardocOutputProtos.StarlarkOtherSymbolInfo;
import com.google.devtools.build.lib.util.Classpath;
import com.google.devtools.build.lib.util.Classpath.ClassPathException;
import com.google.protobuf.ExtensionRegistry;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.text.Collator;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import net.starlark.java.annot.StarlarkAnnotations;
import net.starlark.java.annot.StarlarkBuiltin;
import net.starlark.java.annot.StarlarkMethod;
import net.starlark.java.eval.Starlark;
import net.starlark.java.eval.StarlarkSemantics;

/**
 * A helper class that collects Starlark module documentation.
 *
 * <p>The documentation comes from {@link StarlarkBuiltin} annotations in Java code or from Stardoc
 * protos produced (via {@code starlark_doc_extract} from specially-structured .bzl files serving as
 * entry points for Starlark APIs. Such an entry point .bzl file is expected to contain only the
 * following documentable entities (whose names must be unique across all .bzl files being
 * processed):
 *
 * <ul>
 *   <li>Providers, defined at global scope. Field docstrings can be prefixed with a type expression
 *       enclosed in parentheses, optionally followed by a colon, for example {@code "(list[string])
 *       Some free text about field foo"}
 *   <li>Structs, defined at global scope, documented using {@code #:}-prefixed doc comments, and
 *       containing only function members or aliases of providers. The returns and parameter
 *       sections of the function members' docstrings can be prefixed with a type expression
 *       enclosed in parentheses, optionally followed by a colon, for example {@code "(string |
 *       None): Some free text about parameter blah"}
 * </ul>
 *
 * <p>Notably, .bzl files from which Build Encyclopedia content is extracted have a different,
 * incompatible structure.
 */
final class StarlarkDocumentationCollector {
  private StarlarkDocumentationCollector() {}

  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();
  private static ImmutableMap<Category, ImmutableList<StarlarkDocPage>> all;

  /** Applies {@link #collectDocPages} to all Bazel and Starlark classes. */
  static synchronized ImmutableMap<Category, ImmutableList<StarlarkDocPage>> getAllDocPages(
      StarlarkDocExpander expander, ImmutableList<String> apiStardocProtos)
      throws ClassPathException, IOException {
    if (all == null) {
      ImmutableList.Builder<ModuleInfo> parsedApiStardocProtos = ImmutableList.builder();
      for (String filename : apiStardocProtos) {
        parsedApiStardocProtos.add(
            ModuleInfo.parseFrom(
                new FileInputStream(filename), ExtensionRegistry.getEmptyRegistry()));
      }
      all =
          collectDocPages(
              expander,
              Iterables.concat(
                  /*Bazel*/ Classpath.findClasses("com/google/devtools/build"),
                  /*Starlark*/ Classpath.findClasses("net/starlark/java")),
              parsedApiStardocProtos.build());
    }
    return all;
  }

  /**
   * Collects the documentation for all Starlark modules comprised of the given classes and returns
   * a map from the name of each Starlark module to its documentation.
   */
  static ImmutableMap<Category, ImmutableList<StarlarkDocPage>> collectDocPages(
      StarlarkDocExpander expander,
      Iterable<Class<?>> classes,
      ImmutableList<ModuleInfo> apiStardocProtos) {
    Map<Category, Map<String, StarlarkDocPage>> pages = new EnumMap<>(Category.class);
    for (Category category : Category.values()) {
      pages.put(category, new HashMap<>());
    }

    // 1. Add all classes/interfaces annotated with @StarlarkBuiltin with documented = true.
    for (Class<?> candidateClass : classes) {
      collectStarlarkBuiltin(candidateClass, pages, expander);
    }

    // 2. Add all object methods and global functions.
    //
    //    Also, explicitly process the Starlark interpreter's MethodLibrary
    //    class, which defines None, len, range, etc.
    //    TODO(adonovan): do this without peeking into the implementation,
    //    e.g. by looking at Starlark.UNIVERSE, something like this:
    //
    //    for (Map<String, Object> e : Starlark.UNIVERSE.entrySet()) {
    //      if (e.getValue() instanceof BuiltinFunction) {
    //        BuiltinFunction fn = (BuiltinFunction) e.getValue();
    //        topLevelModuleDoc.addMethod(
    //          new StarlarkJavaMethodDoc("", fn.getJavaMethod(), fn.getAnnotation(), expander));
    //      }
    //    }
    //
    //    Note that BuiltinFunction doesn't actually have getJavaMethod.
    //
    for (Class<?> candidateClass : classes) {
      collectBuiltinMethods(candidateClass, pages, expander);
      collectGlobalMethods(candidateClass, pages, expander);
    }

    // 3. Add all constructors.
    for (Class<?> candidateClass : classes) {
      collectConstructorMethods(candidateClass, pages, expander);
    }

    // 4. Add docs from .bzl files.
    HashMap<String, ModuleInfo> bzlStructPages = new HashMap<>();
    for (ModuleInfo moduleInfo : apiStardocProtos) {
      collectFromStardocProto(moduleInfo, pages, bzlStructPages, expander);
    }

    // 5. Define a parser for type expressions in .bzl-defined doc strings.
    // This parser needs a map from type identifiers (e.g. core Starlark types, BUILD language
    // types, and providers) to their categories, so that it can generate link URLs for them.
    ImmutableMap.Builder<String, Category> typeIdentifierToCategory = ImmutableMap.builder();
    for (Map.Entry<Category, Map<String, StarlarkDocPage>> pagesEntry : pages.entrySet()) {
      if (pagesEntry.getKey() == Category.CONFIGURATION_FRAGMENT) {
        // Assume nothing returns a configuration fragment; some of them clash with names of
        // built-in modules.
        continue;
      }
      for (StarlarkDocPage page : pagesEntry.getValue().values()) {
        typeIdentifierToCategory.put(page.getName(), pagesEntry.getKey());
      }
    }
    expander.setTypeParser(new TypeParser(typeIdentifierToCategory.buildOrThrow()));

    return ImmutableMap.copyOf(
        Maps.transformValues(
            pages,
            pagesInCategory ->
                ImmutableList.sortedCopyOf(
                    Comparator.comparing(
                        StarlarkDocPage::getTitle, Collator.getInstance(Locale.US)),
                    pagesInCategory.values())));
  }

  /**
   * Adds a single {@link StarlarkDocPage} entry to {@code pages} representing the given {@code
   * builtinClass}, if it is a documented builtin.
   */
  private static void collectStarlarkBuiltin(
      Class<?> builtinClass,
      Map<Category, Map<String, StarlarkDocPage>> pages,
      StarlarkDocExpander expander) {
    StarlarkBuiltin starlarkBuiltin = builtinClass.getAnnotation(StarlarkBuiltin.class);
    if (starlarkBuiltin == null || !starlarkBuiltin.documented()) {
      return;
    }

    Map<String, StarlarkDocPage> pagesInCategory = pages.get(Category.of(starlarkBuiltin));
    StarlarkDocPage existingPage = pagesInCategory.get(starlarkBuiltin.name());
    if (existingPage == null) {
      pagesInCategory.put(
          starlarkBuiltin.name(),
          new AnnotStarlarkBuiltinDoc(starlarkBuiltin, builtinClass, expander));
      return;
    }

    // Handle a strange corner-case: If builtinClass has a subclass which is also
    // annotated with @StarlarkBuiltin with the same name, and also has the same
    // docstring, then the subclass takes precedence.
    // (This is useful if one class is the "common" one with stable methods, and its subclass is
    // an experimental class that also supports all stable methods.)
    Preconditions.checkState(
        existingPage instanceof AnnotStarlarkBuiltinDoc,
        "the same name %s is assigned to both a global method environment and a builtin type",
        starlarkBuiltin.name());
    Class<?> clazz = ((AnnotStarlarkBuiltinDoc) existingPage).getClassObject();
    validateCompatibleBuiltins(clazz, builtinClass);

    if (clazz.isAssignableFrom(builtinClass)) {
      // The new builtin is a subclass of the old builtin, so use the subclass.
      pagesInCategory.put(
          starlarkBuiltin.name(),
          new AnnotStarlarkBuiltinDoc(starlarkBuiltin, builtinClass, expander));
    }
  }

  /** Validate that it is acceptable that the given builtin classes with the same name co-exist. */
  private static void validateCompatibleBuiltins(Class<?> one, Class<?> two) {
    StarlarkBuiltin builtinOne = one.getAnnotation(StarlarkBuiltin.class);
    StarlarkBuiltin builtinTwo = two.getAnnotation(StarlarkBuiltin.class);
    if (one.isAssignableFrom(two) || two.isAssignableFrom(one)) {
      if (!builtinOne.doc().equals(builtinTwo.doc())) {
        throw new IllegalStateException(
            String.format(
                "%s and %s are related builtins but have mismatching documentation for '%s'",
                one, two, builtinOne.name()));
      }
    } else {
      throw new IllegalStateException(
          String.format(
              "%s and %s are unrelated builtins with documentation for '%s'",
              one, two, builtinOne.name()));
    }
  }

  private static void collectBuiltinMethods(
      Class<?> builtinClass,
      Map<Category, Map<String, StarlarkDocPage>> pages,
      StarlarkDocExpander expander) {
    StarlarkBuiltin starlarkBuiltin = builtinClass.getAnnotation(StarlarkBuiltin.class);

    if (starlarkBuiltin == null || !starlarkBuiltin.documented()) {
      return;
    }
    AnnotStarlarkBuiltinDoc builtinDoc =
        (AnnotStarlarkBuiltinDoc)
            pages.get(Category.of(starlarkBuiltin)).get(starlarkBuiltin.name());

    if (builtinClass != builtinDoc.getClassObject()) {
      return;
    }
    for (Map.Entry<Method, StarlarkMethod> entry :
        Starlark.getMethodAnnotations(builtinClass).entrySet()) {
      // Collect methods that aren't directly constructors (i.e. have the @StarlarkConstructor
      // annotation).
      if (entry.getKey().isAnnotationPresent(StarlarkConstructor.class)) {
        continue;
      }
      Method javaMethod = entry.getKey();
      StarlarkMethod starlarkMethod = entry.getValue();
      // Struct fields that return a type that has @StarlarkConstructor are a bit special:
      // they're visited here because they're seen as an attribute of the module, but act more
      // like a reference to the type they construct.
      // TODO(wyv): does this actually happen???
      if (starlarkMethod.structField()) {
        Method selfCall =
            Starlark.getSelfCallMethod(StarlarkSemantics.DEFAULT, javaMethod.getReturnType());
        if (selfCall != null && selfCall.isAnnotationPresent(StarlarkConstructor.class)) {
          javaMethod = selfCall;
        }
      }
      builtinDoc.addMember(
          new AnnotStarlarkOrdinaryMethodDoc(
              builtinDoc.getName(), javaMethod, starlarkMethod, expander));
    }
  }

  /**
   * Adds {@link StarlarkJavaMethodDoc} entries to the top level module, one for
   * each @StarlarkMethod method defined in the given @GlobalMethodDocs class {@code clazz}.
   */
  private static void collectGlobalMethods(
      Class<?> clazz,
      Map<Category, Map<String, StarlarkDocPage>> pages,
      StarlarkDocExpander expander) {
    GlobalMethodDocs globalMethods = clazz.getAnnotation(GlobalMethodDocs.class);

    if (globalMethods == null && !clazz.getName().equals("net.starlark.java.eval.MethodLibrary")) {
      return;
    }

    Environment[] environments =
        globalMethods == null ? new Environment[] {Environment.ALL} : globalMethods.environment();
    for (Environment environment : environments) {
      StarlarkDocPage page =
          pages
              .get(Category.GLOBAL_FUNCTION)
              .computeIfAbsent(
                  environment.getTitle(), title -> new StarlarkGlobalsDoc(environment, expander));
      for (Map.Entry<Method, StarlarkMethod> entry :
          Starlark.getMethodAnnotations(clazz).entrySet()) {
        // Only add non-constructor global library methods. Constructors are added later.
        // TODO(wyv): add a redirect instead
        if (!entry.getKey().isAnnotationPresent(StarlarkConstructor.class)) {
          page.addMember(
              new AnnotStarlarkOrdinaryMethodDoc("", entry.getKey(), entry.getValue(), expander));
        }
      }
    }
  }

  private static void collectConstructor(
      Map<Category, Map<String, StarlarkDocPage>> pages,
      Method method,
      StarlarkDocExpander expander) {
    if (!method.isAnnotationPresent(StarlarkConstructor.class)) {
      return;
    }

    StarlarkBuiltin starlarkBuiltin =
        StarlarkAnnotations.getStarlarkBuiltin(method.getReturnType());
    if (starlarkBuiltin == null || !starlarkBuiltin.documented()) {
      // The class of the constructed object type has no documentation, so no place to add
      // constructor information.
      return;
    }
    StarlarkMethod methodAnnot =
        Preconditions.checkNotNull(method.getAnnotation(StarlarkMethod.class));
    StarlarkDocPage doc = pages.get(Category.of(starlarkBuiltin)).get(starlarkBuiltin.name());
    doc.setConstructor(
        new AnnotStarlarkConstructorMethodDoc(
            starlarkBuiltin.name(), method, methodAnnot, expander));
  }

  /**
   * Parses a Starlark API proto to produce {@link StardocProtoStructDocPage} and {@link
   * StardocProtoProviderDocPage} pages, inserting them into the appropriate categories of {@code
   * pages}.
   *
   * @param moduleInfo a Stardoc proto for a .bzl file serving as an entry point for Starlark APIs
   * @param pages the categorized map of documentation pages; added to by this method
   * @param bzlStructPages a map from names of structs whose documentation has been collected to the
   *     Stardoc protos defining them; added to by this method
   * @param expander the expander to use for links
   */
  private static void collectFromStardocProto(
      ModuleInfo moduleInfo,
      Map<Category, Map<String, StarlarkDocPage>> pages,
      Map<String, ModuleInfo> bzlStructPages,
      StarlarkDocExpander expander) {
    // For now, support only the following:
    // - structs containing only functions or provider aliases (classified as TOP_LEVEL_MODULE)
    // - providers not contained in a struct (classified as PROVIDER)
    Map<String, StarlarkDocPage> pagesInCategory = pages.get(Category.TOP_LEVEL_MODULE);
    for (StarlarkOtherSymbolInfo symbolInfo : moduleInfo.getStarlarkOtherSymbolInfoList()) {
      if (symbolInfo.getTypeName().equals("struct")) {
        String structName = symbolInfo.getName();
        if (structName.contains(".")) {
          // Skip nested structs.
          continue;
        }
        if (pagesInCategory.containsKey(structName)) {
          checkState(
              !bzlStructPages.containsKey(structName),
              "Conflicting documentation for struct '%s' defined in Starlark files %s and %s",
              structName,
              moduleInfo.getFile(),
              bzlStructPages.get(structName).getFile());
          logger.atWarning().log(
              "Documentation for struct %s defined in %s overrides previously-seen documentation"
                  + " for module %s implemented in Java",
              structName, moduleInfo.getFile(), structName);
        }
        pagesInCategory.put(
            structName, new StardocProtoStructDocPage(expander, moduleInfo, symbolInfo));
      }
    }

    for (StarlarkFunctionInfo functionInfo : moduleInfo.getFuncInfoList()) {
      String functionName = functionInfo.getFunctionName();
      checkState(
          functionName.contains("."),
          "Function %s defined in %s must be namespaced inside a struct",
          functionName,
          moduleInfo.getFile());
      String structName = getStructName(functionName, moduleInfo);
      StarlarkDocPage page = checkNotNull(pagesInCategory.get(structName));
      page.addMember(new StardocProtoFunctionDoc(expander, moduleInfo, structName, functionInfo));
    }

    for (ProviderInfo providerInfo : moduleInfo.getProviderInfoList()) {
      String providerName = providerInfo.getProviderName();
      if (providerName.contains(".")) {
        // Aliased provider inside a struct.
        String structName = getStructName(providerName, moduleInfo);
        StardocProtoStructDocPage structPage =
            (StardocProtoStructDocPage) checkNotNull(pagesInCategory.get(structName));
        structPage.addProviderAlias(providerInfo);
      } else {
        // Top-level provider.
        pages
            .get(Category.PROVIDER)
            .put(providerName, new StardocProtoProviderDocPage(expander, moduleInfo, providerInfo));
      }
    }

    // TODO(arostovtsev): What about other types of members in structs? Need changes to
    // starlark_doc_extract to check for their presence.
    verifyDoNotExist(
        moduleInfo,
        "aspects",
        moduleInfo.getAspectInfoList().stream()
            .map(AspectInfo::getAspectName)
            .collect(toImmutableList()));
    verifyDoNotExist(
        moduleInfo,
        "macros",
        moduleInfo.getMacroInfoList().stream()
            .map(MacroInfo::getMacroName)
            .collect(toImmutableList()));
    verifyDoNotExist(
        moduleInfo,
        "module extesions",
        moduleInfo.getModuleExtensionInfoList().stream()
            .map(ModuleExtensionInfo::getExtensionName)
            .collect(toImmutableList()));
    verifyDoNotExist(
        moduleInfo,
        "repository rules",
        moduleInfo.getRepositoryRuleInfoList().stream()
            .map(RepositoryRuleInfo::getRuleName)
            .collect(toImmutableList()));
    verifyDoNotExist(
        moduleInfo,
        "rules",
        moduleInfo.getRuleInfoList().stream()
            .map(RuleInfo::getRuleName)
            .collect(toImmutableList()));
  }

  /**
   * Given a name of a struct member, for example "a.b.c", verifies that "a" is the name of a
   * documented struct in the moduleInfo and returns it.
   */
  private static String getStructName(String memberName, ModuleInfo moduleInfo) {
    String structName = Splitter.on('.').splitToList(memberName).getFirst();
    checkState(
        moduleInfo.getStarlarkOtherSymbolInfoList().stream()
            .anyMatch(symbolInfo -> symbolInfo.getName().equals(structName)),
        "Struct %s defined in %s must be documented with '#:'-prefixed doc comments",
        structName,
        moduleInfo.getFile());
    return structName;
  }

  private static void verifyDoNotExist(ModuleInfo moduleInfo, String what, List<String> badNames) {
    checkState(
        badNames.isEmpty(),
        "Starlark and BUILD language API entry point %s is expected not to contain %s;"
            + " found %s",
        moduleInfo.getFile(),
        what,
        badNames);
  }

  /**
   * Collect two types of constructor methods:
   *
   * <p>1. The single method with selfCall=true and @StarlarkConstructor (if present)
   *
   * <p>2. Any methods annotated with @StarlarkConstructor
   *
   * <p>Structfield methods that return an object which itself has selfCall=true
   * and @StarlarkConstructor are *not* collected here (collectModuleMethods does that). (For
   * example, supposed Foo has a structfield method named 'Bar', which refers to the Bar type. In
   * Foo's doc, we describe Foo.Bar as an attribute of type Bar and link to the canonical Bar type
   * documentation)
   */
  private static void collectConstructorMethods(
      Class<?> clazz,
      Map<Category, Map<String, StarlarkDocPage>> pages,
      StarlarkDocExpander expander) {
    if (!clazz.isAnnotationPresent(StarlarkBuiltin.class)
        && !clazz.isAnnotationPresent(GlobalMethodDocs.class)) {
      return;
    }
    Method selfCall = Starlark.getSelfCallMethod(StarlarkSemantics.DEFAULT, clazz);
    if (selfCall != null) {
      collectConstructor(pages, selfCall, expander);
    }

    for (Method method : Starlark.getMethodAnnotations(clazz).keySet()) {
      collectConstructor(pages, method, expander);
    }
  }
}
