#!/usr/bin/env python3
# Copyright (C) 2018 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0

import json_parser
import posixpath
import re
import sys
from typing import Optional, Set
from textwrap import dedent
import os

from special_case_helper import SpecialCaseHandler
from helper import (
    map_qt_library,
    featureName,
    map_platform,
    find_3rd_party_library_mapping,
    generate_find_package_info,
    get_compile_test_dependent_library_mapping,
)

knownTests = set()  # type: Set[str]


class LibraryMapping:
    def __init__(self, package: str, resultVariable: str, appendFoundSuffix: bool = True) -> None:
        self.package = package
        self.resultVariable = resultVariable
        self.appendFoundSuffix = appendFoundSuffix


def map_tests(test: str) -> Optional[str]:
    testmap = {
        "c99": "c_std_99 IN_LIST CMAKE_C_COMPILE_FEATURES",
        "c11": "c_std_11 IN_LIST CMAKE_C_COMPILE_FEATURES",
        "x86SimdAlways": "ON",  # FIXME: Make this actually do a compile test.
        "aesni": "TEST_subarch_aesni",
        "avx": "TEST_subarch_avx",
        "avx2": "TEST_subarch_avx2",
        "avx512f": "TEST_subarch_avx512f",
        "avx512cd": "TEST_subarch_avx512cd",
        "avx512dq": "TEST_subarch_avx512dq",
        "avx512bw": "TEST_subarch_avx512bw",
        "avx512er": "TEST_subarch_avx512er",
        "avx512pf": "TEST_subarch_avx512pf",
        "avx512vl": "TEST_subarch_avx512vl",
        "avx512ifma": "TEST_subarch_avx512ifma",
        "avx512vbmi": "TEST_subarch_avx512vbmi",
        "avx512vbmi2": "TEST_subarch_avx512vbmi2",
        "avx512vpopcntdq": "TEST_subarch_avx512vpopcntdq",
        "avx5124fmaps": "TEST_subarch_avx5124fmaps",
        "avx5124vnniw": "TEST_subarch_avx5124vnniw",
        "bmi": "TEST_subarch_bmi",
        "bmi2": "TEST_subarch_bmi2",
        "cx16": "TEST_subarch_cx16",
        "f16c": "TEST_subarch_f16c",
        "fma": "TEST_subarch_fma",
        "fma4": "TEST_subarch_fma4",
        "fsgsbase": "TEST_subarch_fsgsbase",
        "gfni": "TEST_subarch_gfni",
        "ibt": "TEST_subarch_ibt",
        "libclang": "TEST_libclang",
        "lwp": "TEST_subarch_lwp",
        "lzcnt": "TEST_subarch_lzcnt",
        "mmx": "TEST_subarch_mmx",
        "movbe": "TEST_subarch_movbe",
        "mpx": "TEST_subarch_mpx",
        "no-sahf": "TEST_subarch_no_shaf",
        "pclmul": "TEST_subarch_pclmul",
        "popcnt": "TEST_subarch_popcnt",
        "prefetchwt1": "TEST_subarch_prefetchwt1",
        "prfchw": "TEST_subarch_prfchw",
        "pdpid": "TEST_subarch_rdpid",
        "rdpid": "TEST_subarch_rdpid",
        "rdseed": "TEST_subarch_rdseed",
        "rdrnd": "TEST_subarch_rdrnd",
        "rtm": "TEST_subarch_rtm",
        "shani": "TEST_subarch_shani",
        "shstk": "TEST_subarch_shstk",
        "sse2": "TEST_subarch_sse2",
        "sse3": "TEST_subarch_sse3",
        "ssse3": "TEST_subarch_ssse3",
        "sse4a": "TEST_subarch_sse4a",
        "sse4_1": "TEST_subarch_sse4_1",
        "sse4_2": "TEST_subarch_sse4_2",
        "tbm": "TEST_subarch_tbm",
        "xop": "TEST_subarch_xop",
        "neon": "TEST_subarch_neon",
        "iwmmxt": "TEST_subarch_iwmmxt",
        "crc32": "TEST_subarch_crc32",
        "vis": "TEST_subarch_vis",
        "vis2": "TEST_subarch_vis2",
        "vis3": "TEST_subarch_vis3",
        "dsp": "TEST_subarch_dsp",
        "dspr2": "TEST_subarch_dspr2",
        "altivec": "TEST_subarch_altivec",
        "spe": "TEST_subarch_spe",
        "vsx": "TEST_subarch_vsx",
        "openssl11": '(OPENSSL_VERSION VERSION_GREATER_EQUAL "1.1.0")',
        "libinput_axis_api": "ON",
        "xlib": "X11_FOUND",
        "wayland-scanner": "WaylandScanner_FOUND",
        "3rdparty-hunspell": "VKB_HAVE_3RDPARTY_HUNSPELL",
        "t9write-alphabetic": "VKB_HAVE_T9WRITE_ALPHA",
        "t9write-cjk": "VKB_HAVE_T9WRITE_CJK",
    }
    if test in testmap:
        return testmap.get(test, None)
    if test in knownTests:
        return f"TEST_{featureName(test)}"
    return None


def cm(ctx, *output):
    txt = ctx["output"]
    if txt != "" and not txt.endswith("\n"):
        txt += "\n"
    txt += "\n".join(output)

    ctx["output"] = txt
    return ctx


def readJsonFromDir(path: str) -> str:
    path = posixpath.join(path, "configure.json")

    print(f"Reading {path}...")
    assert posixpath.exists(path)

    parser = json_parser.QMakeSpecificJSONParser()
    return parser.parse(path)


def processFiles(ctx, data):
    print("  files:")
    if "files" in data:
        for (k, v) in data["files"].items():
            ctx[k] = v
    return ctx


