#!/usr/bin/env python3
# Copyright (C) Microsoft Corporation. All rights reserved.
# This file is distributed under the University of Illinois Open Source License.
# See LICENSE.TXT for details.
"""Generate the HLSL embedded headers ``.inc`` files.

For every header file under ``tools/clang/lib/Headers/hlsl`` that the build
system has embedded with ``embed_header.py`` (one ``.inc`` per source file
under ``<build-dir>/<rel-path>.inc``), produce two small ``.inc`` files
that are textually included by the hand-written
``tools/clang/lib/Lex/HLSLEmbeddedHeaders.cpp``:

* ``--decls-output`` receives one ``namespace <ns> { #include "<inc>" }``
  block per header, where ``<ns>`` is derived from the header's relative
  path (slashes and dots replaced with underscores) so each file's ``Data``
  StringRef variable gets its own namespace and never collides with
  another.

* ``--entries-output`` receives one ``HLSL_EMBEDDED_HEADER(rel, ns)`` line
  per header.  The ``HLSLEmbeddedHeaders.cpp`` consumer ``#define``\\s
  ``HLSL_EMBEDDED_HEADER`` before including this file, so the same list
  can be expanded for both the declarations and the StringMap inserts.

Splitting the generated content into two small ``.inc`` files keeps the
hand-written code in source control while limiting build-time generation
to data that genuinely depends on the on-disk header set.
"""

import argparse
import os
import sys


def path_to_namespace(rel_path):
    """Convert a relative header path to a valid C++ namespace identifier.

    Slashes and dots are replaced with underscores so that
    ``dx/linalg.h`` becomes ``dx_linalg_h``.
    """
    out = []
    for ch in rel_path:
        if ch in ("/", "\\", "."):
            out.append("_")
        else:
            out.append(ch)
    return "".join(out)


def normalize_rel_path(rel_path):
    """Normalize a relative path to use forward slashes and no leading ./."""
    rel_path = rel_path.replace("\\", "/")
    while rel_path.startswith("./"):
        rel_path = rel_path[2:]
    return rel_path


def write_if_different(path, text):
    if os.path.exists(path):
        try:
            with open(path, "r", encoding="utf-8") as f:
                if f.read() == text:
                    return
        except OSError:
            pass
    out_dir = os.path.dirname(path)
    if out_dir and not os.path.isdir(out_dir):
        os.makedirs(out_dir, exist_ok=True)
    with open(path, "w", encoding="utf-8") as f:
        f.write(text)


def generate(decls_output, entries_output, entries):
    """Generate the two ``.inc`` files.

    ``entries`` is an iterable of ``(rel_path, inc_path)`` pairs where
    ``rel_path`` is the path relative to ``hlsl/`` and ``inc_path`` is
    the absolute path to the ``.inc`` snippet produced by
    ``embed_header.py``.
    """
    entries = sorted(
        (normalize_rel_path(rel), inc) for rel, inc in entries)

    decls_lines = []
    decls_lines.append(
        "// Generated by utils/generate_hlsl_embedded_headers.py.")
    decls_lines.append("// Do not edit.")
    decls_lines.append("")
    for rel, inc in entries:
        ns = path_to_namespace(rel)
        # Use forward slashes in the include path so the generated file
        # is portable across host platforms.
        inc_fwd = inc.replace("\\", "/")
        decls_lines.append("namespace {} {{".format(ns))
        decls_lines.append("#include \"{}\"".format(inc_fwd))
        decls_lines.append("}} // namespace {}".format(ns))
        decls_lines.append("")

    entries_lines = []
    entries_lines.append(
        "// Generated by utils/generate_hlsl_embedded_headers.py.")
    entries_lines.append("// Do not edit.")
    entries_lines.append("")
    for rel, _ in entries:
        ns = path_to_namespace(rel)
        entries_lines.append(
            "HLSL_EMBEDDED_HEADER(\"{}\", {})".format(rel, ns))
    entries_lines.append("")

    write_if_different(decls_output, "\n".join(decls_lines))
    write_if_different(entries_output, "\n".join(entries_lines))


def main(argv):
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--decls-output", required=True,
                        help="Path to the generated declarations .inc file.")
    parser.add_argument("--entries-output", required=True,
                        help="Path to the generated map-entries .inc file.")
    parser.add_argument(
        "--entry", action="append", default=[],
        metavar="REL_PATH=INC_PATH",
        help=("Relative header path (under hlsl/) and absolute path to its "
              "embedded .inc file. May be specified multiple times."))
    args = parser.parse_args(argv)

    entries = []
    for spec in args.entry:
        if "=" not in spec:
            parser.error("--entry must be of the form REL_PATH=INC_PATH")
        rel, inc = spec.split("=", 1)
        entries.append((rel, inc))

    generate(args.decls_output, args.entries_output, entries)
    return 0


if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))
