"""Implementation of the types in Python 3's typing.py."""

# We should be able to enable pytype on this file once we switch to the
# typed_ast-based pyi parser. The current parser can't handle a constant called
# 'namedtuple':
# pytype: skip-file

# pylint's detection of this is error-prone:
# pylint: disable=unpacking-non-sequence

from pytype import abstract
from pytype import abstract_utils
from pytype import compat
from pytype import function
from pytype import overlay
from pytype import overlay_utils
from pytype import utils
from pytype.overlays import classgen
from pytype.overlays import collections_overlay
from pytype.pytd import pep484
from pytype.pytd import pytd
from pytype.pytd import visitors
from six import moves


# type alias
Param = overlay_utils.Param


class TypingOverlay(overlay.Overlay):
  """A representation of the 'typing' module that allows custom overlays."""

  def __init__(self, vm):
    # Make sure we have typing available as a dependency
    if vm.python_version < (3, 5) and not vm.loader.can_see("typing"):
      vm.errorlog.import_error(vm.frames, "typing")
    member_map = typing_overlay.copy()
    ast = vm.loader.typing
    for cls in ast.classes:
      _, name = cls.name.rsplit(".", 1)
      if name not in member_map and pytd.IsContainer(cls) and cls.template:
        member_map[name] = overlay.build(name, TypingContainer)
    super().__init__(vm, "typing", member_map, ast)


class Union(abstract.AnnotationClass):
  """Implementation of typing.Union[...]."""

  def __init__(self, vm, options=()):
    super().__init__("Union", vm)
    self.options = options

  def _build_value(self, node, inner, ellipses):
    self.vm.errorlog.invalid_ellipses(self.vm.frames, ellipses, self.name)
    return abstract.Union(self.options + inner, self.vm)


class Annotated(abstract.AnnotationClass):
  """Implementation of typing.Annotated[T, *annotations]."""

  def _build_value(self, node, inner, ellipses):
    self.vm.errorlog.invalid_ellipses(self.vm.frames, ellipses, self.name)
    if len(inner) == 1:
      error = "typing.Annotated must have at least 1 annotation"
      self.vm.errorlog.invalid_annotation(self.vm.frames, self, error)
    # discard annotations
    return inner[0]


class TypingContainer(abstract.AnnotationContainer):

  def __init__(self, name, vm):
    if name in pep484.PEP484_CAPITALIZED:
      pytd_name = "builtins." + name.lower()
    else:
      pytd_name = "typing." + name
    base = vm.convert.name_to_value(pytd_name)
    super().__init__(name, vm, base)


class Tuple(TypingContainer):
  """Implementation of typing.Tuple."""

  def _get_value_info(self, inner, ellipses):
    if ellipses:
      # An ellipsis may appear at the end of the parameter list as long as it is
      # not the only parameter.
      return super()._get_value_info(
          inner, ellipses, allowed_ellipses={len(inner) - 1} - {0})
    else:
      template = list(moves.range(len(inner))) + [abstract_utils.T]
      inner += (self.vm.merge_values(inner),)
      return template, inner, abstract.TupleClass


class Callable(TypingContainer):
  """Implementation of typing.Callable[...]."""

  def getitem_slot(self, node, slice_var):
    content = abstract_utils.maybe_extract_tuple(slice_var)
    inner, ellipses = self._build_inner(content)
    args = inner[0]
    if isinstance(args, abstract.List) and not args.could_contain_anything:
      inner[0], inner_ellipses = self._build_inner(args.pyval)
      self.vm.errorlog.invalid_ellipses(
          self.vm.frames, inner_ellipses, args.name)
    else:
      if args.cls and args.cls.full_name == "builtins.list":
        self.vm.errorlog.ambiguous_annotation(self.vm.frames, [args])
      elif 0 not in ellipses or not isinstance(args, abstract.Unsolvable):
        self.vm.errorlog.invalid_annotation(
            self.vm.frames, args, ("First argument to Callable must be a list"
                                   " of argument types or ellipsis."))
      inner[0] = self.vm.convert.unsolvable
    value = self._build_value(node, tuple(inner), ellipses)
    return node, value.to_variable(node)

  def _get_value_info(self, inner, ellipses):
    if isinstance(inner[0], list):
      template = (list(moves.range(len(inner[0]))) +
                  [t.name for t in self.base_cls.template])
      combined_args = self.vm.merge_values(inner[0])
      inner = tuple(inner[0]) + (combined_args,) + inner[1:]
      self.vm.errorlog.invalid_ellipses(self.vm.frames, ellipses, self.name)
      return template, inner, abstract.CallableClass
    else:
      # An ellipsis may take the place of the ARGS list.
      return super()._get_value_info(inner, ellipses, allowed_ellipses={0})