def parseLib(ctx, lib, data, cm_fh, cmake_find_packages_set):
    newlib = find_3rd_party_library_mapping(lib)
    if not newlib:
        print(f'    XXXX Unknown library "{lib}".')
        return

    if newlib.packageName is None:
        print(f'    **** Skipping library "{lib}" -- was masked.')
        return

    print(f"    mapped library {lib} to {newlib.targetName}.")

    # Avoid duplicate find_package calls.
    if newlib.targetName in cmake_find_packages_set:
        return

    # If certain libraries are used within a feature, but the feature
    # is only emitted conditionally with a simple condition (like
    # 'on Windows' or 'on Linux'), we should enclose the find_package
    # call for the library into the same condition.
    emit_if = newlib.emit_if

    # Only look through features if a custom emit_if wasn't provided.
    if not emit_if:
        for feature in data["features"]:
            feature_data = data["features"][feature]
            if (
                "condition" in feature_data
                and f"libs.{lib}" in feature_data["condition"]
                and "emitIf" in feature_data
                and "config." in feature_data["emitIf"]
            ):
                emit_if = feature_data["emitIf"]
                break

    if emit_if:
        emit_if = map_condition(emit_if)

    cmake_find_packages_set.add(newlib.targetName)

    find_package_kwargs = {"emit_if": emit_if}
    if newlib.is_bundled_with_qt:
        # If a library is bundled with Qt, it has 2 FindFoo.cmake
        # modules: WrapFoo and WrapSystemFoo.
        # FindWrapSystemFoo.cmake will try to find the 'Foo' library in
        # the usual CMake locations, and will create a
        # WrapSystemFoo::WrapSystemFoo target pointing to the library.
        #
        # FindWrapFoo.cmake will create a WrapFoo::WrapFoo target which
        # will link either against the WrapSystemFoo or QtBundledFoo
        # target depending on certain feature values.
        #
        # Because the following qt_find_package call is for
        # configure.cmake consumption, we make the assumption that
        # configure.cmake is interested in finding the system library
        # for the purpose of enabling or disabling a system_foo feature.
        find_package_kwargs["use_system_package_name"] = True
    find_package_kwargs["module"] = ctx["module"]

    cm_fh.write(generate_find_package_info(newlib, **find_package_kwargs))

    if "use" in data["libraries"][lib]:
        use_entry = data["libraries"][lib]["use"]
        if isinstance(use_entry, str):
            print(f"1use: {use_entry}")
            cm_fh.write(f"qt_add_qmake_lib_dependency({newlib.soName} {use_entry})\n")
        else:
            for use in use_entry:
                print(f"2use: {use}")
                indentation = ""
                has_condition = False
                if "condition" in use:
                    has_condition = True
                    indentation = "    "
                    condition = map_condition(use["condition"])
                    cm_fh.write(f"if({condition})\n")
                cm_fh.write(
                    f"{indentation}qt_add_qmake_lib_dependency({newlib.soName} {use['lib']})\n"
                )
                if has_condition:
                    cm_fh.write("endif()\n")

    run_library_test = False
    mapped_library = find_3rd_party_library_mapping(lib)
    if mapped_library:
        run_library_test = mapped_library.run_library_test

    if run_library_test and "test" in data["libraries"][lib]:
        test = data["libraries"][lib]["test"]
        write_compile_test(
            ctx, lib, test, data, cm_fh, manual_library_list=[lib], is_library_test=True
        )


def lineify(label, value, quote=True):
    if value:
        if quote:
            escaped_value = value.replace('"', '\\"')
            return f'    {label} "{escaped_value}"\n'
        return f"    {label} {value}\n"
    return ""


def map_condition(condition):
    # Handle NOT:
    if isinstance(condition, list):
        condition = "(" + ") AND (".join(condition) + ")"
    if isinstance(condition, bool):
        if condition:
            return "ON"
        else:
            return "OFF"
    assert isinstance(condition, str)

    mapped_features = {"gbm": "gbm_FOUND"}

    # Turn foo != "bar" into (NOT foo STREQUAL 'bar')
    condition = re.sub(r"([^ ]+)\s*!=\s*('.*?')", "(! \\1 == \\2)", condition)
    # Turn foo != 156 into (NOT foo EQUAL 156)
    condition = re.sub(r"([^ ]+)\s*!=\s*([0-9]?)", "(! \\1 EQUAL \\2)", condition)

    condition = condition.replace("!", "NOT ")
    condition = condition.replace("&&", " AND ")
    condition = condition.replace("||", " OR ")
    condition = condition.replace("==", " STREQUAL ")

    # explicitly handle input.sdk == '':
    condition = re.sub(r"input\.sdk\s*==\s*''", "NOT INPUT_SDK", condition)

    last_pos = 0
    mapped_condition = ""
    has_failed = False
    for match in re.finditer(r"([a-zA-Z0-9_]+)\.([a-zA-Z0-9_+-]+)", condition):
        substitution = None
        # appendFoundSuffix = True
        if match.group(1) == "libs":
            libmapping = find_3rd_party_library_mapping(match.group(2))

            if libmapping and libmapping.packageName:
                substitution = libmapping.packageName
                if libmapping.resultVariable:
                    substitution = libmapping.resultVariable
                if libmapping.appendFoundSuffix:
                    substitution += "_FOUND"

                # Assume that feature conditions are interested whether
                # a system library is found, rather than the bundled one
                # which we always know we can build.
                if libmapping.is_bundled_with_qt:
                    substitution = substitution.replace("Wrap", "WrapSystem")

        elif match.group(1) == "features":
            feature = match.group(2)
            if feature in mapped_features:
                substitution = mapped_features.get(feature)
            else:
                substitution = f"QT_FEATURE_{featureName(match.group(2))}"

        elif match.group(1) == "subarch":
            substitution = f"TEST_arch_{'${TEST_architecture_arch}'}_subarch_{match.group(2)}"

        elif match.group(1) == "call":
            if match.group(2) == "crossCompile":
                substitution = "CMAKE_CROSSCOMPILING"

        elif match.group(1) == "tests":
            substitution = map_tests(match.group(2))

        elif match.group(1) == "input":
            substitution = f"INPUT_{featureName(match.group(2))}"

        elif match.group(1) == "config":
            substitution = map_platform(match.group(2))
        elif match.group(1) == "module":
            substitution = f"TARGET {map_qt_library(match.group(2))}"

        elif match.group(1) == "arch":
            if match.group(2) == "i386":
                # FIXME: Does this make sense?
                substitution = "(TEST_architecture_arch STREQUAL i386)"
            elif match.group(2) == "x86_64":
                substitution = "(TEST_architecture_arch STREQUAL x86_64)"
            elif match.group(2) == "arm":
                # FIXME: Does this make sense?
                substitution = "(TEST_architecture_arch STREQUAL arm)"
            elif match.group(2) == "arm64":
                # FIXME: Does this make sense?
                substitution = "(TEST_architecture_arch STREQUAL arm64)"
            elif match.group(2) == "mips":
                # FIXME: Does this make sense?
                substitution = "(TEST_architecture_arch STREQUAL mips)"

        if substitution is None:
            print(f'    XXXX Unknown condition "{match.group(0)}"')
            has_failed = True
        else:
            mapped_condition += condition[last_pos : match.start(1)] + substitution
            last_pos = match.end(2)

    mapped_condition += condition[last_pos:]

    # Space out '(' and ')':
    mapped_condition = mapped_condition.replace("(", " ( ")
    mapped_condition = mapped_condition.replace(")", " ) ")

    # Prettify:
    condition = re.sub("\\s+", " ", mapped_condition)
    condition = condition.strip()

    # Special case for WrapLibClang in qttools
    condition = condition.replace("TEST_libclang.has_clangcpp", "TEST_libclang")

    if has_failed:
        condition += " OR FIXME"

    return condition


