# 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 buildconfig
import mozpack.path as mozpath
import string
import sys

SERVO_PROPS = mozpath.join(buildconfig.topsrcdir, "servo", "components", "style", "properties")
sys.path.insert(0, SERVO_PROPS)

import data


def props_and_deps():
    properties = data.PropertiesData(engine="gecko")
    # Add all relevant files into the dependencies of the generated file.
    return (
        properties,
        set([
          mozpath.join(SERVO_PROPS, f) for f in ["data.py", "counted_unknown_properties.py", "longhands.toml", "shorthands.toml"]
        ])
    )


def gen_css_prop_list_header(output):
    properties, deps = props_and_deps()

    output.write(
        """/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 https://mozilla.org/MPL/2.0/. */

#ifndef CSS_PROP_LONGHAND
#define CSS_PROP_LONGHAND(name_, id_, method_, flags_, pref_) /* nothing */
#define DEFINED_CSS_PROP_LONGHAND
#endif

#ifndef CSS_PROP_SHORTHAND
#define CSS_PROP_SHORTHAND(name_, id_, method_, flags_, pref_) /* nothing */
#define DEFINED_CSS_PROP_SHORTHAND
#endif

#ifndef CSS_PROP_ALIAS
#define CSS_PROP_ALIAS(name_, aliasid_, id_, method_, flags_, pref_) /* nothing */
#define DEFINED_CSS_PROP_ALIAS
#endif

"""
    )

    MACRO_NAMES = {
        "longhand": "CSS_PROP_LONGHAND",
        "shorthand": "CSS_PROP_SHORTHAND",
        "alias": "CSS_PROP_ALIAS",
    }
    for prop in properties.all_properties_and_aliases():
        flags = " | ".join(
            "CSSPropFlags::{}".format(flag)
            for flag in cpp_flags(prop)
        )
        if not flags:
            flags = "CSSPropFlags(0)"
        pref = '"' + (prop.gecko_pref or "") + '"'
        method = prop.idl_method
        if prop.type() == "alias":
            params = [prop.name, prop.ident, prop.original.ident, method, flags, pref]
        else:
            params = [prop.name, prop.ident, method, flags, pref]
        output.write("{}({})\n".format(MACRO_NAMES[prop.type()], ", ".join(params)))

    output.write(
        """
#ifdef DEFINED_CSS_PROP_ALIAS
#undef CSS_PROP_ALIAS
#undef DEFINED_CSS_PROP_ALIAS
#endif

#ifdef DEFINED_CSS_PROP_SHORTHAND
#undef CSS_PROP_SHORTHAND
#undef DEFINED_CSS_PROP_SHORTHAND
#endif

#ifdef DEFINED_CSS_PROP_LONGHAND
#undef CSS_PROP_LONGHAND
#undef DEFINED_CSS_PROP_LONGHAND
#endif
"""
    )

    return deps


# Generates a line of WebIDL with the given spelling of the property name
# (whether camelCase, _underscorePrefixed, etc.) and the given array of
# extended attributes.
def generateLine(propName, extendedAttrs):
    return "  [%s] attribute [LegacyNullToEmptyString] UTF8String %s;\n" % (
        ", ".join(extendedAttrs),
        propName,
    )


def idl_attribute(p):
    prop = p.idl_method
    # Generate a name with camelCase spelling of property-name (or capitalized,
    # for Moz-prefixed properties):
    if not prop.startswith("Moz"):
        prop = prop[0].lower() + prop[1:]
    return prop


