# 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/.

import os
import sys
from collections import defaultdict

import buildconfig
import yaml
from mozbuild.dirutils import ensureParentDir
from mozbuild.preprocessor import Preprocessor
from mozbuild.util import FileAvoidWrite
from six import StringIO

VALID_KEYS = {
    "name",
    "type",
    "value",
    "mirror",
    "do_not_use_directly",
    "include",
    "rust",
    "set_spidermonkey_pref",
}

# Each key is a C++ type; its value is the equivalent non-atomic C++ type.
VALID_BOOL_TYPES = {
    "bool": "bool",
    # These ones are defined in StaticPrefsBase.h.
    "RelaxedAtomicBool": "bool",
    "ReleaseAcquireAtomicBool": "bool",
    "SequentiallyConsistentAtomicBool": "bool",
}

VALID_TYPES = VALID_BOOL_TYPES.copy()
VALID_TYPES.update(
    {
        "int32_t": "int32_t",
        "uint32_t": "uint32_t",
        "float": "float",
        # These ones are defined in StaticPrefsBase.h.
        "RelaxedAtomicInt32": "int32_t",
        "RelaxedAtomicUint32": "uint32_t",
        "ReleaseAcquireAtomicInt32": "int32_t",
        "ReleaseAcquireAtomicUint32": "uint32_t",
        "SequentiallyConsistentAtomicInt32": "int32_t",
        "SequentiallyConsistentAtomicUint32": "uint32_t",
        "AtomicFloat": "float",
        "String": None,
        "DataMutexString": "nsACString",
    }
)

# Map non-atomic C++ types to equivalent Rust types.
RUST_TYPES = {
    "bool": "bool",
    "int32_t": "i32",
    "uint32_t": "u32",
    "float": "f32",
    "DataMutexString": "nsCString",
}

HEADER_LINE = (
    "// This file was generated by generate_static_pref_list.py from {input_filename}."
    " DO NOT EDIT."
)

MIRROR_TEMPLATES = {
    "never": """\
NEVER_PREF("{name}", {typ}, {value})
""",
    "once": """\
ONCE_PREF(
  "{name}",
   {base_id},
   {full_id},
  {typ}, {value}
)
""",
    "always": """\
ALWAYS_PREF(
  "{name}",
   {base_id},
   {full_id},
  {typ}, {value}
)
""",
    "always_datamutex": """\
ALWAYS_DATAMUTEX_PREF(
  "{name}",
   {base_id},
   {full_id},
  {typ}, {value}
)
""",
}

STATIC_PREFS_GROUP_H_TEMPLATE1 = """\
// Include it to gain access to StaticPrefs::{group}_*.

#ifndef mozilla_StaticPrefs_{group}_h
#define mozilla_StaticPrefs_{group}_h
"""

STATIC_PREFS_GROUP_H_TEMPLATE2 = """\
#include "mozilla/StaticPrefListBegin.h"
#include "mozilla/StaticPrefList_{group}.h"
#include "mozilla/StaticPrefListEnd.h"

#endif  // mozilla_StaticPrefs_{group}_h
"""

STATIC_PREFS_C_GETTERS_TEMPLATE = """\
extern "C" {typ} StaticPrefs_{full_id}() {{
  return mozilla::StaticPrefs::{full_id}();
}}
"""

STATIC_PREFS_C_GETTERS_NSSTRING_TEMPLATE = """\
extern "C" void StaticPrefs_{full_id}(nsACString *result) {{
  const auto preflock = mozilla::StaticPrefs::{full_id}();
  result->Append(*preflock);
}}
"""


def error(msg):
    raise ValueError(msg)


def mk_id(name):
    "Replace '.' and '-' with '_', e.g. 'foo.bar-baz' becomes 'foo_bar_baz'."
    return name.replace(".", "_").replace("-", "_")


def mk_group(pref):
    name = pref["name"]
    return mk_id(name.split(".", 1)[0])