def parseInput(ctx, sinput, data, cm_fh):
    skip_inputs = {
        "prefix",
        "hostprefix",
        "extprefix",
        "archdatadir",
        "bindir",
        "datadir",
        "docdir",
        "examplesdir",
        "external-hostbindir",
        "headerdir",
        "hostbindir",
        "hostdatadir",
        "hostlibdir",
        "importdir",
        "libdir",
        "libexecdir",
        "plugindir",
        "qmldir",
        "settingsdir",
        "sysconfdir",
        "testsdir",
        "translationdir",
        "android-arch",
        "android-ndk",
        "android-ndk-host",
        "android-ndk-platform",
        "android-sdk",
        "android-toolchain-version",
        "android-style-assets",
        "appstore-compliant",
        "avx",
        "avx2",
        "avx512",
        "c++std",
        "ccache",
        "commercial",
        "confirm-license",
        "dbus",
        "dbus-runtime",
        "debug",
        "debug-and-release",
        "developer-build",
        "device",
        "device-option",
        "f16c",
        "force-asserts",
        "force-debug-info",
        "force-pkg-config",
        "framework",
        "gc-binaries",
        "gdb-index",
        "gcc-sysroot",
        "gcov",
        "gnumake",
        "gui",
        "headersclean",
        "incredibuild-xge",
        "libudev",
        "ltcg",
        "make",
        "make-tool",
        "mips_dsp",
        "mips_dspr2",
        "mp",
        "nomake",
        "opensource",
        "optimize-debug",
        "optimize-size",
        "optimized-qmake",
        "optimized-tools",
        "pch",
        "pkg-config",
        "platform",
        "plugin-manifests",
        "profile",
        "qreal",
        "reduce-exports",
        "reduce-relocations",
        "release",
        "rpath",
        "sanitize",
        "sdk",
        "separate-debug-info",
        "shared",
        "silent",
        "qdbus",
        "sse2",
        "sse3",
        "sse4.1",
        "sse4.2",
        "ssse3",
        "static",
        "static-runtime",
        "strip",
        "syncqt",
        "sysroot",
        "testcocoon",
        "use-gold-linker",
        "warnings-are-errors",
        "Werror",
        "widgets",
        "xplatform",
        "zlib",
        "eventfd",
        "glib",
        "icu",
        "inotify",
        "journald",
        "pcre",
        "posix-ipc",
        "pps",
        "slog2",
        "syslog",
    }

    if sinput in skip_inputs:
        print(f"    **** Skipping input {sinput}: masked.")
        return

    dtype = data
    if isinstance(data, dict):
        dtype = data["type"]

    if dtype == "boolean":
        print(f"    **** Skipping boolean input {sinput}: masked.")
        return

    if dtype == "enum":
        values_line = " ".join(data["values"])
        cm_fh.write(f"# input {sinput}\n")
        cm_fh.write(f'set(INPUT_{featureName(sinput)} "undefined" CACHE STRING "")\n')
        cm_fh.write(
            f"set_property(CACHE INPUT_{featureName(sinput)} PROPERTY STRINGS undefined {values_line})\n\n"
        )
        return

    print(f"    XXXX UNHANDLED INPUT TYPE {dtype} in input description")
    return


def get_library_usage_for_compile_test(library):
    result = {}
    mapped_library = find_3rd_party_library_mapping(library)
    if not mapped_library:
        result["fixme"] = f"# FIXME: use: unmapped library: {library}\n"
        return result

    if mapped_library.test_library_overwrite:
        target_name = mapped_library.test_library_overwrite
    else:
        target_name = mapped_library.targetName
    result["target_name"] = target_name
    result["package_name"] = mapped_library.packageName
    result["extra"] = mapped_library.extra
    return result


# Handles config.test/foo/foo.pro projects.
def write_standalone_compile_test(cm_fh, ctx, data, config_test_name, is_library_test):
    rel_test_project_path = f"{ctx['test_dir']}/{config_test_name}"
    if posixpath.exists(f"{ctx['project_dir']}/{rel_test_project_path}/CMakeLists.txt"):
        label = ""
        libraries = []
        packages = []

        if "label" in data:
            label = data["label"]

        if is_library_test and config_test_name in data["libraries"]:
            if "label" in data["libraries"][config_test_name]:
                label = data["libraries"][config_test_name]["label"]

            # If a library entry in configure.json has a test, and
            # the test uses a config.tests standalone project, we
            # need to get the package and target info for the
            # library, and pass it to the test so compiling and
            # linking succeeds.
            library_usage = get_library_usage_for_compile_test(config_test_name)
            if "target_name" in library_usage:
                libraries.append(library_usage["target_name"])
            if "package_name" in library_usage:
                find_package_arguments = []
                find_package_arguments.append(library_usage["package_name"])
                if "extra" in library_usage:
                    find_package_arguments.extend(library_usage["extra"])
                package_line = "PACKAGE " + " ".join(find_package_arguments)
                packages.append(package_line)

        cm_fh.write(
            f"""
qt_config_compile_test("{config_test_name}"
                   LABEL "{label}"
                   PROJECT_PATH "${{CMAKE_CURRENT_SOURCE_DIR}}/{rel_test_project_path}"
"""
        )
        if libraries:
            libraries_string = " ".join(libraries)
            cm_fh.write(f"                   LIBRARIES {libraries_string}\n")
        if packages:
            packages_string = " ".join(packages)
            cm_fh.write(f"                   PACKAGES {packages_string}")
        cm_fh.write(")\n")