class TypeVarError(Exception):
  """Raised if an error is encountered while initializing a TypeVar."""

  def __init__(self, message, bad_call=None):
    super().__init__(message)
    self.bad_call = bad_call


class TypeVar(abstract.PyTDFunction):
  """Representation of typing.TypeVar, as a function."""

  def _get_constant(self, var, name, arg_type, arg_type_desc=None):
    try:
      ret = abstract_utils.get_atomic_python_constant(var, arg_type)
    except abstract_utils.ConversionError as e:
      raise TypeVarError(
          "%s must be %s" %
          (name, arg_type_desc or "a constant " + arg_type.__name__)) from e
    return ret

  def _get_annotation(self, node, var, name):
    with self.vm.errorlog.checkpoint() as record:
      annot = self.vm.annotations_util.extract_annotation(
          node, var, name, self.vm.simple_stack())
    if record.errors:
      raise TypeVarError("\n".join(error.message for error in record.errors))
    return annot

  def _get_namedarg(self, node, args, name, default_value):
    if name not in args.namedargs:
      return default_value
    if name == "bound":
      return self._get_annotation(node, args.namedargs[name], name)
    else:
      ret = self._get_constant(args.namedargs[name], name, bool)
      # This error is logged only if _get_constant succeeds.
      self.vm.errorlog.not_supported_yet(
          self.vm.frames, "argument \"%s\" to TypeVar" % name)
      return ret

  def _get_typeparam(self, node, args):
    args = args.simplify(node, self.vm)
    try:
      self.match_args(node, args)
    except function.InvalidParameters as e:
      raise TypeVarError("wrong arguments", e.bad_call) from e
    except function.FailedFunctionCall as e:
      # It is currently impossible to get here, since the only
      # FailedFunctionCall that is not an InvalidParameters is NotCallable.
      raise TypeVarError("initialization failed") from e
    name = self._get_constant(args.posargs[0], "name", str,
                              arg_type_desc="a constant str")
    constraints = tuple(
        self._get_annotation(node, c, "constraint") for c in args.posargs[1:])
    if len(constraints) == 1:
      raise TypeVarError("the number of constraints must be 0 or more than 1")
    bound = self._get_namedarg(node, args, "bound", None)
    covariant = self._get_namedarg(node, args, "covariant", False)
    contravariant = self._get_namedarg(node, args, "contravariant", False)
    if constraints and bound:
      raise TypeVarError("constraints and a bound are mutually exclusive")
    extra_kwargs = set(args.namedargs) - {"bound", "covariant", "contravariant"}
    if extra_kwargs:
      raise TypeVarError("extra keyword arguments: " + ", ".join(extra_kwargs))
    if args.starargs:
      raise TypeVarError("*args must be a constant tuple")
    if args.starstarargs:
      raise TypeVarError("ambiguous **kwargs not allowed")
    return abstract.TypeParameter(name, self.vm, constraints=constraints,
                                  bound=bound, covariant=covariant,
                                  contravariant=contravariant)

  def call(self, node, _, args):
    """Call typing.TypeVar()."""
    try:
      param = self._get_typeparam(node, args)
    except TypeVarError as e:
      self.vm.errorlog.invalid_typevar(
          self.vm.frames, utils.message(e), e.bad_call)
      return node, self.vm.new_unsolvable(node)
    return node, param.to_variable(node)


class Cast(abstract.PyTDFunction):
  """Implements typing.cast."""

  def call(self, node, func, args):
    if args.posargs:
      annot = self.vm.annotations_util.extract_annotation(
          node, args.posargs[0], "typing.cast", self.vm.simple_stack())
      if annot.formal:
        self.vm.errorlog.invalid_typevar(
            self.vm.frames, "cannot pass a TypeVar to a function")
        return node, self.vm.new_unsolvable(node)
      return self.vm.init_class(node, annot)
    return super().call(node, func, args)


