load(":common/python/semantics.bzl", "TOOLS_REPO")

_CcInfo = _builtins.toplevel.CcInfo

# NOTE: This is copied to PyRuntimeInfo.java
DEFAULT_STUB_SHEBANG = "#!/usr/bin/env python3"

# NOTE: This is copied to PyRuntimeInfo.java
DEFAULT_BOOTSTRAP_TEMPLATE = "@" + TOOLS_REPO + "//tools/python:python_bootstrap_template.txt"
_PYTHON_VERSION_VALUES = ["PY2", "PY3"]

def _PyRuntimeInfo_init(
        *,
        interpreter_path = None,
        interpreter = None,
        files = None,
        coverage_tool = None,
        coverage_files = None,
        python_version,
        stub_shebang = None,
        bootstrap_template = None):
    if (interpreter_path and interpreter) or (not interpreter_path and not interpreter):
        fail("exactly one of interpreter or interpreter_path must be specified")

    if interpreter_path and files != None:
        fail("cannot specify 'files' if 'interpreter_path' is given")

    if (coverage_tool and not coverage_files) or (not coverage_tool and coverage_files):
        fail(
            "coverage_tool and coverage_files must both be set or neither must be set, " +
            "got coverage_tool={}, coverage_files={}".format(
                coverage_tool,
                coverage_files,
            ),
        )

    if python_version not in _PYTHON_VERSION_VALUES:
        fail("invalid python_version: '{}'; must be one of {}".format(
            python_version,
            _PYTHON_VERSION_VALUES,
        ))

    if files != None and type(files) != type(depset()):
        fail("invalid files: got value of type {}, want depset".format(type(files)))

    if interpreter:
        if files == None:
            files = depset()
    else:
        files = None

    if coverage_files == None:
        coverage_files = depset()

    if not stub_shebang:
        stub_shebang = DEFAULT_STUB_SHEBANG

    return {
        "interpreter_path": interpreter_path,
        "interpreter": interpreter,
        "files": files,
        "coverage_tool": coverage_tool,
        "coverage_files": coverage_files,
        "python_version": python_version,
        "stub_shebang": stub_shebang,
        "bootstrap_template": bootstrap_template,
    }

# TODO(#15897): Rename this to PyRuntimeInfo when we're ready to replace the Java
# implemented provider with the Starlark one.
PyRuntimeInfo, _unused_raw_py_runtime_info_ctor = provider(
    doc = """Contains information about a Python runtime, as returned by the `py_runtime`
rule.

A Python runtime describes either a *platform runtime* or an *in-build runtime*.
A platform runtime accesses a system-installed interpreter at a known path,
whereas an in-build runtime points to a `File` that acts as the interpreter. In
both cases, an "interpreter" is really any executable binary or wrapper script
that is capable of running a Python script passed on the command line, following
the same conventions as the standard CPython interpreter.
""",
    init = _PyRuntimeInfo_init,
    fields = {
        "interpreter_path": (
            "If this is a platform runtime, this field is the absolute " +
            "filesystem path to the interpreter on the target platform. " +
            "Otherwise, this is `None`."
        ),
        "interpreter": (
            "If this is an in-build runtime, this field is a `File` representing " +
            "the interpreter. Otherwise, this is `None`. Note that an in-build " +
            "runtime can use either a prebuilt, checked-in interpreter or an " +
            "interpreter built from source."
        ),
        "files": (
            "If this is an in-build runtime, this field is a `depset` of `File`s" +
            "that need to be added to the runfiles of an executable target that " +
            "uses this runtime (in particular, files needed by `interpreter`). " +
            "The value of `interpreter` need not be included in this field. If " +
            "this is a platform runtime then this field is `None`."
        ),
        "coverage_tool": (
            "If set, this field is a `File` representing tool used for collecting code coverage information from python tests. Otherwise, this is `None`."
        ),
        "coverage_files": (
            "The files required at runtime for using `coverage_tool`. " +
            "Will be `None` if no `coverage_tool` was provided."
        ),
        "python_version": (
            "Indicates whether this runtime uses Python major version 2 or 3. " +
            "Valid values are (only) `\"PY2\"` and " +
            "`\"PY3\"`."
        ),
        "stub_shebang": (
            "\"Shebang\" expression prepended to the bootstrapping Python stub " +
            "script used when executing `py_binary` targets.  Does not " +
            "apply to Windows."
        ),
        "bootstrap_template": (
            "See py_runtime_rule.bzl%py_runtime.bootstrap_template for docs."
        ),
    },
)

def _check_arg_type(name, required_type, value):
    value_type = type(value)
    if value_type != required_type:
        fail("parameter '{}' got value of type '{}', want '{}'".format(
            name,
            value_type,
            required_type,
        ))

def _PyInfo_init(
        *,
        transitive_sources,
        uses_shared_libraries = False,
        imports = depset(),
        has_py2_only_sources = False,
        has_py3_only_sources = False):
    _check_arg_type("transitive_sources", "depset", transitive_sources)

    # Verify it's postorder compatible, but retain is original ordering.
    depset(transitive = [transitive_sources], order = "postorder")

    _check_arg_type("uses_shared_libraries", "bool", uses_shared_libraries)
    _check_arg_type("imports", "depset", imports)
    _check_arg_type("has_py2_only_sources", "bool", has_py2_only_sources)
    _check_arg_type("has_py3_only_sources", "bool", has_py3_only_sources)
    return {
        "transitive_sources": transitive_sources,
        "imports": imports,
        "uses_shared_libraries": uses_shared_libraries,
        "has_py2_only_sources": has_py2_only_sources,
        "has_py3_only_sources": has_py2_only_sources,
    }

PyInfo, _unused_raw_py_info_ctor = provider(
    "Encapsulates information provided by the Python rules.",
    init = _PyInfo_init,
    fields = {
        "transitive_sources": """\
A (`postorder`-compatible) depset of `.py` files appearing in the target's
`srcs` and the `srcs` of the target's transitive `deps`.
""",
        "uses_shared_libraries": """
Whether any of this target's transitive `deps` has a shared library file (such
as a `.so` file).

This field is currently unused in Bazel and may go away in the future.
""",
        "imports": """\
A depset of import path strings to be added to the `PYTHONPATH` of executable
Python targets. These are accumulated from the transitive `deps`.
The order of the depset is not guaranteed and may be changed in the future. It
is recommended to use `default` order (the default).
""",
        "has_py2_only_sources": "Whether any of this target's transitive sources requires a Python 2 runtime.",
        "has_py3_only_sources": "Whether any of this target's transitive sources requires a Python 3 runtime.",
    },
)

def _PyCcLinkParamsProvider_init(cc_info):
    return {
        "cc_info": _CcInfo(linking_context = cc_info.linking_context),
    }

# buildifier: disable=name-conventions
PyCcLinkParamsProvider, _unused_raw_py_cc_link_params_provider_ctor = provider(
    doc = ("Python-wrapper to forward CcInfo.linking_context. This is to " +
           "allow Python targets to propagate C++ linking information, but " +
           "without the Python target appearing to be a valid C++ rule dependency"),
    init = _PyCcLinkParamsProvider_init,
    fields = {
        "cc_info": "A CcInfo instance; it has only linking_context set",
    },
)
