load("//tools/skylark:cc.bzl", "cc_library")
load("//tools/skylark:drake_cc.bzl", "drake_cc_library")
load("//tools/skylark:py.bzl", "py_binary", "py_library")
load("//tools/workspace:generate_file.bzl", "generate_file")

def _combine_relative_labels(arg_list, arg_map):
    # Merge the relative_labels= and relative_labels_map= arguments as seen in
    # the public macros below.  The result is a map where the arg_list items
    # are twinned into matching key:key pairs, union'd with the arg_map.  (Also
    # double-checks that the keys and values are all relative labels.)
    result = dict([(x, x) for x in arg_list])
    result.update(arg_map)
    for x in result.keys() + result.values():
        if not x.startswith(":"):
            fail("Expected relative_label, got " + x)
    return result

def drake_cc_hdrs_forwarding_library(
        name,
        relative_labels = [],
        relative_labels_map = {},
        actual_subdir = "",
        add_deprecation_warning = False,
        tags = [],
        **kwargs):
    """Generates a drake_cc_library filled with generated header files that
    merely include other header files, optionally with a deprecation warning.
    This automates the chore of adding compatibility headers when packages
    move.

    The relative_labels + relative_labels_map.keys() specify the header
    filenames to generate.  For each label name ':foo', this macro generates a
    header named 'foo.h' that includes 'drake/{actual_subdir}/{new_foo}.h'
    where new_foo for relative_labels is the same as foo, and new_foo for
    relative_labels_map is the value for the foo key (without the leading
    colon).

    When add_deprecation_warning is true, the generated header will have a
    #warning directive explaining the new location.
    """

    actual_subdir or fail("Missing required actual_subdir")

    # Generate one header file for each mapped label.
    hdrs = []
    mapping = _combine_relative_labels(relative_labels, relative_labels_map)
    for stub_relative_label, actual_relative_label in mapping.items():
        actual_include = "drake/{}/{}.h".format(
            actual_subdir,
            actual_relative_label[1:],
        )
        warning = "#warning This header is deprecated; use {} instead".format(
            actual_include,
        ) if add_deprecation_warning else ""
        basename = stub_relative_label[1:] + ".h"
        generate_file(
            name = basename,
            content = "\n".join([
                "#pragma once",
                "{warning}",
                "#include \"{actual_include}\"",
            ]).format(
                warning = warning,
                actual_include = actual_include,
            ),
        )
        hdrs.append(basename)

    # Place all of the header files into a library.
    drake_cc_library(
        name = name,
        hdrs = hdrs,
        tags = tags + ["nolint"],
        **kwargs
    )

def drake_cc_library_aliases(
        relative_labels = [],
        relative_labels_map = {},
        actual_subdir = "",
        add_deprecation_warning = False,
        tags = [],
        deps = [],
        **kwargs):
    """Generates aliases for drake_cc_library labels that have moved into a new
    package.  This is superior to native.alias() both for its brevity (it can
    generate many aliases at once), and because the deprecation message
    actually works (see https://github.com/bazelbuild/bazel/issues/5802).

    The relative_labels + relative_labels_map specify the correspondence.  For
    each label name foo in the relative_labels and relative_labels_map.keys(),
    this macro generates an alias to '//{actual_subdir}:{new_foo}' where
    new_foo for relative_labels is the same as foo, and new_foo for
    relative_labels_map is the value for the foo key.

    When add_deprecation_warning is true, the generated label in this package
    will have a deprecation warning explaining the new location.  When using
    this mode, the caller should probably set 'tags = ["manual"]' to avoid
    spurious warnings during 'bazel build //...'.
    """

    actual_subdir or fail("Missing required actual_subdir")

    mapping = _combine_relative_labels(relative_labels, relative_labels_map)
    for stub_relative_label, actual_relative_label in mapping.items():
        actual_full_label = "//" + actual_subdir + actual_relative_label
        if add_deprecation_warning:
            deprecation = ("Use the label " + actual_full_label)
        else:
            deprecation = None
        cc_library(
            name = stub_relative_label[1:],
            deps = deps + [actual_full_label],
            tags = tags + ["nolint"],
            deprecation = deprecation,
            **kwargs
        )
        native.alias(
            name = stub_relative_label[1:] + ".installed_headers",
            actual = actual_full_label + ".installed_headers",
        )

_PY_TEMPLATE_NOT_DEPRECATED = r'''"""
Warning:
    This module is an alias that will soon be deprecated.
    Please use ``{module}`` instead.
"""
from {module} import *
'''

_PY_TEMPLATE_DEPRECATED = r'''"""
Warning:
    This module is deprecated and will be removed on or around
    {deprecation_removal_date}. Please use ``{module}`` instead.
"""
from pydrake.common.deprecation import _warn_deprecated
from {module} import *

_warn_deprecated(
    "This module is deprecated and will be removed on or around "
    "{deprecation_removal_date}. Please use '{module}' instead.",
    stacklevel=3)
'''

def _strip_py_suffix(label):
    if not label.endswith("_py"):
        fail("'{}' must end with '_py'".format(label))
    return label[1:-len("_py")]

def drake_py_library_aliases(
        relative_labels = [],
        relative_labels_map = {},
        actual_subdir = "",
        add_deprecation_warning = False,
        deprecation_removal_date = None,
        tags = [],
        **kwargs):
    actual_subdir or fail("Missing required actual_subdir")
    subdir_prefix = "bindings/pydrake/"
    if not actual_subdir.startswith(subdir_prefix):
        fail("'{}' must start with '{}'".format(actual_subdir, subdir_prefix))
    if actual_subdir.endswith("/"):
        fail("'{}' must not end with '/'".format(actual_subdir))
    actual_package = actual_subdir[len("bindings/"):].replace("/", ".")
    mapping = _combine_relative_labels(relative_labels, relative_labels_map)
    for stub_relative_label, actual_relative_label in mapping.items():
        stub_file = _strip_py_suffix(stub_relative_label) + ".py"
        actual_name = _strip_py_suffix(actual_relative_label)
        actual_module = actual_package + "." + actual_name
        if add_deprecation_warning:
            if deprecation_removal_date == None:
                fail("`deprecation_removal_date` must be supplied.")
            generate_file(
                name = stub_file,
                content = _PY_TEMPLATE_DEPRECATED.format(
                    module = actual_module,
                    deprecation_removal_date = deprecation_removal_date,
                ),
            )
        else:
            generate_file(
                name = stub_file,
                content = _PY_TEMPLATE_NOT_DEPRECATED.format(
                    module = actual_module,
                ),
            )
        actual_full_label = "//" + actual_subdir + actual_relative_label
        py_library(
            name = stub_relative_label[1:],
            deps = [actual_full_label],
            srcs = [stub_file],
            tags = tags + ["nolint"],
            **kwargs
        )