class NoReturn(abstract.Singleton):
  """Implements typing.NoReturn as a singleton."""

  def __init__(self, vm):
    super().__init__("NoReturn", vm)


def build_any(vm):
  return vm.convert.unsolvable


class NamedTupleFuncBuilder(collections_overlay.NamedTupleBuilder):
  """Factory for creating typing.NamedTuple classes."""

  @classmethod
  def make(cls, vm):
    typing_ast = vm.loader.import_name("typing")
    # Because NamedTuple is a special case for the pyi parser, typing.pytd has
    # "_NamedTuple" instead. Replace the name of the returned function so that
    # error messages will correctly display "typing.NamedTuple".
    pyval = typing_ast.Lookup("typing._NamedTuple")
    pyval = pyval.Replace(name="typing.NamedTuple")
    self = super().make("NamedTuple", vm, pyval)
    # NamedTuple's fields arg has type Sequence[Sequence[Union[str, type]]],
    # which doesn't provide precise enough type-checking, so we have to do
    # some of our own in _getargs. _NamedTupleFields is an alias to
    # List[Tuple[str, type]], which gives a more understandable error message.
    fields_pyval = typing_ast.Lookup("typing._NamedTupleFields").type
    fields_type = vm.convert.constant_to_value(fields_pyval, {}, vm.root_node)
    # pylint: disable=protected-access
    self._fields_param = function.BadParam(name="fields", expected=fields_type)
    return self

  def _is_str_instance(self, val):
    return (isinstance(val, abstract.Instance) and
            val.full_name in ("builtins.str", "builtins.unicode"))

  def _getargs(self, node, args):
    self.match_args(node, args)
    sig, = self.signatures
    callargs = {name: var for name, var, _ in sig.signature.iter_args(args)}
    # typing.NamedTuple doesn't support rename or verbose
    name_var = callargs["typename"]
    fields_var = callargs["fields"]
    fields = abstract_utils.get_atomic_python_constant(fields_var)
    if isinstance(fields, str):
      # Since str matches Sequence, we have to manually check for it.
      raise function.WrongArgTypes(
          sig.signature, args, self.vm, self._fields_param)
    # The fields is a list of tuples, so we need to deeply unwrap them.
    fields = [abstract_utils.get_atomic_python_constant(t) for t in fields]
    # We need the actual string for the field names and the BaseValue
    # for the field types.
    names = []
    types = []
    for field in fields:
      if isinstance(field, str):
        # Since str matches Sequence, we have to manually check for it.
        raise function.WrongArgTypes(
            sig.signature, args, self.vm, self._fields_param)
      if (len(field) != 2 or
          any(not self._is_str_instance(v) for v in field[0].data)):
        # Note that we don't need to check field[1] because both 'str'
        # (forward reference) and 'type' are valid for it.
        raise function.WrongArgTypes(
            sig.signature, args, self.vm, self._fields_param)
      name, typ = field
      name_py_constant = abstract_utils.get_atomic_python_constant(name)
      if name_py_constant.__class__ is compat.UnicodeType:
        # Unicode values should be ASCII.
        name_py_constant = compat.native_str(name_py_constant.encode("ascii"))
      names.append(name_py_constant)
      types.append(abstract_utils.get_atomic_value(typ))
    return name_var, names, types

  def _build_namedtuple(self, name, field_names, field_types, node):
    # Build an InterpreterClass representing the namedtuple.
    if field_types:
      # TODO(mdemello): Fix this to support late types.
      field_types_union = abstract.Union(field_types, self.vm)
    else:
      field_types_union = self.vm.convert.none_type

    members = {n: t.instantiate(node)
               for n, t in moves.zip(field_names, field_types)}

    # collections.namedtuple has: __dict__, __slots__ and _fields.
    # typing.NamedTuple adds: _field_types, __annotations__ and _field_defaults.
    # __slots__ and _fields are tuples containing the names of the fields.
    slots = tuple(self.vm.convert.build_string(node, f) for f in field_names)
    members["__slots__"] = abstract.Tuple(slots, self.vm).to_variable(node)
    members["_fields"] = abstract.Tuple(slots, self.vm).to_variable(node)
    # __dict__ and _field_defaults are both collections.OrderedDicts that map
    # field names (strings) to objects of the field types.
    ordered_dict_cls = self.vm.convert.name_to_value("collections.OrderedDict",
                                                     ast=self.collections_ast)

    # In Python 2, keys can be `str` or `unicode`; support both.
    # In Python 3, `str_type` and `unicode_type` are the same.
    field_keys_union = abstract.Union([self.vm.convert.str_type,
                                       self.vm.convert.unicode_type], self.vm)

    # Normally, we would use abstract_utils.K and abstract_utils.V, but
    # collections.pyi doesn't conform to that standard.
    field_dict_cls = abstract.ParameterizedClass(
        ordered_dict_cls,
        {"K": field_keys_union, "V": field_types_union},
        self.vm)
    members["__dict__"] = field_dict_cls.instantiate(node)
    members["_field_defaults"] = field_dict_cls.instantiate(node)
    # _field_types and __annotations__ are both collections.OrderedDicts
    # that map field names (strings) to the types of the fields.
    field_types_cls = abstract.ParameterizedClass(
        ordered_dict_cls,
        {"K": field_keys_union, "V": self.vm.convert.type_type},
        self.vm)
    members["_field_types"] = field_types_cls.instantiate(node)
    members["__annotations__"] = field_types_cls.instantiate(node)

    # __new__
    # We set the bound on this TypeParameter later. This gives __new__ the
    # signature: def __new__(cls: Type[_Tname], ...) -> _Tname, i.e. the same
    # signature that visitor.CreateTypeParametersForSignatures would create.
    # This allows subclasses of the NamedTuple to get the correct type from
    # their constructors.
    cls_type_param = abstract.TypeParameter(
        visitors.CreateTypeParametersForSignatures.PREFIX + name,
        self.vm, bound=None)
    cls_type = abstract.ParameterizedClass(
        self.vm.convert.type_type, {abstract_utils.T: cls_type_param}, self.vm)
    params = [Param(n, t) for n, t in moves.zip(field_names, field_types)]
    members["__new__"] = overlay_utils.make_method(
        self.vm, node,
        name="__new__",
        self_param=Param("cls", cls_type),
        params=params,
        return_type=cls_type_param,
    )

    # __init__
    members["__init__"] = overlay_utils.make_method(
        self.vm, node,
        name="__init__",
        varargs=Param("args"),
        kwargs=Param("kwargs"))

    # _make
    # _make is a classmethod, so it needs to be wrapped by
    # specialibuiltins.ClassMethodInstance.
    # Like __new__, it uses the _Tname TypeVar.
    sized_cls = self.vm.convert.name_to_value("typing.Sized")
    iterable_type = abstract.ParameterizedClass(
        self.vm.convert.name_to_value("typing.Iterable"),
        {abstract_utils.T: field_types_union}, self.vm)
    cls_type = abstract.ParameterizedClass(
        self.vm.convert.type_type,
        {abstract_utils.T: cls_type_param}, self.vm)
    len_type = abstract.CallableClass(
        self.vm.convert.name_to_value("typing.Callable"),
        {0: sized_cls,
         abstract_utils.ARGS: sized_cls,
         abstract_utils.RET: self.vm.convert.int_type},
        self.vm)
    params = [
        Param("iterable", iterable_type),
        Param("new").unsolvable(self.vm, node),
        Param("len", len_type).unsolvable(self.vm, node)]
    make = overlay_utils.make_method(
        self.vm, node,
        name="_make",
        params=params,
        self_param=Param("cls", cls_type),
        return_type=cls_type_param)
    make_args = function.Args(posargs=(make,))
    _, members["_make"] = self.vm.special_builtins["classmethod"].call(
        node, None, make_args)

    # _replace
    # Like __new__, it uses the _Tname TypeVar. We have to annotate the `self`
    # param to make sure the TypeVar is substituted correctly.
    members["_replace"] = overlay_utils.make_method(
        self.vm, node,
        name="_replace",
        self_param=Param("self", cls_type_param),
        return_type=cls_type_param,
        kwargs=Param("kwds", field_types_union))

    # __getnewargs__
    getnewargs_tuple_params = dict(
        tuple(enumerate(field_types)) +
        ((abstract_utils.T, field_types_union),))
    getnewargs_tuple = abstract.TupleClass(self.vm.convert.tuple_type,
                                           getnewargs_tuple_params, self.vm)
    members["__getnewargs__"] = overlay_utils.make_method(
        self.vm, node,
        name="__getnewargs__",
        return_type=getnewargs_tuple)

    # __getstate__
    members["__getstate__"] = overlay_utils.make_method(
        self.vm, node, name="__getstate__")

    # _asdict
    members["_asdict"] = overlay_utils.make_method(
        self.vm, node,
        name="_asdict",
        return_type=field_dict_cls)

    # Finally, make the class.
    cls_dict = abstract.Dict(self.vm)
    cls_dict.update(node, members)
    if name.__class__ is compat.UnicodeType:
      # Unicode values should be ASCII.
      name = compat.native_str(name.encode("ascii"))

    node, cls_var = self.vm.make_class(
        node=node,
        name_var=self.vm.convert.build_string(node, name),
        bases=[self.vm.convert.tuple_type.to_variable(node)],
        class_dict_var=cls_dict.to_variable(node),
        cls_var=None)
    cls = cls_var.data[0]

    # Now that the class has been made, we can complete the TypeParameter used
    # by __new__, _make and _replace.
    cls_type_param.bound = cls

    return node, cls_var

  def call(self, node, _, args):
    try:
      name_var, field_names, field_types = self._getargs(node, args)
    except abstract_utils.ConversionError:
      return node, self.vm.new_unsolvable(node)

    try:
      name = abstract_utils.get_atomic_python_constant(name_var)
    except abstract_utils.ConversionError:
      return node, self.vm.new_unsolvable(node)

    try:
      field_names = self._validate_and_rename_args(name, field_names, False)
    except ValueError as e:
      self.vm.errorlog.invalid_namedtuple_arg(self.vm.frames, utils.message(e))
      return node, self.vm.new_unsolvable(node)

    annots = self.vm.annotations_util.convert_annotations_list(
        node, moves.zip(field_names, field_types))
    field_types = [annots.get(field_name, self.vm.convert.unsolvable)
                   for field_name in field_names]
    node, cls_var = self._build_namedtuple(name, field_names, field_types, node)

    self.vm.trace_classdef(cls_var)
    return node, cls_var