def gen_webidl(output, ruleType, interfaceName, bindingTemplate, pref=None):
    properties, deps = props_and_deps()
    output.write(
        """/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 https://mozilla.org/MPL/2.0/. */

[Exposed=Window"""
    )
    if pref:
        output.write(', Pref="' + pref + '"')
    output.write(
        """]
interface """
        + interfaceName
        + " : CSSStyleDeclaration {\n"
    )
    for p in properties.all_properties_and_aliases():
        # Skip properties which aren't valid in style rules.
        if ruleType not in p.rule_types_allowed_names():
            continue

        pref = p.gecko_pref

        propId = p.ident
        if p.type() == "alias":
            if p.idl_method == "MozAppearance":
                # Hide MozAppearance from CSSStyleProperties to prevent outdated
                # special casing against Gecko. (Bug 1977489)
                pref = "layout.css.moz-appearance.webidl.enabled"
            elif p.gecko_pref == p.original.gecko_pref:
                # We already added this as a BindingAlias for the original prop.
                continue
            propId = p.original.ident
        extendedAttrs = [
            "BindingTemplate=(%s, eCSSProperty_%s)" % (bindingTemplate, propId),
            "CEReactions",
            "SetterThrows",
            "SetterNeedsSubjectPrincipal=NonSystem",
        ]

        if pref:
            assert not is_internal(p)
            # backdrop-filter is a special case where we want WebIDL to check a
            # function instead of checking the pref directly.
            if p.name == "backdrop-filter":
                extendedAttrs.append('Func="nsCSSProps::IsBackdropFilterAvailable"')
            else:
                extendedAttrs.append('Pref="%s"' % pref)
        elif p.explicitly_enabled_in_chrome():
            extendedAttrs.append("ChromeOnly")
        elif is_internal(p):
            continue

        def add_extra_accessors(p):
            # webkit properties get a camelcase "webkitFoo" accessor
            # as well as a capitalized "WebkitFoo" alias (added here).
            if p.idl_method.startswith("Webkit"):
                extendedAttrs.append('BindingAlias="%s"' % p.idl_method)

            prop = idl_attribute(p)

            # Per spec, what's actually supposed to happen here is that we're supposed
            # to have properties for:
            #
            # 1) Each supported CSS property name, camelCased.
            # 2) Each supported name that contains or starts with dashes,
            #    without any changes to the name.
            # 3) cssFloat
            #
            # Note that "float" will cause a property called "float" to exist due to (1)
            # in that list.
            #
            # In practice, cssFloat is the only case in which "name" doesn't contain
            # "-" but also doesn't match "prop".  So the generateLine() call will
            # cover (3) and all of (1) except "float".  If we now add an alias
            # for all the cases where "name" doesn't match "prop", that will cover
            # "float" and (2).
            if prop != p.name:
                extendedAttrs.append('BindingAlias="%s"' % p.name)

            return prop

        prop = add_extra_accessors(p)

        if p.type() != "alias":
            for a in p.aliases:
                if p.gecko_pref == a.gecko_pref:
                    newProp = add_extra_accessors(a)
                    extendedAttrs.append('BindingAlias="%s"' % newProp)

        output.write(generateLine(prop, extendedAttrs))

    output.write("};")
    return deps


def gen_style_properties_webidl(output):
    return gen_webidl(output, "style", "CSSStyleProperties", "CSS2Property")


def gen_page_descriptors_webidl(output):
    return gen_webidl(output, "page", "CSSPageDescriptors", "CSSPageDescriptor")


def gen_position_try_descriptors_webidl(output):
    return gen_webidl(output, "position-try", "CSSPositionTryDescriptors",
                      "CSSPositionTryDescriptor",
                      "layout.css.anchor-positioning.enabled")



class PropertyWrapper(object):
    __slots__ = ["index", "prop", "idlname"]

    def __init__(self, index, prop):
        self.index = index
        self.prop = prop
        self.idlname = None if is_internal(prop) else idl_attribute(prop)

    def __getattr__(self, name):
        return getattr(self.prop, name)


# See bug 1454823 for situation of internal flag.
def is_internal(prop):
    # A property which is not controlled by pref and not enabled in
    # content by default is an internal property.
    return not prop.gecko_pref and not prop.enabled_in_content()


