// 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 com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.devtools.build.docgen.annot.DocCategory;
import com.google.devtools.build.docgen.starlark.StarlarkDocExpander;
import com.google.devtools.build.docgen.starlark.StarlarkDocPage;
import com.google.devtools.build.lib.util.Classpath.ClassPathException;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import net.starlark.java.annot.StarlarkBuiltin;

/** A class to assemble documentation for Starlark. */
public final class StarlarkDocumentationProcessor {

  private StarlarkDocumentationProcessor() {}

  /** Generates the Starlark documentation to the given output directory. */
  public static void generateDocumentation(String outputDir, String... args)
      throws IOException, ClassPathException {
    Map<String, String> options = parseOptions(args);

    String docsRoot = options.get("--starlark_docs_root");
    if (docsRoot != null) {
      DocgenConsts.starlarkDocsRoot = docsRoot;
    }

    String linkMapPath = options.get("--link_map_path");
    if (linkMapPath == null) {
      throw new IllegalArgumentException("Required option '--link_map_path' is missing.");
    }

    DocLinkMap linkMap = DocLinkMap.createFromFile(linkMapPath);
    StarlarkDocExpander expander =
        new StarlarkDocExpander(new RuleLinkExpander(/* singlePage= */ false, linkMap));

    ImmutableMap<Category, ImmutableList<StarlarkDocPage>> allPages =
        StarlarkDocumentationCollector.getAllDocPages(expander);

    for (var categoryAndPages : allPages.entrySet()) {
      writeCategoryPage(
          categoryAndPages.getKey(), outputDir, categoryAndPages.getValue(), expander);
      for (StarlarkDocPage page : categoryAndPages.getValue()) {
        writePage(outputDir, categoryAndPages.getKey(), page);
      }
    }

    writeOverviewPage(outputDir, allPages);
    if (shouldCreateToc(options)) {
      writeTableOfContents(outputDir, allPages);
    }
  }

  private static void writePage(String outputDir, Category category, StarlarkDocPage docPage)
      throws IOException {
    File starlarkDocPath =
        new File(String.format("%s/%s/%s.html", outputDir, category.getPath(), docPage.getName()));
    Page page = TemplateEngine.newPage(DocgenConsts.STARLARK_LIBRARY_TEMPLATE);
    page.add("page", docPage);
    page.write(starlarkDocPath);
  }

  private static void writeCategoryPage(
      Category category,
      String outputDir,
      ImmutableList<StarlarkDocPage> allPages,
      StarlarkDocExpander expander)
      throws IOException {
    Files.createDirectories(Path.of(outputDir, category.getPath()));

    File starlarkDocPath = new File(String.format("%s/%s.html", outputDir, category.getPath()));
    Page page = TemplateEngine.newPage(DocgenConsts.STARLARK_MODULE_CATEGORY_TEMPLATE);
    page.add("category", category);
    page.add("allPages", allPages);
    page.add("description", expander.expand(category.description));
    page.write(starlarkDocPath);
  }

  private static void writeOverviewPage(
      String outputDir, ImmutableMap<Category, ImmutableList<StarlarkDocPage>> allPages)
      throws IOException {
    File starlarkDocPath = new File(outputDir + "/overview.html");
    Page page = TemplateEngine.newPage(DocgenConsts.STARLARK_OVERVIEW_TEMPLATE);
    page.add("allPages", allPages);
    page.write(starlarkDocPath);
  }

  private static void writeTableOfContents(
      String outputDir, ImmutableMap<Category, ImmutableList<StarlarkDocPage>> allPages)
      throws IOException {
    File starlarkDocPath = new File(outputDir + "/_toc.yaml");
    Page page = TemplateEngine.newPage(DocgenConsts.STARLARK_TOC_TEMPLATE);
    page.add("allPages", allPages);
    page.write(starlarkDocPath);
  }

  private static Map<String, String> parseOptions(String... args) {
    Map<String, String> options = new HashMap<>();
    for (String arg : args) {
      if (arg.startsWith("--")) {
        String[] parts = arg.split("=", 2);
        options.put(parts[0], parts.length > 1 ? parts[1] : null);
      }
    }
    return options;
  }

  private static boolean shouldCreateToc(Map<String, String> options) {
    String arg = options.get("--create_toc");
    if (arg == null) {
      return false;
    }
    return Boolean.parseBoolean(arg) || arg.equals("1");
  }

  /**
   * An enumeration of categories used to organize the API index. Instances of this class are
   * accessed by templates, using reflection.
   */
  public enum Category {
    GLOBAL_FUNCTION(
        "Global functions",
        "globals",
        "This section lists the global functions available in Starlark. The list of available"
            + " functions differs depending on the file type (whether a BUILD file, or a .bzl file,"
            + " etc)."),

    CONFIGURATION_FRAGMENT(
        "Configuration Fragments",
        "fragments",
        "Configuration fragments give rules access to "
            + "language-specific parts of <a href=\"builtins/configuration.html\">"
            + "configuration</a>. "
            + "<p>Rule implementations can get them using "
            + "<code><a href=\"builtins/ctx.html#fragments\">ctx."
            + "fragments</a>.<i>[fragment name]</i></code>"),

    PROVIDER(
        "Providers",
        "providers",
        "This section lists providers available on built-in rules. See the <a"
            + " href='https://bazel.build/extending/rules#providers'>Rules page</a> for more on"
            + " providers. These symbols are available only in .bzl files."),

    BUILTIN(
        "Built-in Types",
        "builtins",
        "This section lists types of Starlark objects. With some exceptions, these type names are"
            + " not valid Starlark symbols; instances of them may be acquired through different"
            + " means."),

    // Used for top-level modules of functions in the global namespace. Such modules will always
    // be usable solely by accessing their members, via modulename.funcname() or
    // modulename.constantname.
    // Examples: attr, cc_common, config, java_common
    TOP_LEVEL_MODULE(
        "Top-level Modules",
        "toplevel",
        "This section lists top-level modules. These symbols are available only in .bzl files."),

    CORE(
        "Core Starlark data types",
        "core",
        "This section lists the data types of the <a"
            + " href='https://github.com/bazelbuild/starlark/blob/master/spec.md#built-in-constants-and-functions'>Starlark"
            + " core language</a>. With some exceptions, these type names are not valid Starlark"
            + " symbols; instances of them may be acquired through different means.");

    // Maps (essentially free-form) strings in annotations to permitted categories.
    public static Category of(StarlarkBuiltin annot) {
      return switch (annot.category()) {
        case DocCategory.CONFIGURATION_FRAGMENT -> CONFIGURATION_FRAGMENT;
        case DocCategory.PROVIDER -> PROVIDER;
        case DocCategory.BUILTIN -> BUILTIN;
        case DocCategory.TOP_LEVEL_MODULE -> TOP_LEVEL_MODULE;
        case "core", "core.lib" ->
            // interpreter built-ins (e.g. int)
            // Starlark standard modules (e.g. json)
            CORE;
        default ->
            throw new IllegalStateException(
                String.format(
                    "docgen does not recognize DocCategory '%s' for StarlarkBuiltin '%s'",
                    annot.category(), annot.name()));
      };
    }

    Category(String title, String path, String description) {
      this.title = title;
      this.path = path;
      this.description = description;
    }

    private final String title;
    private final String path;
    private final String description;

    public String getTitle() {
      return title;
    }

    public String getDescription() {
      return description;
    }

    public String getPath() {
      return path;
    }
  }
}
