# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.

# This script generates js/public/PrefsGenerated.h from StaticPrefList.yaml

import io

import buildconfig
import yaml
from mozbuild.preprocessor import Preprocessor

HEADER_TEMPLATE = """\
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#ifndef js_PrefsGenerated_h
#define js_PrefsGenerated_h

/* This file is generated by js/src/GeneratePrefs.py. Do not edit! */

#include "mozilla/Atomics.h"

#include <stdint.h>

%(contents)s

#endif // js_PrefsGenerated_h
"""


def load_yaml(yaml_path):
    # First invoke the preprocessor to handle #ifdefs in the YAML file.
    pp = Preprocessor()
    pp.context.update(buildconfig.defines["ALLDEFINES"])

    # To make #ifdef DEBUG work, use a similar hack as in emit_code in
    # generate_static_pref_list.py.
    if buildconfig.substs.get("MOZ_DEBUG"):
        pp.context["DEBUG"] = "1"

    pp.out = io.StringIO()
    pp.do_filter("substitution")
    pp.do_include(yaml_path)
    contents = pp.out.getvalue()
    return yaml.safe_load(contents)


# Returns the C++ type to use for the pref type from the YAML file. Always use
# the non-atomic type for return values and arguments. The field type is
# determined elsewhere.
def get_cpp_type(type):
    if type in ("bool", "RelaxedAtomicBool"):
        return "bool"
    if type in ("uint32_t", "RelaxedAtomicUint32"):
        return "uint32_t"
    if type in ("int32_t", "RelaxedAtomicInt32"):
        return "int32_t"
    raise Exception(f"Unexpected type: {type}")


# Returns a C++ expression for the default pref value. Booleans in the YAML file
# are converted to Pythonic True or False, so those need special handling.
def get_cpp_init_value(val):
    if val is True:
        return "true"
    if val is False:
        return "false"
    return str(val)


def generate_prefs_header(c_out, yaml_path):
    prefs = load_yaml(yaml_path)

    js_options_prefix = "javascript.options."

    def is_js_pref(pref):
        set_spidermonkey_pref = pref.get("set_spidermonkey_pref", False)
        if set_spidermonkey_pref not in (False, "startup", "always"):
            raise Exception("Invalid value for set_spidermonkey_pref")

        # Ignore prefs that don't have the |set_spidermonkey_pref| attribute.
        if set_spidermonkey_pref is False:
            return False

        # Only support prefs with javascript.options prefix.
        if not pref["name"].startswith(js_options_prefix):
            raise Exception("set_spidermonkey_pref only works for JS prefs")

        return True

    # Remove all non-JS prefs and sort prefs by name.
    prefs = list(filter(is_js_pref, prefs))
    prefs.sort(key=lambda pref: pref["name"])

    class_fields = []
    class_fields_inits = []

    macro_entries = []
    browser_set_statements = []
    browser_set_non_startup_statements = []

    for pref in prefs:
        name = pref["name"]
        name = name[len(js_options_prefix) :]

        is_startup_pref = pref["set_spidermonkey_pref"] == "startup"

        cpp_name = name.replace(".", "_").replace("-", "_")
        type = get_cpp_type(pref["type"])
        init_value = get_cpp_init_value(pref["value"])

        setter_name = ("setAtStartup_" if is_startup_pref else "set_") + cpp_name

        # Use a relaxed atomic for non-startup prefs because those might be changed
        # after startup.
        field_type = type
        if not is_startup_pref:
            field_type = f"mozilla::Atomic<{field_type}, mozilla::Relaxed>"
        class_fields.append(f"static {field_type} {cpp_name}_;")
        class_fields_inits.append(
            f"{field_type} JS::Prefs::{cpp_name}_{{{init_value}}};"
        )

        is_startup_pref_bool = "true" if is_startup_pref else "false"

        # Generate a MACRO invocation like this:
        #   MACRO("arraybuffer_transfer", arraybuffer_transfer, bool, setAtStartup_arraybuffer_transfer, true)
        macro_entries.append(
            f'MACRO("{name}", {cpp_name}, {type}, {setter_name}, {is_startup_pref_bool})'
        )

        # Generate a C++ statement to set the JS pref based on Gecko's StaticPrefs:
        #   JS::Prefs::setAtStartup_foo(StaticPrefs::javascript_options_foo());
        browser_pref_cpp_name = pref["name"].replace(".", "_").replace("-", "_")
        if pref.get("do_not_use_directly", False):
            browser_pref_cpp_name += "_DoNotUseDirectly"

        statement = f"JS::Prefs::{setter_name}(mozilla::StaticPrefs::{browser_pref_cpp_name}());"
        browser_set_statements.append(statement)

        # For non-startup prefs, also generate code to update the pref after startup.
        if not is_startup_pref:
            browser_set_non_startup_statements.append(statement)

    contents = ""

    contents += "#define JS_PREF_CLASS_FIELDS \\\n"
    contents += "".join(map(lambda s: f"  {s}\\\n", class_fields))
    contents += "\n\n"

    contents += "#define JS_PREF_CLASS_FIELDS_INIT \\\n"
    contents += "".join(map(lambda s: f"  {s}\\\n", class_fields_inits))
    contents += "\n\n"

    contents += "#define FOR_EACH_JS_PREF(MACRO) \\\n"
    contents += "".join(map(lambda s: f"  {s}\\\n", macro_entries))
    contents += "\n\n"

    contents += "#define SET_JS_PREFS_FROM_BROWSER_PREFS \\\n"
    contents += "".join(map(lambda s: f"  {s}\\\n", browser_set_statements))
    contents += "\n\n"

    contents += "#define SET_NON_STARTUP_JS_PREFS_FROM_BROWSER_PREFS \\\n"
    contents += "".join(map(lambda s: f"  {s}\\\n", browser_set_non_startup_statements))
    contents += "\n\n"

    c_out.write(
        HEADER_TEMPLATE
        % {
            "contents": contents,
        }
    )