# TODO(emilio): Get this to zero.
LONGHANDS_NOT_SERIALIZED_WITH_SERVO = set([
    # These resolve auto to zero in a few cases, but not all.
    "max-height",
    "max-width",
    "min-height",
    "min-width",

    # resistfingerprinting stuff.
    "-moz-osx-font-smoothing",

    # Layout dependent.
    "width",
    "height",
    "grid-template-rows",
    "grid-template-columns",
    "perspective-origin",
    "transform-origin",
    "transform",
    "-webkit-transform",
    "top",
    "right",
    "bottom",
    "left",
    "margin-top",
    "margin-right",
    "margin-bottom",
    "margin-left",
    "padding-top",
    "padding-right",
    "padding-bottom",
    "padding-left",
])


def serialized_by_servo(prop):
    if prop.type() == "alias":
        return True # Doesn't matter, we resolve the alias early
    return prop.name not in LONGHANDS_NOT_SERIALIZED_WITH_SERVO


def exposed_on_getcs(prop):
    if "style" not in prop.rule_types_allowed_names():
        return False
    if is_internal(prop):
        return False
    return True


def cpp_flags(prop):
    RUST_TO_CPP_FLAGS = {
      "CAN_ANIMATE_ON_COMPOSITOR": "CanAnimateOnCompositor",
      "AFFECTS_LAYOUT": "AffectsLayout",
      "AFFECTS_PAINT": "AffectsPaint",
      "AFFECTS_OVERFLOW": "AffectsOverflow",
    }
    result = []
    if prop.explicitly_enabled_in_chrome():
        result.append("EnabledInUASheetsAndChrome")
    elif prop.explicitly_enabled_in_ua_sheets():
        result.append("EnabledInUASheets")
    if is_internal(prop):
        result.append("Internal")
    if prop.enabled_in == "":
        result.append("Inaccessible")
    for (k, v) in RUST_TO_CPP_FLAGS.items():
        if k in prop.flags:
            result.append(v)
    if serialized_by_servo(prop):
        result.append("SerializedByServo")
    if prop.type() == "longhand" and prop.logical:
        result.append("IsLogical")
    return result



def gen_ns_css_props(output):
    raw_properties, deps = props_and_deps()
    output.write(
        """/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */

/* processed file that defines CSS property tables that can't be generated
   with the pre-processor, designed to be #included in nsCSSProps.cpp */

"""
    )

    properties = [
        PropertyWrapper(i, p)
        for i, p in enumerate(raw_properties.longhands + raw_properties.shorthands)
        if p.type() != "alias"
    ]

    # Generate kIDLNameTable
    output.write(
        "const char* const nsCSSProps::" "kIDLNameTable[eCSSProperty_COUNT] = {\n"
    )
    for p in properties:
        if p.idlname is None:
            output.write("  nullptr,  // {}\n".format(p.name))
        else:
            output.write('  "{}",\n'.format(p.idlname))
    output.write("};\n\n")

    # Generate kIDLNameSortPositionTable
    ps = sorted(properties, key=lambda p: p.idlname if p.idlname else "")
    ps = [(p, position) for position, p in enumerate(ps)]
    ps.sort(key=lambda item: item[0].index)
    output.write(
        "const int32_t nsCSSProps::"
        "kIDLNameSortPositionTable[eCSSProperty_COUNT] = {\n"
    )
    for p, position in ps:
        output.write("  {},\n".format(position))
    output.write("};\n\n")

    # Generate preferences table
    output.write(
        "const nsCSSProps::PropertyPref " "nsCSSProps::kPropertyPrefTable[] = {\n"
    )
    for p in raw_properties.all_properties_and_aliases():
        if not p.gecko_pref:
            continue
        if p.type() != "alias":
            prop_id = "eCSSProperty_" + p.ident
        else:
            prop_id = "eCSSPropertyAlias_" + p.ident
        output.write('  {{ {}, "{}" }},\n'.format(prop_id, p.gecko_pref))
    output.write("  { eCSSProperty_UNKNOWN, nullptr },\n")
    output.write("};\n\n")

    # Generate shorthand subprop tables
    names = []
    for p in properties:
        if p.type() != "shorthand":
            continue
        name = "g{}SubpropTable".format(p.idl_method)
        names.append(name)
        output.write(f"static const NonCustomCSSPropertyId {name}[] = {{\n")
        for subprop in p.sub_properties:
            output.write(f"  eCSSProperty_{subprop.ident},\n")
        output.write("  eCSSProperty_UNKNOWN\n")
        output.write("};\n\n")
    output.write("const NonCustomCSSPropertyId* const\n")
    output.write(
        "nsCSSProps::kSubpropertyTable["
        "eCSSProperty_COUNT - eCSSProperty_COUNT_no_shorthands"
        "] = {\n"
    )
    for name in names:
        output.write("  {},\n".format(name))
    output.write("};\n\n")

    # Generate assertions
    msg = (
        "GenerateCSSPropsGenerated.py did not list properties "
        "in NonCustomCSSPropertyId order"
    )
    for p in properties:
        output.write(
            'static_assert(eCSSProperty_{} == {}, "{}");\n'.format(p.ident, p.index, msg)
        )

    return deps