def write_compile_test(
    ctx, name, details, data, cm_fh, manual_library_list=None, is_library_test=False
):

    if manual_library_list is None:
        manual_library_list = []

    inherited_test_name = details["inherit"] if "inherit" in details else None
    inherit_details = None
    if inherited_test_name and is_library_test:
        inherit_details = data["libraries"][inherited_test_name]["test"]
        if not inherit_details:
            print(f"    XXXX Failed to locate inherited library test {inherited_test_name}")

    if isinstance(details, str):
        write_standalone_compile_test(cm_fh, ctx, data, details, is_library_test)
        return

    def resolve_head(detail):
        head = detail.get("head")
        if isinstance(head, list):
            head = "\n".join(head)
        return head + "\n" if head else ""

    head = ""
    if inherit_details:
        head += resolve_head(inherit_details)
    head += resolve_head(details)

    sourceCode = head

    def resolve_include(detail, keyword):
        include = detail.get(keyword, "")
        if isinstance(include, list):
            include = "#include <" + ">\n#include <".join(include) + ">\n"
        elif include:
            include = f"#include <{include}>\n"
        return include

    include = ""
    if is_library_test:
        if inherit_details:
            inherited_lib_data = data["libraries"][inherited_test_name]
            include += resolve_include(inherited_lib_data, "headers")
        this_lib_data = data["libraries"][name]
        include += resolve_include(this_lib_data, "headers")
    else:
        if inherit_details:
            include += resolve_include(inherit_details, "include")
        include += resolve_include(details, "include")

    sourceCode += include

    def resolve_tail(detail):
        tail = detail.get("tail")
        if isinstance(tail, list):
            tail = "\n".join(tail)
        return tail + "\n" if tail else ""

    tail = ""
    if inherit_details:
        tail += resolve_tail(inherit_details)
    tail += resolve_tail(details)

    sourceCode += tail

    if sourceCode:  # blank line before main
        sourceCode += "\n"
    sourceCode += "int main(void)\n"
    sourceCode += "{\n"
    sourceCode += "    /* BEGIN TEST: */\n"

    def resolve_main(detail):
        main = detail.get("main")
        if isinstance(main, list):
            main = "\n".join(main)
        return main + "\n" if main else ""

    main = ""
    if inherit_details:
        main += resolve_main(inherit_details)
    main += resolve_main(details)

    sourceCode += main

    sourceCode += "    /* END TEST: */\n"
    sourceCode += "    return 0;\n"
    sourceCode += "}\n"

    sourceCode = sourceCode.replace('"', '\\"')

    librariesCmakeName = ""
    languageStandard = ""
    compileOptions = ""
    qmakeFixme = ""

    cm_fh.write(f"# {name}\n")

    if "qmake" in details:  # We don't really have many so we can just enumerate them all
        if details["qmake"] == "unix:LIBS += -lpthread":
            librariesCmakeName = format(featureName(name)) + "_TEST_LIBRARIES"
            cm_fh.write("if (UNIX)\n")
            cm_fh.write("    set(" + librariesCmakeName + " pthread)\n")
            cm_fh.write("endif()\n")
        elif details["qmake"] == "linux: LIBS += -lpthread -lrt":
            librariesCmakeName = format(featureName(name)) + "_TEST_LIBRARIES"
            cm_fh.write("if (LINUX)\n")
            cm_fh.write("    set(" + librariesCmakeName + " pthread rt)\n")
            cm_fh.write("endif()\n")
        elif details["qmake"] == "!winrt: LIBS += runtimeobject.lib":
            librariesCmakeName = format(featureName(name)) + "_TEST_LIBRARIES"
            cm_fh.write("if (NOT WINRT)\n")
            cm_fh.write("    set(" + librariesCmakeName + " runtimeobject)\n")
            cm_fh.write("endif()\n")
        elif details["qmake"] == "CONFIG += c++11":
            # do nothing we're always in c++11 mode
            pass
        elif details["qmake"] == "CONFIG += c++11 c++14":
            languageStandard = "CXX_STANDARD 14"
        elif details["qmake"] == "CONFIG += c++11 c++14 c++17":
            languageStandard = "CXX_STANDARD 17"
        elif details["qmake"] == "CONFIG += c++11 c++14 c++17 c++20":
            languageStandard = "CXX_STANDARD 20"
        elif details["qmake"] == "QMAKE_CXXFLAGS += -fstack-protector-strong":
            compileOptions = details["qmake"][18:]
        else:
            qmakeFixme = f"# FIXME: qmake: {details['qmake']}\n"

    library_list = []
    test_libraries = manual_library_list

    if "use" in data:
        test_libraries += data["use"].split(" ")

    for library in test_libraries:
        if len(library) == 0:
            continue

        adjusted_library = get_compile_test_dependent_library_mapping(name, library)
        library_usage = get_library_usage_for_compile_test(adjusted_library)
        if "fixme" in library_usage:
            qmakeFixme += library_usage["fixme"]
            continue
        else:
            library_list.append(library_usage["target_name"])

    cm_fh.write(f"qt_config_compile_test({featureName(name)}\n")
    cm_fh.write(lineify("LABEL", data.get("label", "")))
    if librariesCmakeName != "" or len(library_list) != 0:
        cm_fh.write("    LIBRARIES\n")
        if librariesCmakeName != "":
            cm_fh.write(lineify("", "${" + librariesCmakeName + "}"))
        if len(library_list) != 0:
            cm_fh.write("        ")
            cm_fh.write("\n        ".join(library_list))
            cm_fh.write("\n")
    if compileOptions != "":
        cm_fh.write(f"    COMPILE_OPTIONS {compileOptions}\n")
    cm_fh.write("    CODE\n")
    cm_fh.write('"' + sourceCode + '"')
    if qmakeFixme != "":
        cm_fh.write(qmakeFixme)
    if languageStandard != "":
        cm_fh.write(f"\n    {languageStandard}\n")
    cm_fh.write(")\n\n")


#  "tests": {
#        "cxx11_future": {
#            "label": "C++11 <future>",
#            "type": "compile",
#            "test": {
#                "include": "future",
#                "main": [
#                    "std::future<int> f = std::async([]() { return 42; });",
#                    "(void)f.get();"
#                ],
#                "qmake": "unix:LIBS += -lpthread"
#            }
#        },


def write_compiler_supports_flag_test(
    ctx, name, details, data, cm_fh, manual_library_list=None, is_library_test=False
):
    cm_fh.write(f"qt_config_compiler_supports_flag_test({featureName(name)}\n")
    cm_fh.write(lineify("LABEL", data.get("label", "")))
    cm_fh.write(lineify("FLAG", data.get("flag", "")))
    cm_fh.write(")\n\n")


def write_linker_supports_flag_test(
    ctx, name, details, data, cm_fh, manual_library_list=None, is_library_test=False
):
    cm_fh.write(f"qt_config_linker_supports_flag_test({featureName(name)}\n")
    cm_fh.write(lineify("LABEL", data.get("label", "")))
    cm_fh.write(lineify("FLAG", data.get("flag", "")))
    cm_fh.write(")\n\n")


