# Lint as: python3
# pylint: disable=g-direct-third-party-import
# Copyright 2022 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.
"""A tool for building the documentation for a Bazel release."""
import os
import shutil
import sys
import tarfile
import tempfile
import zipfile

from absl import app
from absl import flags

from scripts.docs import rewriter

FLAGS = flags.FLAGS

flags.DEFINE_string("version", None, "Name of the Bazel release.")
flags.DEFINE_string(
    "toc_path",
    None,
    "Path to the _toc.yaml file that contains the table of contents for the versions menu.",
)
flags.DEFINE_string(
    "narrative_docs_path",
    None,
    "Path of the archive (zip or tar) that contains the narrative documentation.",
)
flags.DEFINE_string(
    "reference_docs_path",
    None,
    "Path of the archive (zip or tar) that contains the reference documentation.",
)
flags.DEFINE_string(
    "output_path", None,
    "Location where the zip'ed documentation should be written to.")

_ARCHIVE_FUNCTIONS = {".tar": tarfile.open, ".zip": zipfile.ZipFile}


def validate_flag(name):
  """Ensures that a flag is set, and returns its value (if yes).

  This function exits with an error if the flag was not set.

  Args:
    name: Name of the flag.

  Returns:
    The value of the flag, if set.
  """
  value = getattr(FLAGS, name, None)
  if value:
    return value

  print("Missing --{} flag.".format(name), file=sys.stderr)
  exit(1)


def create_docs_tree(version, toc_path, narrative_docs_path,
                     reference_docs_path):
  """Creates a directory tree containing the docs for the Bazel version.

  Args:
    version: Version of this Bazel release.
    toc_path: Absolute path to the _toc.yaml file that lists the most recent
      Bazel versions.
    narrative_docs_path: Absolute path of an archive that contains the narrative
      documentation (can be .zip or .tar).
    reference_docs_path: Absolute path of an archive that contains the reference
      documentation (can be .zip or .tar).

  Returns:
    The absolute paths of the root of the directory tree and of
      the final _toc.yaml file.
  """
  root_dir = tempfile.mkdtemp()

  versions_dir = os.path.join(root_dir, "versions")
  os.makedirs(versions_dir)

  toc_dest_path = os.path.join(versions_dir, "_toc.yaml")
  shutil.copyfile(toc_path, toc_dest_path)

  release_dir = os.path.join(versions_dir, version)
  os.makedirs(release_dir)

  try_extract(narrative_docs_path, release_dir)
  try_extract(reference_docs_path, release_dir)

  return root_dir, toc_dest_path


def try_extract(archive_path, output_dir):
  """Tries to extract the given archive into the given directory.

  This function will raise an error if the archive type is not supported.

  Args:
    archive_path: Absolute path of an archive that should be extracted. Can be
      .tar or .zip.
    output_dir: Absolute path of the directory into which the archive should be
      extracted

  Raises:
    ValueError: If the archive has an unsupported file type.
  """
  _, ext = os.path.splitext(archive_path)
  open_func = _ARCHIVE_FUNCTIONS.get(ext)
  if not open_func:
    raise ValueError("File {}: Invalid file extension '{}'. Allowed: {}".format(
        archive_path, ext, _ARCHIVE_FUNCTIONS.keys.join(", ")))

  with open_func(archive_path, "r") as archive:
    archive.extractall(output_dir)


def build_archive(version, root_dir, toc_path, output_path):
  """Builds a documentation archive for the given Bazel release.

  This function reads all documentation files from the tree rooted in root_dir,
  fixes all links so that they point at versioned files, then builds a zip
  archive of all files.

  Args:
    version: Version of the Bazel release whose documentation is being built.
    root_dir: Absolute path of the directory that contains the documentation
      tree.
    toc_path: Absolute path of the _toc.yaml file.
    output_path: Absolute path where the archive should be written to.
  """
  with zipfile.ZipFile(output_path, "w") as archive:
    for root, _, files in os.walk(root_dir):
      for f in files:
        src = os.path.join(root, f)
        dest = src[len(root_dir) + 1:]

        if src != toc_path and rewriter.can_rewrite(src):
          archive.writestr(dest, get_versioned_content(src, version))
        else:
          archive.write(src, dest)


def get_versioned_content(path, version):
  """Rewrites links in the given file to point at versioned docs.

  Args:
    path: Absolute path of the file that should be rewritten.
    version: Version of the Bazel release whose documentation is being built.

  Returns:
    The content of the given file, with rewritten links.
  """
  with open(path, "rt", encoding="utf-8") as f:
    content = f.read()

  return rewriter.rewrite_links(path, content, version)


def main(unused_argv):
  version = validate_flag("version")
  output_path = validate_flag("output_path")
  root_dir, toc_path = create_docs_tree(
      version=version,
      toc_path=validate_flag("toc_path"),
      narrative_docs_path=validate_flag("narrative_docs_path"),
      reference_docs_path=validate_flag("reference_docs_path"),
  )

  build_archive(
      version=version,
      root_dir=root_dir,
      toc_path=toc_path,
      output_path=output_path,
  )


if __name__ == "__main__":
  FLAGS(sys.argv)
  app.run(main)
