# Copyright 2015 The Cobalt Authors. All Rights Reserved.
# coding=utf-8
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Functions for generating bindings for overloaded operations/constructors."""

from functools import partial

from v8_interface import distinguishing_argument_index
from v8_interface import effective_overload_set_by_length


def get_overload_contexts(expression_generator, method_overloads):
  """Create overload contexts for overloaded methods/constructors.

  Arguments:
      expression_generator: An ExpressionGenerator instance.
      method_overloads: [overload_sets] where overload_sets is itself
          a list of method_contexts (or constructor_contexts). All methods
          in the list of method_contexts are overloads of the same symbol.
  Returns:
      [overload_contexts]
  """
  overload_contexts = []
  for overloaded_methods in method_overloads:
    # Create an overload context. Binding generation will use this as a top
    # level context, and the identifier will be bound to an overload
    # resolution function.
    overload_context = {
        'overloads': overloaded_methods,
    }

    # Add a property to the overloaded methods indicating their overload
    # indices. This allows us to refer to each overload with a unique name.
    for index, method in enumerate(overloaded_methods, 1):
      method['overload_index'] = index

    # Get the effective overload set, and group by the number of arguments.
    # This is a list of tuples (numargs, [overload_set])
    effective_overloads_by_length = (
        effective_overload_set_by_length(overloaded_methods))

    overload_context['length'] = min(
        length for length, _ in effective_overloads_by_length)

    # The first four steps of the overload resolution algorithm involve
    # removing entries from the set S of all overloads where the number
    # of arguments in the overload does not match the number of overloads
    # passed to the function.
    # We accomplish this by partitioning the set S into S0...S{maxarg} where
    # each S{n} contains overloads with n arguments, and then switch on
    # the number of arguments in the bindings code.
    #
    # http://heycam.github.io/webidl/#dfn-overload-resolution-algorithm
    # 1. Let maxarg be the length of the longest type list of the entries
    #    in S.
    # 2. Initialize argcount to be min(maxarg, n).
    # 3. Remove from S all entries whose type list is not of length
    #    argcount.
    #
    # overload_resolution_context_by_length will be a list of tuples
    # (length, distinguishing_index, [overload_resolution_tests])
    overload_resolution_context_by_length = []
    for length, overload_set in effective_overloads_by_length:
      # For each overload, create a list of tuples for resolution:
      #   (length, distinguishing_argument_index, [resolution_tests])
      # where the length refers to the number of arguments for the set Sn of
      # overloads that take that number of arguments,
      # distinguishing_argument_index is as defined in the spec (except we
      # use None in the case that the size of the set Sn is 1), and
      # resolution tests is a list of tuples:
      #    (test_callable, method_context)
      # where test_callable is a function that takes one argument which is a
      # string representing the name of a variable in the generated bindings
      # code, and returns a boolean expression. If this evaluates to true,
      # then the test's associated overloaded method_context should be called.
      #
      # http://heycam.github.io/webidl/#dfn-overload-resolution-algorithm
      # 7. If there is more than one entry in S, then set d to be the
      #    distinguishing argument index for the entries of S.
      overload_resolution_context_by_length.append(
          (length, distinguishing_argument_index(overload_set)
           if len(overload_set) > 1 else None,
           list(resolution_tests_methods(expression_generator, overload_set))))
    overload_context['overload_resolution_by_length'] = (
        overload_resolution_context_by_length)

    overload_contexts.append(overload_context)

  return overload_contexts


def resolution_tests_methods(expression_generator, effective_overloads):
  """Yields resolution test and associated method.

  Tests and methods are in resolution order for effective overloads of a given
  length.

  This is the heart of the resolution algorithm.
  http://heycam.github.io/webidl/#dfn-overload-resolution-algorithm

  This function is based off of v8_interface.resolution_tests_methods. In Blink
  the first item in the tuple is a string representing C++ code that can be
  executed to perform the test. In Cobalt it is a function that takes one
  argument which is a string representing the variable name of a C++ object
  on which the test should be performed, and returns an expression that
  performs the test.

  Args:
      expression_generator: An ExpressionGenerator instance.
      effective_overloads: List of tuples returned from
          effective_overload_set_by_length
  Yields:
      (test, method)
  """

  methods = [
      effective_overload[0] for effective_overload in effective_overloads
  ]
  if len(methods) == 1:
    # If only one method with a given length, no test needed
    yield lambda arg: 'true', methods[0]
    return

  # 6. If there is more than one entry in S, then set d to be the
  # distinguishing argument index for the entries of S.
  index = distinguishing_argument_index(effective_overloads)
  # (7-9 are for handling |undefined| values for optional arguments before
  # the distinguishing argument (as "missing"), so you can specify only some
  # optional arguments. We don't support this, so we skip these steps.)
  # 10. If i = d, then:
  # (d is the distinguishing argument index)
  # 1. Let V be argi.
  #     Note: This is the argument that will be used to resolve which
  #           overload is selected.
  # js_value = 'info[%s]' % index

  # Extract argument and IDL type to simplify accessing these in each loop.
  arguments = [method['arguments'][index] for method in methods]
  arguments_methods = list(zip(arguments, methods))
  idl_types = [argument['idl_type_object'] for argument in arguments]
  idl_types_methods = list(zip(idl_types, methods))

  # We can't do a single loop through all methods or simply sort them, because
  # a method may be listed in multiple steps of the resolution algorithm, and
  # which test to apply differs depending on the step.
  #
  # Instead, we need to go through all methods at each step, either finding
  # first match (if only one test is allowed) or filtering to matches (if
  # multiple tests are allowed), and generating an appropriate tests.

  # 2. If V is undefined, and there is an entry in S whose list of
  # optionality values has "optional" at index i, then remove from S all
  # other entries.
  try:
    method = next(method for argument, method in arguments_methods
                  if argument['is_optional'])
    yield expression_generator.is_undefined, method
  except StopIteration:
    pass

  # 3. Otherwise: if V is null or undefined, and there is an entry in S that
  # has one of the following types at position i of its type list,
  # - a nullable type
  try:
    method = next(method for idl_type, method in idl_types_methods
                  if idl_type.is_nullable)
    yield expression_generator.is_undefined_or_null, method
  except StopIteration:
    pass

  # 4. Otherwise: if V is a platform object - but not a platform array
  # object - and there is an entry in S that has one of the following
  # types at position i of its type list,
  # - an interface type that V implements
  # (Unlike most of these tests, this can return multiple methods, since we
  #  test if it implements an interface. Thus we need a for loop, not a next.)
  # (We distinguish wrapper types from built-in interface types.)
  for idl_type, method in ((idl_type, method)
                           for idl_type, method in idl_types_methods
                           if idl_type.is_wrapper_type):
    base_type = idl_type.base_type if idl_type.is_nullable else idl_type
    yield partial(expression_generator.inherits_interface, base_type), method

  # 7. Otherwise: if Type(V) is Object, V has an [[ArrayBuffer]] internal
  # slot, and there is an entry in S that has one of the following types at
  # position i of its type list,
  # - ArrayBuffer
  # - Object
  # - A nullable version of either of the above types
  # - An annotated type whose inner type is one of the above types
  # - A union type, nullable union type, or annotated union type that has one
  #   of the above types in its flattened member types
  # 8. Otherwise: if Type(V) is Object, V has an [[DataView]] internal slot,
  # and there is an entry in S that has one of the following types at position
  # i of its type list,
  # - DataView
  # - Object
  # - A nullable version of either of the above types
  # - An annotated type whose inner type is one of the above types
  # - A union type, nullable union type, or annotated union type that has one
  #   of the above types in its flattened member types
  # 9. Otherwise: if Type(V) is Object, V has an [[TypedArray]] internal slot,
  # and there is an entry in S that has one of the following types at position
  # i of its type list,
  # - A typed array type whose name is equal to the value of V's
  # [[TypedArrayName]] internal slot
  # - Object
  # - A nullable version of either of the above types
  # - An annotated type whose inner type is one of the above types
  # - A union type, nullable union type, or annotated union type that has one
  #   of the above types in its flattened member types

  for idl_type, method in ((idl_type, method)
                           for idl_type, method in idl_types_methods
                           if idl_type.is_array_buffer_or_view):
    base_type = idl_type.base_type if idl_type.is_nullable else idl_type
    yield partial(expression_generator.is_type, base_type), method

  # FIXME:
  # We don't strictly follow the algorithm here. The algorithm says "remove
  # all other entries" if there is "one entry" matching, but we yield all
  # entries to support following constructors:
  # [constructor(sequence<DOMString> arg), constructor(Dictionary arg)]
  # interface I { ... }
  # (Need to check array types before objects because an array is an object)
  for idl_type, method in idl_types_methods:
    if idl_type.is_dictionary or idl_type.name == 'Dictionary':
      raise NotImplementedError('Dictionary types not yet supported.')

  # (Check for exact type matches before performing automatic type conversion;
  # only needed if distinguishing between primitive types.)
  if len([idl_type.is_primitive_type for idl_type in idl_types]) > 1:
    # (Only needed if match in step 11, otherwise redundant.)
    if any(
        idl_type.is_string_type or idl_type.is_enum for idl_type in idl_types):
      # 10. Otherwise: if V is a Number value, and there is an entry in S
      # that has one of the following types at position i of its type
      # list,
      # - a numeric type
      try:
        method = next(method for idl_type, method in idl_types_methods
                      if idl_type.is_numeric_type)
        yield (expression_generator.is_number), method
      except StopIteration:
        pass

  # (Perform automatic type conversion, in order. If any of these match,
  # that's the end, and no other tests are needed.) To keep this code simple,
  # we rely on the C++ compiler's dead code elimination to deal with the
  # redundancy if both cases below trigger.

  # 11. Otherwise: if there is an entry in S that has one of the following
  # types at position i of its type list,
  # - DOMString
  # - ByteString
  # - USVString
  # - an enumeration type
  try:
    method = next(method for idl_type, method in idl_types_methods
                  if idl_type.is_string_type or idl_type.is_enum)
    yield lambda arg: 'true', method
  except StopIteration:
    pass

  # 12. Otherwise: if there is an entry in S that has one of the following
  # types at position i of its type list,
  # - a numeric type
  try:
    method = next(method for idl_type, method in idl_types_methods
                  if idl_type.is_numeric_type)
    yield lambda arg: 'true', method
  except StopIteration:
    pass
