# Copyright 2013 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Generates C++ source files from a mojom.Module."""
import os
import sys
from generators.mojom_cpp_generator import _NameFormatter as CppNameFormatter
from generators.mojom_cpp_generator import Generator as CppGenerator
from generators.mojom_cpp_generator import IsNativeOnlyKind, NamespaceToArray
import mojom.generate.generator as generator
import mojom.generate.module as mojom
import mojom.generate.pack as pack
from mojom.generate.template_expander import UseJinja, UseJinjaForImportedTemplate

_kind_to_proto_type = {
    mojom.BOOL: "bool",
    mojom.INT8: "int32",
    mojom.UINT8: "uint32",
    mojom.INT16: "int32",
    mojom.UINT16: "uint32",
    mojom.INT32: "int32",
    mojom.UINT32: "uint32",
    mojom.FLOAT: "float",
    mojom.INT64: "int64",
    mojom.UINT64: "uint64",
    mojom.DOUBLE: "double",
}

_kind_to_cpp_proto_type = {
    mojom.BOOL: "bool",
    mojom.INT8: "::google::protobuf::int32",
    mojom.UINT8: "::google::protobuf::uint32",
    mojom.INT16: "::google::protobuf::int32",
    mojom.UINT16: "::google::protobuf::uint32",
    mojom.INT32: "::google::protobuf::int32",
    mojom.UINT32: "::google::protobuf::uint32",
    mojom.FLOAT: "float",
    mojom.INT64: "::google::protobuf::int64",
    mojom.UINT64: "::google::protobuf::int64",
    mojom.DOUBLE: "double",
}


def _IsStrOrUnicode(x):
  if sys.version_info[0] < 3:
    return isinstance(x, (unicode, str))
  return isinstance(x, str)


class _NameFormatter(CppNameFormatter):
  """A formatter for the names of kinds or values."""

  def __init__(self, *args, **kwargs):
    super(_NameFormatter, self).__init__(*args, **kwargs)

  def FormatForProto(self,
                     omit_namespace_for_module=None,
                     flatten_nested_kind=False):
    return self.Format(".",
                       prefixed=True,
                       omit_namespace_for_module=omit_namespace_for_module,
                       flatten_nested_kind=flatten_nested_kind)