def parseTest(ctx, test, data, cm_fh):
    skip_tests = {
        "c11",
        "c99",
        "gc_binaries",
        "precomile_header",
        "reduce_exports",
        "gc_binaries",
        "libinput_axis_api",
        "wayland-scanner",
        "xlib",
    }

    if test in skip_tests:
        print(f"    **** Skipping features {test}: masked.")
        return

    if data["type"] == "compile":
        knownTests.add(test)

        if "test" in data:
            details = data["test"]
        else:
            details = test

        write_compile_test(ctx, test, details, data, cm_fh)

    if data["type"] == "compilerSupportsFlag":
        knownTests.add(test)

        if "test" in data:
            details = data["test"]
        else:
            details = test

        write_compiler_supports_flag_test(ctx, test, details, data, cm_fh)

    if data["type"] == "linkerSupportsFlag":
        knownTests.add(test)

        if "test" in data:
            details = data["test"]
        else:
            details = test

        write_linker_supports_flag_test(ctx, test, details, data, cm_fh)

    elif data["type"] == "libclang":
        knownTests.add(test)

        cm_fh.write(f"# {test}\n")
        lib_clang_lib = find_3rd_party_library_mapping("libclang")
        cm_fh.write(generate_find_package_info(lib_clang_lib))
        cm_fh.write(
            dedent(
                """
        if(TARGET WrapLibClang::WrapLibClang)
            set(TEST_libclang "ON" CACHE BOOL "Required libclang version found." FORCE)
        endif()
        """
            )
        )
        cm_fh.write("\n")

    elif data["type"] == "x86Simd":
        knownTests.add(test)

        label = data["label"]

        cm_fh.write(f"# {test}\n")
        cm_fh.write(f'qt_config_compile_test_x86simd({test} "{label}")\n')
        cm_fh.write("\n")

    elif data["type"] == "machineTuple":
        knownTests.add(test)

        label = data["label"]

        cm_fh.write(f"# {test}\n")
        cm_fh.write(f'qt_config_compile_test_machine_tuple("{label}")\n')
        cm_fh.write("\n")

    #    "features": {
    #        "android-style-assets": {
    #            "label": "Android Style Assets",
    #            "condition": "config.android",
    #            "output": [ "privateFeature" ],
    #            "comment": "This belongs into gui, but the license check needs it here already."
    #        },
    else:
        print(f"    XXXX UNHANDLED TEST TYPE {data['type']} in test description")


def get_feature_mapping():
    # This is *before* the feature name gets normalized! So keep - and + chars, etc.
    feature_mapping = {
        "alloc_h": None,  # handled by alloc target
        "alloc_malloc_h": None,
        "alloc_stdlib_h": None,
        "build_all": None,
        "ccache": {"autoDetect": "1", "condition": "QT_USE_CCACHE"},
        "compiler-flags": None,
        "cross_compile": {"condition": "CMAKE_CROSSCOMPILING"},
        "debug_and_release": {
            "autoDetect": "1",  # Setting this to None has weird effects...
            "condition": "QT_GENERATOR_IS_MULTI_CONFIG",
        },
        "debug": {
            "autoDetect": "ON",
            "condition": "CMAKE_BUILD_TYPE STREQUAL Debug OR Debug IN_LIST CMAKE_CONFIGURATION_TYPES",
        },
        "dlopen": {"condition": "UNIX"},
        "force_debug_info": {
            "autoDetect": "CMAKE_BUILD_TYPE STREQUAL RelWithDebInfo OR RelWithDebInfo IN_LIST CMAKE_CONFIGURATION_TYPES"
        },
        "framework": {
            "condition": "APPLE AND BUILD_SHARED_LIBS AND NOT CMAKE_BUILD_TYPE STREQUAL Debug"
        },
        "gc_binaries": {"condition": "NOT QT_FEATURE_shared"},
        "gcc-sysroot": None,
        "gcov": None,
        "GNUmake": None,
        "host-dbus": None,
        "iconv": {
            "condition": "NOT QT_FEATURE_icu AND QT_FEATURE_textcodec AND NOT WIN32 AND NOT QNX AND NOT ANDROID AND NOT APPLE AND WrapIconv_FOUND",
        },
        "incredibuild_xge": None,
        "ltcg": {
            "autoDetect": "ON",
            "cmakePrelude": """set(__qt_ltcg_detected FALSE)
if(CMAKE_INTERPROCEDURAL_OPTIMIZATION)
    set(__qt_ltcg_detected TRUE)
else()
    foreach(config ${CMAKE_BUILD_TYPE} ${CMAKE_CONFIGURATION_TYPES})
        string(TOUPPER "${config}" __qt_uc_config)
        if(CMAKE_INTERPROCEDURAL_OPTIMIZATION_${__qt_uc_config})
            set(__qt_ltcg_detected TRUE)
            break()
        endif()
    endforeach()
    unset(__qt_uc_config)
endif()""",
            "condition": "__qt_ltcg_detected",
        },
        "msvc_mp": None,
        "simulator_and_device": {"condition": "UIKIT AND NOT QT_UIKIT_SDK"},
        "pkg-config": {"condition": "PKG_CONFIG_FOUND"},
        "precompile_header": {"condition": "BUILD_WITH_PCH"},
        "profile": None,
        "qmakeargs": None,
        "qpa_default_platform": None,  # Not a bool!
        "qreal": {
            "condition": 'DEFINED QT_COORD_TYPE AND NOT QT_COORD_TYPE STREQUAL "double"',
            "output": [
                {
                    "type": "define",
                    "name": "QT_COORD_TYPE",
                    "value": "${QT_COORD_TYPE}",
                },
                {
                    "type": "define",
                    "name": "QT_COORD_TYPE_STRING",
                    "value": '\\"${QT_COORD_TYPE}\\"',
                },
            ],
        },
        "reduce_exports": {
            "condition": "NOT MSVC",
        },
        "release": None,
        "release_tools": None,
        "rpath": {
            "autoDetect": "1",
            "condition": "BUILD_SHARED_LIBS AND UNIX AND NOT WIN32 AND NOT ANDROID",
        },
        "shared": {
            "condition": "BUILD_SHARED_LIBS",
            "output": [
                "publicFeature",
                "publicQtConfig",
                "publicConfig",
                {
                    "type": "define",
                    "name": "QT_STATIC",
                    "prerequisite": "!defined(QT_SHARED) && !defined(QT_STATIC)",
                    "negative": True,
                },
            ],
        },
        "silent": None,
        "sql-sqlite": {"condition": "QT_FEATURE_datestring"},
        "stl": None,  # Do we really need to test for this in 2018?!
        "strip": None,
        "verifyspec": None,  # qmake specific...
        "warnings_are_errors": None,  # FIXME: Do we need these?
        "xkbcommon-system": None,  # another system library, just named a bit different from the rest
    }
    return feature_mapping