class NamedTupleClassBuilder(abstract.PyTDClass):
  """Factory for creating typing.NamedTuple classes."""

  # attributes prohibited to set in NamedTuple class syntax
  _prohibited = ("__new__", "__init__", "__slots__", "__getnewargs__",
                 "_fields", "_field_defaults", "_field_types",
                 "_make", "_replace", "_asdict", "_source")

  _special = ("__module__", "__name__", "__qualname__", "__annotations__")

  def __init__(self, vm):
    typing_ast = vm.loader.import_name("typing")
    pyval = typing_ast.Lookup("typing._NamedTupleClass")
    pyval = pyval.Replace(name="typing.NamedTuple")
    super().__init__("NamedTuple", pyval, vm)
    # Prior to python 3.6, NamedTuple is a function. Although NamedTuple is a
    # class in python 3.6+, we can still use it like a function. Hold the
    # an instance of 'NamedTupleFuncBuilder' so that we can reuse the
    # old implementation to implement the NamedTuple in python 3.6+
    self.namedtuple = NamedTupleFuncBuilder.make(vm)

  def call(self, node, _, args):
    posargs = args.posargs
    if isinstance(args.namedargs, dict):
      namedargs = args.namedargs
    else:
      namedargs = self.vm.convert.value_to_constant(args.namedargs, dict)
    if namedargs and self.vm.python_version < (3, 6):
      errmsg = "Keyword syntax for NamedTuple is only supported in Python 3.6+"
      self.vm.errorlog.invalid_namedtuple_arg(self.vm.frames, err_msg=errmsg)
    if namedargs and len(posargs) == 1:
      namedargs = [abstract.Tuple(
          (self.vm.convert.build_string(node, k), v), self.vm).to_variable(node)
                   for k, v in namedargs.items()]
      namedargs = abstract.List(namedargs, self.vm).to_variable(node)
      posargs += (namedargs,)
      args = function.Args(posargs)
    elif namedargs:
      errmsg = ("Either list of fields or keywords can be provided to "
                "NamedTuple, not both")
      self.vm.errorlog.invalid_namedtuple_arg(self.vm.frames, err_msg=errmsg)
    return self.namedtuple.call(node, None, args)

  def make_class(self, node, f_locals):
    # If BuildClass.call() hits max depth, f_locals will be [unsolvable]
    # Since we don't support defining NamedTuple subclasses in a nested scope
    # anyway, we can just return unsolvable here to prevent a crash, and let the
    # invalid namedtuple error get raised later.
    if f_locals.data[0].isinstance_Unsolvable():
      return node, self.vm.new_unsolvable(node)

    f_locals = abstract_utils.get_atomic_python_constant(f_locals)

    # retrieve __qualname__ to get the name of class
    name = f_locals["__qualname__"]

    # assemble the arguments that are compatible with NamedTupleFuncBuilder.call
    field_list = []
    defaults = []
    cls_locals = classgen.get_class_locals(
        abstract_utils.get_atomic_python_constant(name), allow_methods=True,
        ordering=classgen.Ordering.FIRST_ANNOTATE, vm=self.vm)
    for k, local in cls_locals.items():
      assert local.typ
      if k in f_locals:
        defaults.append(f_locals[k])
      k = self.vm.convert.constant_to_var(k, node=node)
      field_list.append(self.vm.convert.build_tuple(node, (k, local.typ)))
    anno = self.vm.convert.build_list(node, field_list)
    posargs = (name, anno)
    args = function.Args(posargs=posargs)
    node, cls_var = self.namedtuple.call(node, None, args)
    cls_val = abstract_utils.get_atomic_value(cls_var)

    if not isinstance(cls_val, abstract.Unsolvable):
      # set __new__.__defaults__
      defaults = abstract.Tuple(tuple(defaults), self.vm).to_variable(node)
      node, new_attr = self.vm.attribute_handler.get_attribute(
          node, cls_val, "__new__")
      new_attr = abstract_utils.get_atomic_value(new_attr)
      node = self.vm.attribute_handler.set_attribute(
          node, new_attr, "__defaults__", defaults)

      # set the attribute without overriding special namedtuple attributes
      node, fields = self.vm.attribute_handler.get_attribute(
          node, cls_val, "_fields")
      fields = abstract_utils.get_atomic_python_constant(fields, tuple)
      fields = [abstract_utils.get_atomic_python_constant(field, str)
                for field in fields]
      for key in f_locals:
        if key in self._prohibited:
          self.vm.errorlog.not_writable(self.vm.frames, cls_val, key)
        if key not in self._special and  key not in fields:
          node = self.vm.attribute_handler.set_attribute(
              node, cls_val, key, f_locals[key])

    return node, cls_var