class Generator(CppGenerator):
  def __init__(self, *args, **kwargs):
    super(Generator, self).__init__(*args, **kwargs)
    self.needs_mojolpm_proto = False
    self.enum_name_cache = dict()

  def _GetAllExtraTraitsHeaders(self):
    extra_headers = set()
    for typemap in self._GetAllTypemaps():
      extra_headers.update(typemap.get("traits_headers", []))
    extra_headers.update(self._GetExtraTraitsHeaders())
    return sorted(extra_headers)

  def _GetAllTypemaps(self):
    """Returns the typemaps for types needed in this module.
    """
    all_typemaps = []
    seen_types = set()

    def AddKind(kind):
      if (mojom.IsIntegralKind(kind) or mojom.IsStringKind(kind)
          or mojom.IsDoubleKind(kind) or mojom.IsFloatKind(kind)
          or mojom.IsAnyHandleKind(kind) or mojom.IsInterfaceKind(kind)
          or mojom.IsInterfaceRequestKind(kind) or mojom.IsAssociatedKind(kind)
          or mojom.IsPendingRemoteKind(kind)
          or mojom.IsPendingReceiverKind(kind)):
        pass
      elif mojom.IsArrayKind(kind):
        AddKind(kind.kind)
      elif mojom.IsMapKind(kind):
        AddKind(kind.key_kind)
        AddKind(kind.value_kind)
      else:
        name = self._GetFullMojomNameForKind(kind)
        if name in seen_types:
          return
        seen_types.add(name)

        typemap = self.typemap.get(name, None)
        if typemap:
          all_typemaps.append(typemap)
        if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind):
          for field in kind.fields:
            AddKind(field.kind)

    for kind in self.module.enums + self.module.structs + self.module.unions:
      AddKind(kind)

    return all_typemaps

  def _ProtoImports(self):
    """Scans all of the types used in this module to check which of the imports
    are needed for the generated proto files. This is somewhat different to the
    general case, since the generated proto files don't reference response
    parameters.
    """
    all_imports = self.module.imports
    seen_imports = set()
    seen_types = set()

    def AddKind(kind):
      if (mojom.IsIntegralKind(kind) or mojom.IsStringKind(kind)
          or mojom.IsDoubleKind(kind) or mojom.IsFloatKind(kind)):
        pass
      elif (mojom.IsAnyHandleKind(kind)):
        self.needs_mojolpm_proto = True
      elif mojom.IsArrayKind(kind):
        AddKind(kind.kind)
      elif mojom.IsMapKind(kind):
        AddKind(kind.key_kind)
        AddKind(kind.value_kind)
      elif (mojom.IsStructKind(kind) or mojom.IsUnionKind(kind)
            or mojom.IsEnumKind(kind) or mojom.IsInterfaceKind(kind)):
        name = self._GetFullMojomNameForKind(kind)
        if name in seen_types:
          return
        seen_types.add(name)
        if kind.module in all_imports:
          seen_imports.add(kind.module)
      elif (mojom.IsInterfaceRequestKind(kind)
            or mojom.IsAssociatedInterfaceKind(kind)
            or mojom.IsAssociatedInterfaceRequestKind(kind)
            or mojom.IsPendingRemoteKind(kind)
            or mojom.IsPendingReceiverKind(kind)
            or mojom.IsPendingAssociatedRemoteKind(kind)
            or mojom.IsPendingAssociatedReceiverKind(kind)):
        AddKind(kind.kind)

    for kind in self.module.structs + self.module.unions:
      for field in kind.fields:
        AddKind(field.kind)

    for interface in self.module.interfaces:
      for method in interface.methods:
        for parameter in method.parameters:
          AddKind(parameter.kind)
        if method.response_parameters:
          for parameter in method.response_parameters:
            AddKind(parameter.kind)

    import_files = list(
        map(lambda x: '{}.mojolpm.proto'.format(x.path), seen_imports))
    if self.needs_mojolpm_proto:
      import_files.append('mojo/public/tools/fuzzers/mojolpm.proto')
    import_files.sort()

    return import_files

  def _GetJinjaExports(self):
    all_enums = list(self.module.enums)
    for struct in self.module.structs:
      all_enums.extend(struct.enums)
    for interface in self.module.interfaces:
      all_enums.extend(interface.enums)

    return {
        "all_enums": all_enums,
        "all_extra_traits_headers": self._GetAllExtraTraitsHeaders(),
        "enums": self.module.enums,
        "extra_public_headers": self._GetExtraPublicHeaders(),
        "extra_traits_headers": self._GetExtraTraitsHeaders(),
        "imports": self.module.imports,
        "interfaces": self.module.interfaces,
        "module": self.module,
        "module_namespace": self.module.namespace,
        "namespaces_as_array": NamespaceToArray(self.module.namespace),
        "proto_imports": self._ProtoImports(),
        "structs": self.module.structs,
        "unions": self.module.unions,
    }

  @staticmethod
  def GetTemplatePrefix():
    return "mojolpm_templates"

  def GetFilters(self):
    cpp_filters = {
        "camel_to_under": generator.ToLowerSnakeCase,
        "contains_handles_or_interfaces": mojom.ContainsHandlesOrInterfaces,
        "cpp_wrapper_call_type": self._GetCppWrapperCallType,
        "cpp_wrapper_param_type": self._GetCppWrapperParamType,
        "cpp_wrapper_proto_type": self._GetCppWrapperProtoType,
        "cpp_wrapper_type": self._GetCppWrapperType,
        "enum_field_name": self._EnumFieldName,
        "get_name_for_kind": self._GetNameForKind,
        "get_qualified_name_for_kind": self._GetQualifiedNameForKind,
        "has_duplicate_values": self._EnumHasDuplicateValues,
        "nullable_is_same_kind": self._NullableIsSameKind,
        "proto_field_type": self._GetProtoFieldType,
        "proto_id": self._GetProtoId,
        "is_array_kind": mojom.IsArrayKind,
        "is_enum_kind": mojom.IsEnumKind,
        "is_double_kind": mojom.IsDoubleKind,
        "is_float_kind": mojom.IsFloatKind,
        "is_integral_kind": mojom.IsIntegralKind,
        "is_interface_kind": mojom.IsInterfaceKind,
        "is_receiver_kind": self._IsReceiverKind,
        "is_pending_associated_receiver_kind":
        mojom.IsPendingAssociatedReceiverKind,
        "is_pending_receiver_kind": mojom.IsPendingReceiverKind,
        "is_pending_associated_remote_kind":
        mojom.IsPendingAssociatedRemoteKind,
        "is_pending_remote_kind": mojom.IsPendingRemoteKind,
        "is_platform_handle_kind": mojom.IsPlatformHandleKind,
        "is_associated_interface_kind": mojom.IsAssociatedInterfaceKind,
        "is_native_only_kind": IsNativeOnlyKind,
        "is_any_handle_kind": mojom.IsAnyHandleKind,
        "is_any_interface_kind": mojom.IsAnyInterfaceKind,
        "is_any_handle_or_interface_kind": mojom.IsAnyHandleOrInterfaceKind,
        "is_associated_kind": mojom.IsAssociatedKind,
        "is_float_kind": mojom.IsFloatKind,
        "is_hashable": self._IsHashableKind,
        "is_map_kind": mojom.IsMapKind,
        "is_move_only_kind": self._IsMoveOnlyKind,
        "is_nullable_kind": mojom.IsNullableKind,
        "is_object_kind": mojom.IsObjectKind,
        "is_reference_kind": mojom.IsReferenceKind,
        "is_string_kind": mojom.IsStringKind,
        "is_struct_kind": mojom.IsStructKind,
        "is_typemapped_kind": self._IsTypemappedKind,
        "is_union_kind": mojom.IsUnionKind,
        "under_to_camel": self._UnderToCamel,
    }
    return cpp_filters

  @UseJinja("mojolpm.proto.tmpl")
  def _GenerateMojolpmProto(self):
    return self._GetJinjaExports()

  @UseJinja("mojolpm.h.tmpl")
  def _GenerateMojolpmHeader(self):
    return self._GetJinjaExports()

  @UseJinja("mojolpm.cc.tmpl")
  def _GenerateMojolpmSource(self):
    return self._GetJinjaExports()

  def GenerateFiles(self, args):
    self.module.Stylize(generator.Stylizer())

    if self.generate_non_variant_code:
      self.WriteWithComment(self._GenerateMojolpmProto(),
                            "%s.mojolpm.proto" % self.module.path)
    else:
      self.WriteWithComment(self._GenerateMojolpmHeader(),
                            "%s-mojolpm.h" % self.module.path)
      self.WriteWithComment(self._GenerateMojolpmSource(),
                            "%s-mojolpm.cc" % self.module.path)

  def _GetCppProtoNameForKind(self,
                              kind,
                              flatten_nested_kind=False,
                              add_same_module_namespaces=False):
    name = _NameFormatter(kind, self.variant).FormatForCpp(
        flatten_nested_kind=flatten_nested_kind,
        omit_namespace_for_module=(None if add_same_module_namespaces else
                                   self.module))
    if name.startswith('::'):
      name = 'mojolpm' + name
    return name

  def _GetProtoNameForKind(self,
                           kind,
                           flatten_nested_kind=False,
                           add_same_module_namespaces=False):
    name = _NameFormatter(kind, self.variant).FormatForProto(
        flatten_nested_kind=flatten_nested_kind,
        omit_namespace_for_module=(None if add_same_module_namespaces else
                                   self.module))
    if name.startswith('.'):
      name = 'mojolpm' + name
    return name

  def _IsMoveOnlyKind(self, kind):
    if self._IsTypemappedKind(kind):
      if mojom.IsEnumKind(kind):
        return False
      return self.typemap[self._GetFullMojomNameForKind(kind)]["move_only"]
    if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind):
      return True
    if mojom.IsArrayKind(kind):
      return self._IsMoveOnlyKind(kind.kind)
    if mojom.IsMapKind(kind):
      return (self._IsMoveOnlyKind(kind.value_kind)
              or self._IsMoveOnlyKind(kind.key_kind))
    if mojom.IsAnyHandleOrInterfaceKind(kind):
      return True
    return False

  def _GetNativeTypeName(self, typemapped_kind):
    return self.typemap[self._GetFullMojomNameForKind(
        typemapped_kind)]["typename"]

  def _FormatConstantDeclaration(self, constant, nested=False):
    if mojom.IsStringKind(constant.kind):
      if nested:
        return "const char %s[]" % constant.name
      return "%sextern const char %s[]" % \
          ((self.export_attribute + " ") if self.export_attribute else "",
           constant.name)
    return "constexpr %s %s = %s" % (GetCppPodType(
        constant.kind), constant.name, self._ConstantValue(constant))

  def _IsMoveOnlyKind(self, kind):
    if self._IsTypemappedKind(kind):
      if mojom.IsEnumKind(kind):
        return False
      return self.typemap[self._GetFullMojomNameForKind(kind)]["move_only"]
    if mojom.IsStructKind(kind) or mojom.IsUnionKind(kind):
      return True
    if mojom.IsArrayKind(kind):
      return self._IsMoveOnlyKind(kind.kind)
    if mojom.IsMapKind(kind):
      return (self._IsMoveOnlyKind(kind.value_kind)
              or self._IsMoveOnlyKind(kind.key_kind))
    if mojom.IsAnyHandleOrInterfaceKind(kind):
      return True
    return False

  def _GetCppWrapperProtoType(self, kind, add_same_module_namespaces=False):
    if (mojom.IsEnumKind(kind) or mojom.IsStructKind(kind)
        or mojom.IsUnionKind(kind)):
      return self._GetCppProtoNameForKind(
          kind, add_same_module_namespaces=add_same_module_namespaces)
    elif (mojom.IsPendingRemoteKind(kind) or mojom.IsPendingReceiverKind(kind)
          or mojom.IsPendingAssociatedRemoteKind(kind)
          or mojom.IsPendingAssociatedReceiverKind(kind)):
      return "uint32_t"
    elif mojom.IsStringKind(kind):
      return "std::string"
    elif mojom.IsGenericHandleKind(kind):
      return "mojolpm::Handle"
    elif mojom.IsDataPipeConsumerKind(kind):
      return "mojolpm::DataPipeConsumerHandle"
    elif mojom.IsDataPipeProducerKind(kind):
      return "mojolpm::DataPipeProducerHandle"
    elif mojom.IsMessagePipeKind(kind):
      return "mojolpm::MessagePipeHandle"
    elif mojom.IsSharedBufferKind(kind):
      return "mojolpm::SharedBufferHandle"
    elif mojom.IsPlatformHandleKind(kind):
      return "mojolpm::PlatformHandle"

    if not kind in _kind_to_cpp_proto_type:
      raise Exception("Unrecognized kind %s" % kind.spec)
    return _kind_to_cpp_proto_type[kind]

  def _GetProtoFieldType(self, kind, quantified=True):
    # TODO(markbrand): This will not handle array<array> or array<map>
    # TODO(markbrand): This also will not handle array<x, 10>
    unquantified = ''
    if (mojom.IsEnumKind(kind) or mojom.IsStructKind(kind)
        or mojom.IsUnionKind(kind)):
      unquantified = self._GetProtoNameForKind(kind)
    elif mojom.IsArrayKind(kind):
      return "repeated %sEntry" % self._GetProtoFieldType(kind.kind,
                                                          quantified=False)
    elif mojom.IsMapKind(kind):
      return ("map<%sKey, %sValue>" %
              (self._GetProtoFieldType(kind.key_kind, quantified=False),
               self._GetProtoFieldType(kind.value_kind, quantified=False)))
    elif (mojom.IsPendingRemoteKind(kind) or mojom.IsPendingReceiverKind(kind)
          or mojom.IsPendingAssociatedRemoteKind(kind)
          or mojom.IsPendingAssociatedReceiverKind(kind)):
      unquantified = "uint32"
    elif mojom.IsStringKind(kind):
      unquantified = "string"
    elif mojom.IsGenericHandleKind(kind):
      unquantified = "mojolpm.Handle"
    elif mojom.IsDataPipeConsumerKind(kind):
      unquantified = "mojolpm.DataPipeConsumerHandle"
    elif mojom.IsDataPipeProducerKind(kind):
      unquantified = "mojolpm.DataPipeProducerHandle"
    elif mojom.IsMessagePipeKind(kind):
      unquantified = "mojolpm.MessagePipeHandle"
    elif mojom.IsSharedBufferKind(kind):
      unquantified = "mojolpm.SharedBufferHandle"
    elif mojom.IsPlatformHandleKind(kind):
      unquantified = "mojolpm.PlatformHandle"
    else:
      unquantified = _kind_to_proto_type[kind]

    if quantified and mojom.IsNullableKind(kind):
      return 'optional %s' % unquantified
    elif quantified:
      return 'required %s' % unquantified
    else:
      return unquantified

  def _GetProtoId(self, name, kind=''):
    # We reserve ids [0,15]
    # Protobuf implementation reserves [19000,19999]
    # Max proto id is 2^29-1
    string = '{}@{}'.format(name, self._GetProtoFieldType(kind, False)).lower()
    # 32-bit fnv-1a
    fnv = 2166136261
    for c in string:
      fnv = fnv ^ ord(c)
      fnv = (fnv * 16777619) & 0xffffffff
    # xor-fold to 29-bits
    fnv = (fnv >> 29) ^ (fnv & 0x1fffffff)
    # now use a modulo to reduce to [0,2^29-1 - 1016]
    fnv = fnv % 536869895
    # now we move out the disallowed ranges
    fnv = fnv + 15
    if fnv >= 19000:
      fnv += 1000
    return fnv

  def _NullableIsSameKind(self, kind):
    if self._IsTypemappedKind(kind):
      if not self.typemap[self._GetFullMojomNameForKind(
          kind)]["nullable_is_same_type"]:
        return False
    if mojom.IsArrayKind(kind):
      return False
    if mojom.IsMapKind(kind):
      return False
    if mojom.IsStringKind(kind):
      return False
    return True

  def _EnumHasDuplicateValues(self, kind):
    values = set()
    i = 0
    for field in kind.fields:
      if field.name == 'MAX':
        continue
      if field.value:
        if _IsStrOrUnicode(field.value):
          if field.value in values:
            return True
          values.add(field.value)
          i = int(field.value, 0) + 1
        else:
          return True
      else:
        if str(i) in values:
          return True
        values.add(str(i))
        i += 1
    return False

  def _EnumFieldName(self, name, kind):
    # The WebFeature enum has entries that differ only by the casing of the
    # names. Protobuf doesn't support this, so we add the value to the end of
    # the name in these cases to disambiguate.
    if kind not in self.enum_name_cache:
      field_names = dict()
      lower_field_names = set()
      for field in kind.fields:
        new_field_name = field.name
        if new_field_name.lower() in lower_field_names:
          new_field_name = '{}_{}'.format(new_field_name, field.numeric_value)
        lower_field_names.add(new_field_name.lower())
        field_names[field.name] = new_field_name
      self.enum_name_cache[kind] = field_names
    return self.enum_name_cache[kind][name]