def parseFeature(ctx, feature, data, cm_fh):
    feature_mapping = get_feature_mapping()
    mapping = feature_mapping.get(feature, {})

    if mapping is None:
        print(f"    **** Skipping features {feature}: masked.")
        return

    handled = {
        "autoDetect",
        "comment",
        "condition",
        "description",
        "disable",
        "emitIf",
        "enable",
        "label",
        "output",
        "purpose",
        "section",
    }
    label = mapping.get("label", data.get("label", ""))
    purpose = mapping.get("purpose", data.get("purpose", data.get("description", label)))
    autoDetect = map_condition(mapping.get("autoDetect", data.get("autoDetect", "")))
    condition = map_condition(mapping.get("condition", data.get("condition", "")))
    output = mapping.get("output", data.get("output", []))
    comment = mapping.get("comment", data.get("comment", ""))
    section = mapping.get("section", data.get("section", ""))
    enable = map_condition(mapping.get("enable", data.get("enable", "")))
    disable = map_condition(mapping.get("disable", data.get("disable", "")))
    emitIf = map_condition(mapping.get("emitIf", data.get("emitIf", "")))
    cmakePrelude = mapping.get("cmakePrelude", None)
    cmakeEpilogue = mapping.get("cmakeEpilogue", None)

    for k in [k for k in data.keys() if k not in handled]:
        print(f"    XXXX UNHANDLED KEY {k} in feature description")

    if not output:
        # feature that is only used in the conditions of other features
        output = ["internalFeature"]

    publicFeature = False  # #define QT_FEATURE_featurename in public header
    privateFeature = False  # #define QT_FEATURE_featurename in private header
    negativeFeature = False  # #define QT_NO_featurename in public header
    internalFeature = False  # No custom or QT_FEATURE_ defines
    publicDefine = False  # #define MY_CUSTOM_DEFINE in public header
    publicConfig = False  # add to CONFIG in public pri file
    privateConfig = False  # add to CONFIG in private pri file
    publicQtConfig = False  # add to QT_CONFIG in public pri file

    for o in output:
        outputType = o
        if isinstance(o, dict):
            outputType = o["type"]

        if outputType in [
            "varAssign",
            "varAppend",
            "varRemove",
            "useBFDLinker",
            "useGoldLinker",
            "useLLDLinker",
        ]:
            continue
        elif outputType == "define":
            publicDefine = True
        elif outputType == "feature":
            negativeFeature = True
        elif outputType == "publicFeature":
            publicFeature = True
        elif outputType == "privateFeature":
            privateFeature = True
        elif outputType == "internalFeature":
            internalFeature = True
        elif outputType == "publicConfig":
            publicConfig = True
        elif outputType == "privateConfig":
            privateConfig = True
        elif outputType == "publicQtConfig":
            publicQtConfig = True
        else:
            print(f"    XXXX UNHANDLED OUTPUT TYPE {outputType} in feature {feature}.")
            continue

    if not any(
        [
            publicFeature,
            privateFeature,
            internalFeature,
            publicDefine,
            negativeFeature,
            publicConfig,
            privateConfig,
            publicQtConfig,
        ]
    ):
        print(f"    **** Skipping feature {feature}: Not relevant for C++.")
        return

    normalized_feature_name = featureName(feature)

    def writeFeature(
        name,
        publicFeature=False,
        privateFeature=False,
        labelAppend="",
        superFeature=None,
        autoDetect="",
        cmakePrelude=None,
        cmakeEpilogue=None,
    ):
        if comment:
            cm_fh.write(f"# {comment}\n")

        if cmakePrelude is not None:
            cm_fh.write(cmakePrelude)
            cm_fh.write("\n")

        cm_fh.write(f'qt_feature("{name}"')
        if publicFeature:
            cm_fh.write(" PUBLIC")
        if privateFeature:
            cm_fh.write(" PRIVATE")
        cm_fh.write("\n")

        cm_fh.write(lineify("SECTION", section))
        cm_fh.write(lineify("LABEL", label + labelAppend))
        if purpose != label:
            cm_fh.write(lineify("PURPOSE", purpose))
        cm_fh.write(lineify("AUTODETECT", autoDetect, quote=False))
        if superFeature:
            feature_condition = f"QT_FEATURE_{superFeature}"
        else:
            feature_condition = condition
        cm_fh.write(lineify("CONDITION", feature_condition, quote=False))
        cm_fh.write(lineify("ENABLE", enable, quote=False))
        cm_fh.write(lineify("DISABLE", disable, quote=False))
        cm_fh.write(lineify("EMIT_IF", emitIf, quote=False))
        cm_fh.write(")\n")

        if cmakeEpilogue is not None:
            cm_fh.write(cmakeEpilogue)
            cm_fh.write("\n")

    # Write qt_feature() calls before any qt_feature_definition() calls

    # Default internal feature case.
    featureCalls = {}
    featureCalls[feature] = {
        "name": feature,
        "labelAppend": "",
        "autoDetect": autoDetect,
        "cmakePrelude": cmakePrelude,
        "cmakeEpilogue": cmakeEpilogue,
    }

    # Go over all outputs to compute the number of features that have to be declared
    for o in output:
        outputType = o
        name = feature

        # The label append is to provide a unique label for features that have more than one output
        # with different names.
        labelAppend = ""

        if isinstance(o, dict):
            outputType = o["type"]
            if "name" in o:
                name = o["name"]
                labelAppend = f": {o['name']}"

        if outputType not in ["feature", "publicFeature", "privateFeature"]:
            continue
        if name not in featureCalls:
            featureCalls[name] = {"name": name, "labelAppend": labelAppend}

        if name != feature:
            featureCalls[name]["superFeature"] = normalized_feature_name

        if outputType in ["feature", "publicFeature"]:
            featureCalls[name]["publicFeature"] = True
        elif outputType == "privateFeature":
            featureCalls[name]["privateFeature"] = True
        elif outputType == "publicConfig":
            featureCalls[name]["publicConfig"] = True
        elif outputType == "privateConfig":
            featureCalls[name]["privateConfig"] = True
        elif outputType == "publicQtConfig":
            featureCalls[name]["publicQtConfig"] = True

    # Write the qt_feature() calls from the computed feature map
    for _, args in featureCalls.items():
        writeFeature(**args)

    # Write qt_feature_definition() calls
    for o in output:
        outputType = o
        outputArgs = {}
        if isinstance(o, dict):
            outputType = o["type"]
            outputArgs = o

        # Map negative feature to define:
        if outputType == "feature":
            outputType = "define"
            outputArgs = {
                "name": f"QT_NO_{normalized_feature_name.upper()}",
                "negative": True,
                "value": 1,
                "type": "define",
            }

        if outputType != "define":
            continue

        if outputArgs.get("name") is None:
            print(f"    XXXX DEFINE output without name in feature {feature}.")
            continue

        out_name = outputArgs.get("name")
        cm_fh.write(f'qt_feature_definition("{feature}" "{out_name}"')
        if outputArgs.get("negative", False):
            cm_fh.write(" NEGATE")
        if outputArgs.get("value") is not None:
            cm_fh.write(f' VALUE "{outputArgs.get("value")}"')
        if outputArgs.get("prerequisite") is not None:
            cm_fh.write(f' PREREQUISITE "{outputArgs.get("prerequisite")}"')
        cm_fh.write(")\n")

    # Write qt_feature_config() calls
    for o in output:
        outputType = o
        name = feature
        modified_name = name

        outputArgs = {}
        if isinstance(o, dict):
            outputType = o["type"]
            outputArgs = o
            if "name" in o:
                modified_name = o["name"]

        if outputType not in ["publicConfig", "privateConfig", "publicQtConfig"]:
            continue

        config_type = ""
        if outputType == "publicConfig":
            config_type = "QMAKE_PUBLIC_CONFIG"
        elif outputType == "privateConfig":
            config_type = "QMAKE_PRIVATE_CONFIG"
        elif outputType == "publicQtConfig":
            config_type = "QMAKE_PUBLIC_QT_CONFIG"

        if not config_type:
            print("    XXXX config output without type in feature {}.".format(feature))
            continue

        cm_fh.write('qt_feature_config("{}" {}'.format(name, config_type))
        if outputArgs.get("negative", False):
            cm_fh.write("\n    NEGATE")
        if modified_name != name:
            cm_fh.write("\n")
            cm_fh.write(lineify("NAME", modified_name, quote=True))

        cm_fh.write(")\n")