class NewType(abstract.PyTDFunction):
  """Implementation of typing.NewType as a function."""

  def __init__(self, name, signatures, kind, vm):
    super().__init__(name, signatures, kind, vm)
    assert len(self.signatures) == 1, "NewType has more than one signature."
    signature = self.signatures[0].signature
    self._name_arg_name = signature.param_names[0]
    self._type_arg_name = signature.param_names[1]
    self._internal_name_counter = 0

  @property
  def internal_name_counter(self):
    val = self._internal_name_counter
    self._internal_name_counter += 1
    return val

  def call(self, node, func, args):
    args = args.simplify(node, self.vm)
    self.match_args(node, args, match_all_views=True)
    # As long as the types match we do not really care about the actual
    # class name. But, if we have a string literal value as the name arg,
    # we will use it.
    name_arg = args.namedargs.get(self._name_arg_name) or args.posargs[0]
    try:
      _ = abstract_utils.get_atomic_python_constant(name_arg, str)
    except abstract_utils.ConversionError:
      name_arg = self.vm.convert.constant_to_var(
          "_NewType_Internal_Class_Name_%d_" % self.internal_name_counter)
    type_arg = args.namedargs.get(self._type_arg_name) or args.posargs[1]
    try:
      type_value = abstract_utils.get_atomic_value(type_arg)
    except abstract_utils.ConversionError:
      # We need the type arg to be an atomic value. If not, we just
      # silently return unsolvable.
      return node, self.vm.new_unsolvable(node)
    value_arg_name = "val"
    constructor = overlay_utils.make_method(
        self.vm, node,
        name="__init__",
        params=[Param(value_arg_name, type_value)])
    members = abstract.Dict(self.vm)
    members.set_str_item(node, "__init__", constructor)
    return self.vm.make_class(node, name_arg, (type_arg,),
                              members.to_variable(node), None)