def gen_counted_unknown_properties(output, prop_file):
    properties, deps = props_and_deps()

    output.write("/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */\n\n")

    for prop in properties.counted_unknown_properties:
        output.write(
            "COUNTED_UNKNOWN_PROPERTY({}, {})\n".format(prop.name, prop.ident)
        )
    return deps


def gen_compositor_animatable_properties(output):
    properties, deps = props_and_deps()
    output.write(
        """/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */
#ifndef COMPOSITOR_ANIMATABLE_PROPERTY_LIST
#define COMPOSITOR_ANIMATABLE_PROPERTY_LIST { \\
"""
    )

    count = 0
    for p in properties.longhands:
        if "CAN_ANIMATE_ON_COMPOSITOR" in p.flags:
            output.write("  eCSSProperty_{}, \\\n".format(p.ident))
            count += 1

    output.write("}\n")
    output.write("#endif /* COMPOSITOR_ANIMATABLE_PROPERTY_LIST */\n")

    output.write("\n")
    output.write("#ifndef COMPOSITOR_ANIMATABLE_PROPERTY_LIST_LENGTH\n")
    output.write(
        "#define COMPOSITOR_ANIMATABLE_PROPERTY_LIST_LENGTH {}\n".format(count)
    )
    output.write("#endif /* COMPOSITOR_ANIMATABLE_PROPERTY_LIST_LENGTH */\n")
    return deps


def gen_non_custom_css_property_id(output, template):
    properties, deps = props_and_deps()
    with open(template, "r") as f:
        template = string.Template(f.read())

    property_ids = []
    for prop in properties.longhands:
        property_ids.append("eCSSProperty_{}".format(prop.ident))
    for prop in properties.shorthands:
        property_ids.append("eCSSProperty_{}".format(prop.ident))
    for alias in properties.all_aliases():
        property_ids.append("eCSSPropertyAlias_{}".format(alias.ident))

    longhand_count = property_ids[len(properties.longhands)]
    shorthand_count = property_ids[len(properties.longhands) + len(properties.shorthands)]
    output.write("/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */\n\n")
    output.write(
        template.substitute(
            {
                "property_ids": "\n".join("  {},".format(p) for p in property_ids),
                "longhand_first": property_ids[0],
                "longhand_count": longhand_count,
                "shorthand_count": shorthand_count,
            }
        )
    )
    return deps


