# Copyright 2020 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.

"""Rules for importing and registering a local JDK."""

load(":default_java_toolchain.bzl", "JVM8_TOOLCHAIN_CONFIGURATION", "default_java_toolchain")

def _detect_java_version(repository_ctx, java_bin):
    properties_out = repository_ctx.execute([java_bin, "-XshowSettings:properties"]).stderr
    # This returns an indented list of properties separated with newlines:
    # "  java.vendor.url.bug = ... \n"
    # "  java.version = 11.0.8\n"
    # "  java.version.date = 2020-11-05\"

    strip_properties = [property.strip() for property in properties_out.splitlines()]
    version_property = [property for property in strip_properties if property.startswith("java.version = ")]
    if len(version_property) != 1:
        return None

    version_value = version_property[0][len("java.version = "):]
    parts = version_value.split(".")
    major = parts[0]
    if len(parts) == 1:
        return major
    elif major == "1":  # handles versions below 1.8
        minor = parts[1]
        return minor
    return major

def local_java_runtime(name, java_home, version, runtime_name = None, visibility = ["//visibility:public"]):
    """Defines a java_runtime target together with Java runtime and compile toolchain definitions.

    Java runtime toolchain is constrained by flag --java_runtime_version having
    value set to either name or version argument.

    Java compile toolchains are created for --java_language_version flags values
    between 8 and version (inclusive). Java compile toolchains use the same
    (local) JDK for compilation. This requires a different configuration for JDK8
    than the newer versions.

    Args:
      name: name of the target.
      java_home: Path to the JDK.
      version: Version of the JDK.
      runtime_name: name of java_runtime target if it already exists.
      visibility: Visibility that will be applied to the java runtime target
    """
    if runtime_name == None:
        runtime_name = name
        native.java_runtime(
            name = runtime_name,
            java_home = java_home,
            visibility = visibility,
        )

    native.config_setting(
        name = name + "_name_setting",
        values = {"java_runtime_version": name},
        visibility = ["//visibility:private"],
    )
    native.config_setting(
        name = name + "_version_setting",
        values = {"java_runtime_version": version},
        visibility = ["//visibility:private"],
    )
    native.config_setting(
        name = name + "_name_version_setting",
        values = {"java_runtime_version": name + "_" + version},
        visibility = ["//visibility:private"],
    )
    native.alias(
        name = name + "_settings_alias",
        actual = select({
            name + "_name_setting": name + "_name_setting",
            name + "_version_setting": name + "_version_setting",
            "//conditions:default": name + "_name_version_setting",
        }),
        visibility = ["//visibility:private"],
    )
    native.toolchain(
        name = "runtime_toolchain_definition",
        target_settings = [":%s_settings_alias" % name],
        toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type",
        toolchain = runtime_name,
    )

    if version == "8":
        default_java_toolchain(
            name = name + "_toolchain_java8",
            configuration = JVM8_TOOLCHAIN_CONFIGURATION,
            source_version = version,
            target_version = version,
            java_runtime = runtime_name,
        )
    elif type(version) == type("") and version.isdigit() and int(version) > 8:
        for version in range(8, int(version) + 1):
            default_java_toolchain(
                name = name + "_toolchain_java" + str(version),
                source_version = str(version),
                target_version = str(version),
                java_runtime = runtime_name,
            )

    # else version is not recognized and no compilation toolchains are predefined

def _local_java_repository_impl(repository_ctx):
    """Repository rule local_java_repository implementation.

    Args:
      repository_ctx: repository context
    """
    java_home = repository_ctx.attr.java_home
    java_home_path = repository_ctx.path(java_home)
    if not java_home_path.exists:
        fail('The path indicated by the "java_home" attribute "%s" (absolute: "%s") ' +
             "does not exist." % (java_home, str(java_home_path)))

    repository_ctx.file(
        "WORKSPACE",
        "# DO NOT EDIT: automatically generated WORKSPACE file for local_java_repository\n" +
        "workspace(name = \"{name}\")\n".format(name = repository_ctx.name),
    )

    extension = ".exe" if repository_ctx.os.name.lower().find("windows") != -1 else ""
    java_bin = java_home_path.get_child("bin").get_child("java" + extension)

    if not java_bin.exists:
        # Java binary does not exist
        repository_ctx.file(
            "BUILD.bazel",
            _NOJDK_BUILD_TPL.format(
                local_jdk = repository_ctx.name,
                java_binary = "bin/java" + extension,
                java_home = java_home,
            ),
            False,
        )
        return

    # Detect version
    version = repository_ctx.attr.version if repository_ctx.attr.version != "" else _detect_java_version(repository_ctx, java_bin)

    # Prepare BUILD file using "local_java_runtime" macro
    build_file = ""
    if repository_ctx.attr.build_file != None:
        build_file = repository_ctx.read(repository_ctx.path(repository_ctx.attr.build_file))

    runtime_name = '"jdk"' if repository_ctx.attr.build_file else None
    local_java_runtime_macro = """
local_java_runtime(
    name = "%s",
    runtime_name = %s,
    java_home = "%s",
    version = "%s",
)
""" % (repository_ctx.name, runtime_name, java_home, version)

    repository_ctx.file(
        "BUILD.bazel",
        'load("@bazel_tools//tools/jdk:local_java_repository.bzl", "local_java_runtime")\n' +
        build_file +
        local_java_runtime_macro,
    )

    # Symlink all files
    for file in repository_ctx.path(java_home).readdir():
        repository_ctx.symlink(file, file.basename)

# Build file template, when JDK does not exist
_NOJDK_BUILD_TPL = '''load("@bazel_tools//tools/jdk:fail_rule.bzl", "fail_rule")
fail_rule(
   name = "jdk",
   header = "Auto-Configuration Error:",
   message = ("Cannot find Java binary {java_binary} in {java_home}; either correct your JAVA_HOME, " +
          "PATH or specify Java from remote repository (e.g. " +
          "--java_runtime_version=remotejdk_11")
)
config_setting(
   name = "localjdk_setting",
   values = {{"java_runtime_version": "{local_jdk}"}},
   visibility = ["//visibility:private"],
)
toolchain(
   name = "runtime_toolchain_definition",
   target_settings = [":localjdk_setting"],
   toolchain_type = "@bazel_tools//tools/jdk:runtime_toolchain_type",
   toolchain = ":jdk",
)
'''

_local_java_repository_rule = repository_rule(
    implementation = _local_java_repository_impl,
    local = True,
    configure = True,
    attrs = {
        "java_home": attr.string(),
        "version": attr.string(),
        "build_file": attr.label(),
    },
)

def local_java_repository(name, java_home, version = "", build_file = None):
    """Registers a runtime toolchain for local JDK and creates an unregistered compile toolchain.

    Toolchain resolution is constrained with --java_runtime_version flag
    having value of the "name" or "version" parameter.

    Java compile toolchains are created for --java_language_version flags values
    between 8 and version (inclusive). Java compile toolchains use the same
    (local) JDK for compilation.

    If there is no JDK "virtual" targets are created, which fail only when actually needed.

    Args:
      name: A unique name for this rule.
      java_home: Location of the JDK imported.
      build_file: optionally BUILD file template
      version: optionally java version
    """
    _local_java_repository_rule(name = name, java_home = java_home, version = version, build_file = build_file)
    native.register_toolchains("@" + name + "//:runtime_toolchain_definition")

[]