class Overload(abstract.PyTDFunction):
  """Implementation of typing.overload."""

  def call(self, node, unused_func, args):
    """Marks that the given function is an overload."""
    self.match_args(node, args)

    # Since we have only 1 argument, it's easy enough to extract.
    func_var = args.posargs[0] if args.posargs else args.namedargs["func"]

    for func in func_var.data:
      if isinstance(func, abstract.INTERPRETER_FUNCTION_TYPES):
        func.is_overload = True
        self.vm.frame.overloads[func.name].append(func)

    return node, func_var


class Generic(TypingContainer):
  """Implementation of typing.Generic."""

  def _get_value_info(self, inner, ellipses):
    if not all(isinstance(item, abstract.TypeParameter) for item in inner):
      self.vm.errorlog.invalid_annotation(
          self.vm.frames, self,
          "Parameters to Generic[...] must all be type variables")
      inner = [item for item in inner
               if isinstance(item, abstract.TypeParameter)]

    template = [item.name for item in inner]

    if len(set(template)) != len(template):
      self.vm.errorlog.invalid_annotation(
          self.vm.frames, self,
          "Parameters to Generic[...] must all be unique")

    return template, inner, abstract.ParameterizedClass


class Optional(abstract.AnnotationClass):
  """Implementation of typing.Optional."""

  def _build_value(self, node, inner, ellipses):
    self.vm.errorlog.invalid_ellipses(self.vm.frames, ellipses, self.name)
    if len(inner) != 1:
      error = "typing.Optional can only contain one type parameter"
      self.vm.errorlog.invalid_annotation(self.vm.frames, self, error)
    return abstract.Union((self.vm.convert.none_type,) + inner, self.vm)


