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

# Common codegen classes.

from collections import defaultdict
from itertools import groupby

import operator
import os
import re
import string
import textwrap
import functools

from WebIDL import (
    BuiltinTypes,
    IDLBuiltinType,
    IDLNullValue,
    IDLNullableType,
    IDLObject,
    IDLType,
    IDLInterfaceMember,
    IDLUndefinedValue,
    IDLWrapperType,
)

from Configuration import (
    MakeNativeName,
    MemberIsUnforgeable,
    getModuleFromObject,
    getTypesFromCallback,
    getTypesFromDescriptor,
    getTypesFromDictionary,
    iteratorNativeType
)

AUTOGENERATED_WARNING_COMMENT = \
    "/* THIS FILE IS AUTOGENERATED - DO NOT EDIT */\n\n"
FINALIZE_HOOK_NAME = '_finalize'
TRACE_HOOK_NAME = '_trace'
CONSTRUCT_HOOK_NAME = '_constructor'
HASINSTANCE_HOOK_NAME = '_hasInstance'

RUST_KEYWORDS = {"abstract", "alignof", "as", "become", "box", "break", "const", "continue",
                 "else", "enum", "extern", "false", "final", "fn", "for", "if", "impl", "in",
                 "let", "loop", "macro", "match", "mod", "move", "mut", "offsetof", "override",
                 "priv", "proc", "pub", "pure", "ref", "return", "static", "self", "sizeof",
                 "struct", "super", "true", "trait", "type", "typeof", "unsafe", "unsized",
                 "use", "virtual", "where", "while", "yield"}


def replaceFileIfChanged(filename, newContents):
    """
    Read a copy of the old file, so that we don't touch it if it hasn't changed.
    Returns True if the file was updated, false otherwise.
    """
    # XXXjdm This doesn't play well with make right now.
    #       Force the file to always be updated, or else changing CodegenRust.py
    #       will cause many autogenerated bindings to be regenerated perpetually
    #       until the result is actually different.

    # oldFileContents = ""
    # try:
    #     with open(filename, 'rb') as oldFile:
    #         oldFileContents = ''.join(oldFile.readlines())
    # except:
    #     pass

    # if newContents == oldFileContents:
    #     return False

    with open(filename, 'wb') as f:
        f.write(newContents)

    return True


def toStringBool(arg):
    return str(not not arg).lower()


def toBindingNamespace(arg):
    return re.sub("((_workers)?$)", "Binding\\1", MakeNativeName(arg))


def stripTrailingWhitespace(text):
    tail = '\n' if text.endswith('\n') else ''
    lines = text.splitlines()
    for i in range(len(lines)):
        lines[i] = lines[i].rstrip()
    return '\n'.join(lines) + tail


def innerContainerType(type):
    assert type.isSequence() or type.isMozMap()
    return type.inner.inner if type.nullable() else type.inner


def wrapInNativeContainerType(type, inner):
    if type.isSequence():
        containerType = "Vec"
    elif type.isMozMap():
        containerType = "MozMap"
    else:
        raise TypeError("Unexpected container type %s", typ
e)

    return CGWrapper(inner, pre=containerType + "<", post=">")


builtinNames = {
    IDLType.Tags.bool: 'bool',
    IDLType.Tags.int8: 'i8',
    IDLType.Tags.int16: 'i16',
    IDLType.Tags.int32: 'i32',
    IDLType.Tags.int64: 'i64',
    IDLType.Tags.uint8: 'u8',
    IDLType.Tags.uint16: 'u16',
    IDLType.Tags.uint32: 'u32',
    IDLType.Tags.uint64: 'u64',
    IDLType.Tags.unrestricted_float: 'f32',
    IDLType.Tags.float: 'Finite<f32>',
    IDLType.Tags.unrestricted_double: 'f64',
    IDLType
.Tags.double: 'Finite<f64>'
}