def processSummaryHelper(ctx, entries, cm_fh):
    for entry in entries:
        if isinstance(entry, str):
            name = entry
            cm_fh.write(f'qt_configure_add_summary_entry(ARGS "{name}")\n')
        elif "type" in entry and entry["type"] in [
            "feature",
            "firstAvailableFeature",
            "featureList",
        ]:
            function_args = []
            entry_type = entry["type"]

            if entry_type in ["firstAvailableFeature", "featureList"]:
                feature_mapping = get_feature_mapping()
                unhandled_feature = False
                for feature_name, value in feature_mapping.items():
                    # Skip entries that mention a feature which is
                    # skipped by configurejson2cmake in the feature
                    # mapping. This is not ideal, but prevents errors at
                    # CMake configuration time.
                    if not value and f"{feature_name}" in entry["args"]:
                        unhandled_feature = True
                        break

                if unhandled_feature:
                    print(f"    XXXX UNHANDLED FEATURE in SUMMARY TYPE {entry}.")
                    continue

            if entry_type != "feature":
                function_args.append(lineify("TYPE", entry_type))
            if "args" in entry:
                args = entry["args"]
                function_args.append(lineify("ARGS", args))
            if "message" in entry:
                message = entry["message"]
                function_args.append(lineify("MESSAGE", message))
            if "condition" in entry:
                condition = map_condition(entry["condition"])
                function_args.append(lineify("CONDITION", condition, quote=False))
            entry_args_string = "".join(function_args)
            cm_fh.write(f"qt_configure_add_summary_entry(\n{entry_args_string})\n")
        elif "type" in entry and entry["type"] == "buildTypeAndConfig":
            cm_fh.write("qt_configure_add_summary_build_type_and_config()\n")
        elif "type" in entry and entry["type"] == "buildMode":
            message = entry["message"]
            cm_fh.write(f"qt_configure_add_summary_build_mode({message})\n")
        elif "type" in entry and entry["type"] == "buildParts":
            message = entry["message"]
            cm_fh.write(f'qt_configure_add_summary_build_parts("{message}")\n')
        elif "section" in entry:
            section = entry["section"]
            cm_fh.write(f'qt_configure_add_summary_section(NAME "{section}")\n')
            processSummaryHelper(ctx, entry["entries"], cm_fh)
            cm_fh.write(f'qt_configure_end_summary_section() # end of "{section}" section\n')
        else:
            print(f"    XXXX UNHANDLED SUMMARY TYPE {entry}.")


report_condition_mapping = {
    "(features.rpath || features.rpath_dir) && !features.shared": "(features.rpath || QT_EXTRA_RPATHS) && !features.shared",
    "(features.rpath || features.rpath_dir) && var.QMAKE_LFLAGS_RPATH == ''": None,
}


def processReportHelper(ctx, entries, cm_fh):
    feature_mapping = get_feature_mapping()

    for entry in entries:
        if isinstance(entry, dict):
            entry_args = []
            if "type" not in entry:
                print(f"    XXXX UNHANDLED REPORT TYPE missing type in {entry}.")
                continue

            report_type = entry["type"]
            if report_type not in ["note", "warning", "error"]:
                print(f"    XXXX UNHANDLED REPORT TYPE unknown type in {entry}.")
                continue

            report_type = report_type.upper()
            entry_args.append(lineify("TYPE", report_type, quote=False))
            message = entry["message"]

            # Replace semicolons, qt_parse_all_arguments can't handle
            # them due to an escaping bug in CMake regarding escaping
            # macro arguments.
            # https://gitlab.kitware.com/cmake/cmake/issues/19972
            message = message.replace(";", ",")

            entry_args.append(lineify("MESSAGE", message))
            # Need to overhaul everything to fix conditions.
            if "condition" in entry:
                condition = entry["condition"]

                unhandled_condition = False
                for feature_name, value in feature_mapping.items():
                    # Skip reports that mention a feature which is
                    # skipped by configurejson2cmake in the feature
                    # mapping. This is not ideal, but prevents errors at
                    # CMake configuration time.
                    if not value and f"features.{feature_name}" in condition:
                        unhandled_condition = True
                        break

                if unhandled_condition:
                    print(f"    XXXX UNHANDLED CONDITION in REPORT TYPE {entry}.")
                    continue

                if isinstance(condition, str) and condition in report_condition_mapping:
                    new_condition = report_condition_mapping[condition]
                    if new_condition is None:
                        continue
                    else:
                        condition = new_condition
                condition = map_condition(condition)
                entry_args.append(lineify("CONDITION", condition, quote=False))
            entry_args_string = "".join(entry_args)
            cm_fh.write(f"qt_configure_add_report_entry(\n{entry_args_string})\n")
        else:
            print(f"    XXXX UNHANDLED REPORT TYPE {entry}.")