class Literal(TypingContainer):
  """Implementation of typing.Literal."""

  def _build_value(self, node, inner, ellipses):
    values = []
    errors = []
    for i, param in enumerate(inner):
      # TODO(b/173742489): Once pytype has proper support for enums, we should
      # stop allowing unsolvable and handle enums here.
      if (param == self.vm.convert.none or
          isinstance(param, abstract.LiteralClass) or
          param == self.vm.convert.unsolvable and i not in ellipses):
        value = param
      elif (isinstance(param, abstract.ConcreteValue) and
            isinstance(param.pyval, (int, str, bytes))):
        value = abstract.LiteralClass(param, self.vm)
      else:
        if i in ellipses:
          invalid_param = "..."
        else:
          invalid_param = param.name
        errors.append((invalid_param, i))
        value = self.vm.convert.unsolvable
      values.append(value)
    if errors:
      self.vm.errorlog.invalid_annotation(
          self.vm.frames, self,
          "\n".join("Bad parameter %r at index %d" % e for e in errors))
    return self.vm.merge_values(values)


def not_supported_yet(name, vm):
  vm.errorlog.not_supported_yet(vm.frames, "typing." + name)
  return vm.convert.unsolvable


def build_namedtuple(vm):
  if vm.python_version < (3, 6):
    return NamedTupleFuncBuilder.make(vm)
  else:
    return NamedTupleClassBuilder(vm)


def build_newtype(vm):
  return NewType.make("NewType", vm, "typing")


def build_noreturn(vm):
  return vm.convert.no_return


def build_overload(vm):
  return Overload.make("overload", vm, "typing")


def build_typevar(vm):
  return TypeVar.make("TypeVar", vm, "typing", pyval_name="_typevar_new")


def build_typechecking(vm):
  return vm.convert.true


def build_cast(vm):
  return Cast.make("cast", vm, "typing")


typing_overlay = {
    "Annotated": overlay.build("Annotated", Annotated),
    "Any": build_any,
    "Callable": overlay.build("Callable", Callable),
    "Generic": overlay.build("Generic", Generic),
    "Literal": overlay.build("Literal", Literal),
    "NamedTuple": build_namedtuple,
    "NewType": build_newtype,
    "NoReturn": build_noreturn,
    "Optional": overlay.build("Optional", Optional),
    "Tuple": overlay.build("Tuple", Tuple),
    "TypeVar": build_typevar,
    "Union": Union,
    "TYPE_CHECKING": build_typechecking,
    "cast": build_cast,
    "overload": build_overload,
}