def check_pref_list(pref_list):
    # Pref names seen so far. Used to detect any duplicates.
    seen_names = set()

    # The previous pref. Used to detect mis-ordered prefs.
    prev_pref = None

    for pref in pref_list:
        # Check all given keys are known ones.
        for key in pref:
            if key not in VALID_KEYS:
                error("invalid key `{}`".format(key))

        # 'name' must be present, valid, and in the right section.
        if "name" not in pref:
            error("missing `name` key")
        name = pref["name"]
        if type(name) != str:
            error("non-string `name` value `{}`".format(name))
        if "." not in name:
            error("`name` value `{}` lacks a '.'".format(name))
        if name in seen_names:
            error("`{}` pref is defined more than once".format(name))
        seen_names.add(name)

        # Prefs must be ordered appropriately.
        if prev_pref:
            if mk_group(prev_pref) > mk_group(pref):
                error(
                    "`{}` pref must come before `{}` pref".format(
                        name, prev_pref["name"]
                    )
                )

        # 'type' must be present and valid.
        if "type" not in pref:
            error("missing `type` key for pref `{}`".format(name))
        typ = pref["type"]
        if typ not in VALID_TYPES:
            error("invalid `type` value `{}` for pref `{}`".format(typ, name))

        # 'value' must be present and valid.
        if "value" not in pref:
            error("missing `value` key for pref `{}`".format(name))
        value = pref["value"]
        if typ == "String" or typ == "DataMutexString":
            if type(value) != str:
                error(
                    "non-string `value` value `{}` for `{}` pref `{}`; "
                    "add double quotes".format(value, typ, name)
                )
        elif typ in VALID_BOOL_TYPES:
            if value not in (True, False):
                error("invalid boolean value `{}` for pref `{}`".format(value, name))

        # 'mirror' must be present and valid.
        if "mirror" not in pref:
            error("missing `mirror` key for pref `{}`".format(name))
        mirror = pref["mirror"]
        if typ.startswith("DataMutex"):
            mirror += "_datamutex"
        if mirror not in MIRROR_TEMPLATES:
            error("invalid `mirror` value `{}` for pref `{}`".format(mirror, name))

        # Check 'do_not_use_directly' if present.
        if "do_not_use_directly" in pref:
            do_not_use_directly = pref["do_not_use_directly"]
            if type(do_not_use_directly) != bool:
                error(
                    "non-boolean `do_not_use_directly` value `{}` for pref "
                    "`{}`".format(do_not_use_directly, name)
                )
            if do_not_use_directly and mirror == "never":
                error(
                    "`do_not_use_directly` uselessly set with `mirror` value "
                    "`never` for pref `{}`".format(pref["name"])
                )

        # Check 'include' if present.
        if "include" in pref:
            include = pref["include"]
            if type(include) != str:
                error(
                    "non-string `include` value `{}` for pref `{}`".format(
                        include, name
                    )
                )
            if include.startswith("<") and not include.endswith(">"):
                error(
                    "`include` value `{}` starts with `<` but does not "
                    "end with `>` for pref `{}`".format(include, name)
                )

        # Check 'rust' if present.
        if "rust" in pref:
            rust = pref["rust"]
            if type(rust) != bool:
                error("non-boolean `rust` value `{}` for pref `{}`".format(rust, name))
            if rust and mirror == "never":
                error(
                    "`rust` uselessly set with `mirror` value `never` for "
                    "pref `{}`".format(pref["name"])
                )

        prev_pref = pref