def parseCommandLineCustomHandler(ctx, data, cm_fh):
    cm_fh.write(f"qt_commandline_custom({data})\n")


def parseCommandLineOptions(ctx, data, cm_fh):
    for key in data:
        args = [key]
        option = data[key]
        if isinstance(option, str):
            args += ["TYPE", option]
        else:
            if "type" in option:
                args += ["TYPE", option["type"]]
            if "name" in option:
                args += ["NAME", option["name"]]
            if "value" in option:
                args += ["VALUE", option["value"]]
            if "values" in option:
                values = option["values"]
                if isinstance(values, list):
                    args += ["VALUES", " ".join(option["values"])]
                else:
                    args += ["MAPPING"]
                    for lhs in values:
                        args += [lhs, values[lhs]]

        cm_fh.write(f"qt_commandline_option({' '.join(args)})\n")


def parseCommandLinePrefixes(ctx, data, cm_fh):
    for key in data:
        cm_fh.write(f"qt_commandline_prefix({key} {data[key]})\n")


def processCommandLine(ctx, data, cm_fh):
    print("  commandline:")

    if "subconfigs" in data:
        for subconf in data["subconfigs"]:
            cm_fh.write(f"qt_commandline_subconfig({subconf})\n")

    if "commandline" not in data:
        return

    commandLine = data["commandline"]
    if "custom" in commandLine:
        print("    custom:")
        parseCommandLineCustomHandler(ctx, commandLine["custom"], cm_fh)
    if "options" in commandLine:
        print("    options:")
        parseCommandLineOptions(ctx, commandLine["options"], cm_fh)
    if "prefix" in commandLine:
        print("    prefix:")
        parseCommandLinePrefixes(ctx, commandLine["prefix"], cm_fh)
    if "assignments" in commandLine:
        print("    assignments are ignored")


def processInputs(ctx, data, cm_fh):
    print("  inputs:")
    if "commandline" not in data:
        return

    commandLine = data["commandline"]
    if "options" not in commandLine:
        return

    for input_option in commandLine["options"]:
        parseInput(ctx, input_option, commandLine["options"][input_option], cm_fh)


def processTests(ctx, data, cm_fh):
    print("  tests:")
    if "tests" not in data:
        return

    for test in data["tests"]:
        parseTest(ctx, test, data["tests"][test], cm_fh)


def processFeatures(ctx, data, cm_fh):
    print("  features:")
    if "features" not in data:
        return

    for feature in data["features"]:
        parseFeature(ctx, feature, data["features"][feature], cm_fh)


def processLibraries(ctx, data, cm_fh):
    cmake_find_packages_set = set()
    print("  libraries:")
    if "libraries" not in data:
        return

    for lib in data["libraries"]:
        parseLib(ctx, lib, data, cm_fh, cmake_find_packages_set)


def processReports(ctx, data, cm_fh):
    if "summary" in data:
        print("  summary:")
        processSummaryHelper(ctx, data["summary"], cm_fh)
    if "report" in data:
        print("  report:")
        processReportHelper(ctx, data["report"], cm_fh)
    if "earlyReport" in data:
        print("  earlyReport:")
        processReportHelper(ctx, data["earlyReport"], cm_fh)


def processSubconfigs(path, ctx, data):
    assert ctx is not None
    if "subconfigs" in data:
        for subconf in data["subconfigs"]:
            subconfDir = posixpath.join(path, subconf)
            subconfData = readJsonFromDir(subconfDir)
            subconfCtx = ctx
            processJson(subconfDir, subconfCtx, subconfData)


class special_cased_file:
    def __init__(self, base_dir: str, file_name: str, skip_special_case_preservation: bool):
        self.base_dir = base_dir
        self.file_path = posixpath.join(base_dir, file_name)
        self.gen_file_path = self.file_path + ".gen"
        self.preserve_special_cases = not skip_special_case_preservation

    def __enter__(self):
        self.file = open(self.gen_file_path, "w")
        if self.preserve_special_cases:
            self.sc_handler = SpecialCaseHandler(
                os.path.abspath(self.file_path),
                os.path.abspath(self.gen_file_path),
                os.path.abspath(self.base_dir),
                debug=False,
            )
        return self.file

    def __exit__(self, type, value, trace_back):
        self.file.close()
        if self.preserve_special_cases:
            self.sc_handler.handle_special_cases()
        os.replace(self.gen_file_path, self.file_path)


def processJson(path, ctx, data, skip_special_case_preservation=False):
    ctx["project_dir"] = path
    ctx["module"] = data.get("module", "global")
    ctx["test_dir"] = data.get("testDir", "config.tests")

    ctx = processFiles(ctx, data)

    with special_cased_file(path, "qt_cmdline.cmake", skip_special_case_preservation) as cm_fh:
        processCommandLine(ctx, data, cm_fh)

    with special_cased_file(path, "configure.cmake", skip_special_case_preservation) as cm_fh:
        cm_fh.write("\n\n#### Inputs\n\n")

        processInputs(ctx, data, cm_fh)

        cm_fh.write("\n\n#### Libraries\n\n")

        processLibraries(ctx, data, cm_fh)

        cm_fh.write("\n\n#### Tests\n\n")

        processTests(ctx, data, cm_fh)

        cm_fh.write("\n\n#### Features\n\n")

        processFeatures(ctx, data, cm_fh)

        processReports(ctx, data, cm_fh)

        if ctx.get("module") == "global":
            cm_fh.write(
                '\nqt_extra_definition("QT_VERSION_STR" "\\"${PROJECT_VERSION}\\"" PUBLIC)\n'
            )
            cm_fh.write('qt_extra_definition("QT_VERSION_MAJOR" ${PROJECT_VERSION_MAJOR} PUBLIC)\n')
            cm_fh.write('qt_extra_definition("QT_VERSION_MINOR" ${PROJECT_VERSION_MINOR} PUBLIC)\n')
            cm_fh.write('qt_extra_definition("QT_VERSION_PATCH" ${PROJECT_VERSION_PATCH} PUBLIC)\n')

    # do this late:
    processSubconfigs(path, ctx, data)


def main():
    if len(sys.argv) < 2:
        print("This scripts needs one directory to process!")
        quit(1)

    directory = sys.argv[1]
    skip_special_case_preservation = "-s" in sys.argv[2:]

    print(f"Processing: {directory}.")

    data = readJsonFromDir(directory)
    processJson(directory, {}, data, skip_special_case_preservation=skip_special_case_preservation)


if __name__ == "__main__":
    main()