numericTags = [
    IDLType.Tags.int8, IDLType.Tags.uint8,
    IDLType.Tags.int16, IDLType.Tags.uint16,
    IDLType.Tags.int32, IDLType.Tags.uint32,
    IDLType.Tags.int64, IDLType.Tags.uint64,
    IDLType.Tags.unrestricted_float,
    IDLType.Tags.unrestricted_double
]


# We'll want to insert the indent at the beginnings of lines, but we
# don't want to indent empty lines.  So only indent lines that have a
# non-newline character on them.
lineStartDetector = re.compile("^(?=[^\n#])", re.MULTILINE)


def indent(s, indentLevel=2):
    """
    Indent C++ code.

    Weird secret feature: this doesn't indent lines that start with # (such as
    #include lines or #ifdef/#endif).
    """
    if s == "":
        return s
    return re.sub(lineStartDetector, indentLevel * " ", s)


# dedent() and fill() are often called on the same string multiple
# times.  We want to memoize their return values so we don't keep
# recomputing them all the time.
def memoize(fn):
    """
    Decorator to memoize a function of one argument.  The cache just
    grows without bound.
    """
    cache = {}

    @functools.wraps(fn)
    def wrapper(arg):
        retval = cache.get(arg)
        if retval is None:
            retval = cache[arg] = fn(arg)
        return retval
    return wrapper


@memoize
def dedent(s):
    """
    Remove all leading whitespace from s, and remove a blank line
    at the beginning.
    """
    if s.startswith('\n'):
        s = s[1:]
    return textwrap.dedent(s)


# This works by transforming the fill()-template to an equivalent
# string.Template.
fill_multiline_substitution_re = re.compile(r"( *)\$\*{(\w+)}(\n)?")


@memoize
def compile_fill_template(template):
    """
    Helper function for fill().  Given the template string passed to fill(),
    do the reusable part of template processing and return a pair (t,
    argModList) that can be used every time fill() is called with that
    template argument.

    argsModList is list of tuples that represent modifications to be
    made to args.  Each modification has, in order: i) the arg name,
    ii) the modified name, iii) the indent depth.
    """
    t = dedent(template)
    assert t.endswith("\n") or "\n" not in t
    argModList = []

    def replace(match):
        """
        Replaces a line like '  $*{xyz}\n' with '${xyz_n}',
        where n is the indent depth, and add a corresponding entry to
        argModList.

        Note that this needs to close over argModList, so it has to be
        defined inside compile_fill_template().
        """
        indentation, name, nl = match.groups()
        depth = len(indentation)

        # Check that $*{xyz} appears by itself on a line.
        prev = match.string[:match.start()]
        if (prev and not prev.endswith("\n")) or nl is None:
            raise ValueError("Invalid fill() template: $*{%s} must appear by itself on a line" % name)

        # Now replace this whole line of template with the indented equivalent.
        modified_name = name + "_" + str(depth)
        argModList.append((name, modified_name, depth))
        return "${" + modified_name + "}"

    t = re.sub(fill_multiline_substitution_re, replace, t)
    return (string.Template(t), argModList)


def fill(template, **args):
    """
    Convenience function for filling in a multiline template.

    `fill(template, name1=v1, name2=v2)` is a lot like
    `string.Template(template).substitute({"name1": v1, "name2": v2})`.

    However, it's shorter, and has a few nice features:

      * If `template` is indented, fill() automatically dedents it!
        This makes code using fill() with Python's multiline strings
        much nicer to look at.

      * If `template` starts with a blank line, fill() strips it off.
        (Again, convenient with multiline strings.)

      * fill() recognizes a special kind of substitution
        of the form `$*{name}`.

        Use this to paste in, and automatically indent, multiple lines.
        (Mnemonic: The `*` is for "multiple lines").

        A `$*` substitution must appear by itself on a line, with optional
        preceding indentation (spaces only). The whole line is replaced by the
        corresponding keyword argument, indented appropriately.  If the
        argument is an empty string, no output is generated, not even a blank
        line.
    """

    t, argModList = compile_fill_template(template)
    # Now apply argModList to args
    for (name, modified_name, depth) in argModList:
        if not (args[name] == "" or args[name].endswith("\n")):
 