def generate_code(pref_list, input_filename):
    check_pref_list(pref_list)

    first_line = HEADER_LINE.format(input_filename=input_filename)

    # The required includes for StaticPrefs_<group>.h.
    includes = defaultdict(set)

    # StaticPrefList_<group>.h contains all the pref definitions for this
    # group.
    static_pref_list_group_h = defaultdict(lambda: [first_line, ""])

    # StaticPrefsCGetters.cpp contains C getters for all the mirrored prefs,
    # for use by Rust code.
    static_prefs_c_getters_cpp = [first_line, ""]

    # static_prefs.rs contains C getter declarations and a macro.
    static_prefs_rs_decls = []
    static_prefs_rs_macro = []

    # Generate the per-pref code (spread across multiple files).
    for pref in pref_list:
        name = pref["name"]
        typ = pref["type"]
        value = pref["value"]
        mirror = pref["mirror"]
        do_not_use_directly = pref.get("do_not_use_directly")
        include = pref.get("include")
        rust = pref.get("rust")

        base_id = mk_id(pref["name"])
        full_id = base_id
        if mirror == "once":
            full_id += "_AtStartup"
        if do_not_use_directly:
            full_id += "_DoNotUseDirectly"
        if typ.startswith("DataMutex"):
            mirror += "_datamutex"

        group = mk_group(pref)

        if include:
            if not include.startswith("<"):
                # It's not a system header. Add double quotes.
                include = '"{}"'.format(include)
            includes[group].add(include)

        if typ == "String":
            # Quote string literals, and escape double-quote chars.
            value = '"{}"'.format(value.replace('"', '\\"'))
        elif typ == "DataMutexString":
            # Quote string literals, and escape double-quote chars.
            value = '"{}"_ns'.format(value.replace('"', '\\"'))
        elif typ in VALID_BOOL_TYPES:
            # Convert Python bools to C++ bools.
            if value is True:
                value = "true"
            elif value is False:
                value = "false"

        # Append the C++ definition to the relevant output file's code.
        static_pref_list_group_h[group].append(
            MIRROR_TEMPLATES[mirror].format(
                name=name,
                base_id=base_id,
                full_id=full_id,
                typ=typ,
                value=value,
            )
        )

        if rust:
            passed_type = VALID_TYPES[typ]
            if passed_type == "nsACString":
                # Generate the C getter.
                static_prefs_c_getters_cpp.append(
                    STATIC_PREFS_C_GETTERS_NSSTRING_TEMPLATE.format(full_id=full_id)
                )

                # Generate the C getter declaration, in Rust.
                decl = "    pub fn StaticPrefs_{full_id}(result: *mut nsstring::nsACString);"
                static_prefs_rs_decls.append(decl.format(full_id=full_id))

                # Generate the Rust macro entry.
                macro = '    ("{name}") => (unsafe {{ let mut result = $crate::nsCString::new(); $crate::StaticPrefs_{full_id}(&mut *result); result }});'
                static_prefs_rs_macro.append(macro.format(name=name, full_id=full_id))

            else:
                # Generate the C getter.
                static_prefs_c_getters_cpp.append(
                    STATIC_PREFS_C_GETTERS_TEMPLATE.format(
                        typ=passed_type, full_id=full_id
                    )
                )

                # Generate the C getter declaration, in Rust.
                decl = "    pub fn StaticPrefs_{full_id}() -> {typ};"
                static_prefs_rs_decls.append(
                    decl.format(full_id=full_id, typ=RUST_TYPES[passed_type])
                )

                # Generate the Rust macro entry.
                macro = (
                    '    ("{name}") => (unsafe {{ $crate::StaticPrefs_{full_id}() }});'
                )
                static_prefs_rs_macro.append(macro.format(name=name, full_id=full_id))

        # Delete this so that `group` can be reused below without Flake8
        # complaining.
        del group

    # StaticPrefListAll.h contains one `#include "mozilla/StaticPrefList_X.h`
    # line per pref group.
    static_pref_list_all_h = [first_line, ""]
    static_pref_list_all_h.extend(
        '#include "mozilla/StaticPrefList_{}.h"'.format(group)
        for group in sorted(static_pref_list_group_h)
    )
    static_pref_list_all_h.append("")

    # StaticPrefsAll.h contains one `#include "mozilla/StaticPrefs_X.h` line per
    # pref group.
    static_prefs_all_h = [first_line, ""]
    static_prefs_all_h.extend(
        '#include "mozilla/StaticPrefs_{}.h"'.format(group)
        for group in sorted(static_pref_list_group_h)
    )
    static_prefs_all_h.append("")

    # StaticPrefs_<group>.h wraps StaticPrefList_<group>.h. It is the header
    # used directly by application code.
    static_prefs_group_h = defaultdict(list)
    for group in sorted(static_pref_list_group_h):
        static_prefs_group_h[group] = [first_line]
        static_prefs_group_h[group].append(
            STATIC_PREFS_GROUP_H_TEMPLATE1.format(group=group)
        )
        if group in includes:
            # Add any necessary includes, from 'h_include' values.
            for include in sorted(includes[group]):
                static_prefs_group_h[group].append("#include {}".format(include))
            static_prefs_group_h[group].append("")
        static_prefs_group_h[group].append(
            STATIC_PREFS_GROUP_H_TEMPLATE2.format(group=group)
        )

    # static_prefs.rs contains the Rust macro getters.
    static_prefs_rs = [first_line, "", "pub use nsstring::nsCString;", 'extern "C" {']
    static_prefs_rs.extend(static_prefs_rs_decls)
    static_prefs_rs.extend(["}", "", "#[macro_export]", "macro_rules! pref {"])
    static_prefs_rs.extend(static_prefs_rs_macro)
    static_prefs_rs.extend(["}", ""])

    def fold(lines):
        return "\n".join(lines)

    return {
        "static_pref_list_all_h": fold(static_pref_list_all_h),
        "static_prefs_all_h": fold(static_prefs_all_h),
        "static_pref_list_group_h": {
            k: fold(v) for k, v in static_pref_list_group_h.items()
        },
        "static_prefs_group_h": {k: fold(v) for k, v in static_prefs_group_h.items()},
        "static_prefs_c_getters_cpp": fold(static_prefs_c_getters_cpp),
        "static_prefs_rs": fold(static_prefs_rs),
    }