# This builds the list of CSS properties that we expect in the
# property-database file for testing.
def gen_css_properties_js(output):
    # Don't print the properties that aren't accepted by the parser
    # TODO(emilio): Pretty sure we can automate this more.
    inaccessible_properties = set([
        "-x-cols",
        "-x-lang",
        "-x-span",
        "-x-text-scale",
        "-moz-default-appearance",
        "-moz-theme",
        "-moz-inert",
        "-moz-script-level",  # parsed by UA sheets only
        "-moz-math-variant",
        "-moz-math-display",                  # parsed by UA sheets only
        "-moz-top-layer",                     # parsed by UA sheets only
        "-moz-min-font-size-ratio",           # parsed by UA sheets only
        "-moz-box-collapse",                  # chrome-only internal properties
        "-moz-subtree-hidden-only-visually",  # chrome-only internal properties
        "-moz-user-focus",                    # chrome-only internal properties
        "-moz-window-input-region-margin",    # chrome-only internal properties
        "-moz-window-dragging",               # chrome-only internal properties
        "-moz-window-opacity",                # chrome-only internal properties
        "-moz-window-transform",              # chrome-only internal properties
        "-moz-window-shadow",                 # chrome-only internal properties
    ])

    properties, deps = props_and_deps()

    def print_array(name, props):
        first = True
        output.write(f"var {name} = [\n");
        for prop in props:
            if prop.name in inaccessible_properties:
                continue
            if "style" not in prop.rule_types_allowed_names():
                continue
            if not first:
                output.write(",\n")
            first = False
            output.write(f"\t{{ name: \"{prop.name}\", prop: \"{idl_attribute(prop)}\"")
            if prop.gecko_pref:
                output.write(f", pref: \"{prop.gecko_pref}\"")
            output.write(" }")
        output.write("\n];\n")

    output.write("/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */\n\n")
    print_array("gLonghandProperties", properties.longhands)
    print_array("gShorthandProperties", properties.shorthands)
    return deps


COMPUTED_STYLE_INC = """/* THIS IS AN AUTOGENERATED FILE.  DO NOT EDIT */

/* processed file that defines entries for nsComputedDOMStyle, designed
   to be #included in nsComputedDOMStyle.cpp */

static constexpr size_t kEntryIndices[eCSSProperty_COUNT] = {{
  {indices}
}};

{asserts}

static constexpr Entry kEntries[eCSSProperty_COUNT] = {{
  {entries}
}};
"""


def gen_computed_style(output):
    properties, deps = props_and_deps()

    def order_key(p):
        # Put prefixed properties after normal properties.
        # The spec is unclear about this, and Blink doesn't have any sensible
        # order at all, so it probably doesn't matter a lot. But originally
        # Gecko put then later so we do so as well. See w3c/csswg-drafts#2827.
        order = p.name.startswith("-")
        return (order, p.name)

    def has_cpp_getter(p):
        if not exposed_on_getcs(p):
            return False
        if serialized_by_servo(p):
            return False
        if p.type() == "longhand" and p.logical:
            return False
        return True

    def getter_entry(p):
        if has_cpp_getter(p):
            return "DoGet" + p.idl_method
        # Put a dummy getter here instead of nullptr because MSVC seems
        # to have bug which ruins the table when we put nullptr for
        # pointer-to-member-function. See bug 1471426.
        return "DummyGetter"

    entries = []
    indices = []
    asserts = []
    index_map = {}
    non_aliases = properties.longhands + properties.shorthands
    for i, p in enumerate(sorted(non_aliases, key=order_key)):
        can_be_exposed = "true" if exposed_on_getcs(p) else "false"
        entries.append(
            "{{ eCSSProperty_{}, {}, &nsComputedDOMStyle::{}}}".format(
                p.ident, can_be_exposed, getter_entry(p)
            )
        )
        index_map[p.ident] = i
        i += 1

    for i, p in enumerate(non_aliases):
        indices.append(str(index_map[p.ident]))
        asserts.append(
            'static_assert(size_t(eCSSProperty_{}) == {}, "");'.format(p.ident, i)
        )

    assert len(indices) == len(entries)

    output.write(
        COMPUTED_STYLE_INC.format(
            indices=", ".join(indices),
            entries=",\n    ".join(entries),
            asserts="\n".join(asserts),
        )
    )
    return deps