def emit_code(fd, pref_list_filename):
    pp = Preprocessor()
    pp.context.update(buildconfig.defines["ALLDEFINES"])

    # A necessary hack until MOZ_DEBUG_FLAGS are part of buildconfig.defines.
    if buildconfig.substs.get("MOZ_DEBUG"):
        pp.context["DEBUG"] = "1"

    if buildconfig.substs.get("TARGET_CPU") == "aarch64":
        pp.context["MOZ_AARCH64"] = True

    if buildconfig.substs.get("MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS"):
        pp.context["MOZ_ANDROID_CONTENT_SERVICE_ISOLATED_PROCESS"] = True

    pp.out = StringIO()
    pp.do_filter("substitution")
    pp.do_include(pref_list_filename)

    try:
        pref_list = yaml.safe_load(pp.out.getvalue())
        input_file = os.path.relpath(
            pref_list_filename,
            os.environ.get("GECKO_PATH", os.environ.get("TOPSRCDIR")),
        )
        code = generate_code(pref_list, input_file)
    except (IOError, ValueError) as e:
        print("{}: error:\n  {}\n".format(pref_list_filename, e))
        sys.exit(1)

    # When generating multiple files from a script, the build system treats the
    # first named output file (StaticPrefListAll.h in this case) specially -- it
    # is created elsewhere, and written to via `fd`.
    fd.write(code["static_pref_list_all_h"])

    # We must create the remaining output files ourselves. This requires
    # creating the output directory directly if it doesn't already exist.
    ensureParentDir(fd.name)
    init_dirname = os.path.dirname(fd.name)

    with FileAvoidWrite("StaticPrefsAll.h") as fd:
        fd.write(code["static_prefs_all_h"])

    for group, text in sorted(code["static_pref_list_group_h"].items()):
        filename = "StaticPrefList_{}.h".format(group)
        with FileAvoidWrite(os.path.join(init_dirname, filename)) as fd:
            fd.write(text)

    for group, text in sorted(code["static_prefs_group_h"].items()):
        filename = "StaticPrefs_{}.h".format(group)
        with FileAvoidWrite(filename) as fd:
            fd.write(text)

    with FileAvoidWrite(os.path.join(init_dirname, "StaticPrefsCGetters.cpp")) as fd:
        fd.write(code["static_prefs_c_getters_cpp"])

    with FileAvoidWrite("static_prefs.rs") as fd:
        fd.write(code["static_prefs_rs"])
