# 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.
"""Code shared by the various language-specific code generators."""

from __future__ import print_function

from functools import partial
import os.path
import re

from mojom import fileutil
from mojom.generate import module as mojom
from mojom.generate import pack


def ExpectedArraySize(kind):
  if mojom.IsArrayKind(kind):
    return kind.length
  return None


def SplitCamelCase(identifier):
  """Splits a camel-cased |identifier| and returns a list of lower-cased
  strings.
  """
  # Add underscores after uppercase letters when appropriate. An uppercase
  # letter is considered the end of a word if it is followed by an upper and a
  # lower. E.g. URLLoaderFactory -> URL_LoaderFactory
  identifier = re.sub('([A-Z][0-9]*)(?=[A-Z][0-9]*[a-z])', r'\1_', identifier)
  # Add underscores after lowercase letters when appropriate. A lowercase letter
  # is considered the end of a word if it is followed by an upper.
  # E.g. URLLoaderFactory -> URLLoader_Factory
  identifier = re.sub('([a-z][0-9]*)(?=[A-Z])', r'\1_', identifier)
  return [x.lower() for x in identifier.split('_')]


def ToCamel(identifier, lower_initial=False, digits_split=False, delimiter='_'):
  """Splits |identifier| using |delimiter|, makes the first character of each
  word uppercased (but makes the first character of the first word lowercased
  if |lower_initial| is set to True), and joins the words. Please note that for
  each word, all the characters except the first one are untouched.
  """
  result = ''
  capitalize_next = True
  for i in range(len(identifier)):
    if identifier[i] == delimiter:
      capitalize_next = True
    elif digits_split and identifier[i].isdigit():
      capitalize_next = True
      result += identifier[i]
    elif capitalize_next:
      capitalize_next = False
      result += identifier[i].upper()
    else:
      result += identifier[i]

  if lower_initial and result:
    result = result[0].lower() + result[1:]

  return result


def _ToSnakeCase(identifier, upper=False):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "URL_LOADER_FACTORY" if upper, otherwise "url_loader_factory".
  """
  words = SplitCamelCase(identifier)
  if words[0] == 'k' and len(words) > 1:
    words = words[1:]

  # Variables cannot start with a digit
  if (words[0][0].isdigit()):
    words[0] = '_' + words[0]


  if upper:
    words = map(lambda x: x.upper(), words)

  return '_'.join(words)


def ToUpperSnakeCase(identifier):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "URL_LOADER_FACTORY".
  """
  return _ToSnakeCase(identifier, upper=True)


def ToLowerSnakeCase(identifier):
  """Splits camel-cased |identifier| into lower case words, removes the first
  word if it's "k" and joins them using "_" e.g. for "URLLoaderFactory", returns
  "url_loader_factory".
  """
  return _ToSnakeCase(identifier, upper=False)


class Stylizer(object):
  """Stylizers specify naming rules to map mojom names to names in generated
  code. For example, if you would like method_name in mojom to be mapped to
  MethodName in the generated code, you need to define a subclass of Stylizer
  and override StylizeMethod to do the conversion."""

  def StylizeConstant(self, mojom_name):
    return mojom_name

  def StylizeField(self, mojom_name):
    return mojom_name

  def StylizeStruct(self, mojom_name):
    return mojom_name

  def StylizeUnion(self, mojom_name):
    return mojom_name

  def StylizeParameter(self, mojom_name):
    return mojom_name

  def StylizeMethod(self, mojom_name):
    return mojom_name

  def StylizeInterface(self, mojom_name):
    return mojom_name

  def StylizeEnumField(self, mojom_name):
    return mojom_name

  def StylizeEnum(self, mojom_name):
    return mojom_name

  def StylizeModule(self, mojom_namespace):
    return mojom_namespace


def WriteFile(contents, full_path):
  # If |contents| is same with the file content, we skip updating.
  if not isinstance(contents, bytes):
    data = contents.encode('utf8')
  else:
    data = contents

  if os.path.isfile(full_path):
    with open(full_path, 'rb') as destination_file:
      if destination_file.read() == data:
        return

  # Make sure the containing directory exists.
  full_dir = os.path.dirname(full_path)
  fileutil.EnsureDirectoryExists(full_dir)

  # Dump the data to disk.
  with open(full_path, 'wb') as f:
    f.write(data)


def AddComputedData(module):
  """Adds computed data to the given module. The data is computed once and
  used repeatedly in the generation process."""

  def _AddStructComputedData(exported, struct):
    struct.packed = pack.PackedStruct(struct)
    struct.bytes = pack.GetByteLayout(struct.packed)
    struct.versions = pack.GetVersionInfo(struct.packed)
    struct.exported = exported

  def _AddInterfaceComputedData(interface):
    interface.version = 0
    for method in interface.methods:
      # this field is never scrambled
      method.sequential_ordinal = method.ordinal

      if method.min_version is not None:
        interface.version = max(interface.version, method.min_version)

      method.param_struct = _GetStructFromMethod(method)
      if interface.stable:
        method.param_struct.attributes[mojom.ATTRIBUTE_STABLE] = True
        if method.explicit_ordinal is None:
          raise Exception(
              'Stable interfaces must declare explicit method ordinals. The '
              'method %s on stable interface %s does not declare an explicit '
              'ordinal.' % (method.mojom_name, interface.qualified_name))
      interface.version = max(interface.version,
                              method.param_struct.versions[-1].version)

      if method.response_parameters is not None:
        method.response_param_struct = _GetResponseStructFromMethod(method)
        if interface.stable:
          method.response_param_struct.attributes[mojom.ATTRIBUTE_STABLE] = True
        interface.version = max(
            interface.version,
            method.response_param_struct.versions[-1].version)
      else:
        method.response_param_struct = None

  def _GetStructFromMethod(method):
    """Converts a method's parameters into the fields of a struct."""
    params_class = "%s_%s_Params" % (method.interface.mojom_name,
                                     method.mojom_name)
    struct = mojom.Struct(params_class,
                          module=method.interface.module,
                          attributes={})
    for param in method.parameters:
      struct.AddField(
          param.mojom_name,
          param.kind,
          param.ordinal,
          attributes=param.attributes)
    _AddStructComputedData(False, struct)
    return struct

  def _GetResponseStructFromMethod(method):
    """Converts a method's response_parameters into the fields of a struct."""
    params_class = "%s_%s_ResponseParams" % (method.interface.mojom_name,
                                             method.mojom_name)
    struct = mojom.Struct(params_class,
                          module=method.interface.module,
                          attributes={})
    for param in method.response_parameters:
      struct.AddField(
          param.mojom_name,
          param.kind,
          param.ordinal,
          attributes=param.attributes)
    _AddStructComputedData(False, struct)
    return struct

  for struct in module.structs:
    _AddStructComputedData(True, struct)
  for interface in module.interfaces:
    _AddInterfaceComputedData(interface)


class Generator(object):
  # Pass |output_dir| to emit files to disk. Omit |output_dir| to echo all
  # files to stdout.
  def __init__(self,
               module,
               output_dir=None,
               typemap=None,
               variant=None,
               bytecode_path=None,
               for_blink=False,
               js_bindings_mode="new",
               js_generate_struct_deserializers=False,
               export_attribute=None,
               export_header=None,
               generate_non_variant_code=False,
               support_lazy_serialization=False,
               disallow_native_types=False,
               disallow_interfaces=False,
               generate_message_ids=False,
               generate_fuzzing=False,
               enable_kythe_annotations=False,
               extra_cpp_template_paths=None,
               generate_extra_cpp_only=False):
    self.module = module
    self.output_dir = output_dir
    self.typemap = typemap or {}
    self.variant = variant
    self.bytecode_path = bytecode_path
    self.for_blink = for_blink
    self.js_bindings_mode = js_bindings_mode
    self.js_generate_struct_deserializers = js_generate_struct_deserializers
    self.export_attribute = export_attribute
    self.export_header = export_header
    self.generate_non_variant_code = generate_non_variant_code
    self.support_lazy_serialization = support_lazy_serialization
    self.disallow_native_types = disallow_native_types
    self.disallow_interfaces = disallow_interfaces
    self.generate_message_ids = generate_message_ids
    self.generate_fuzzing = generate_fuzzing
    self.enable_kythe_annotations = enable_kythe_annotations
    self.extra_cpp_template_paths = extra_cpp_template_paths
    self.generate_extra_cpp_only = generate_extra_cpp_only

  def Write(self, contents, filename):
    if self.output_dir is None:
      print(contents)
      return
    full_path = os.path.join(self.output_dir, filename)
    WriteFile(contents, full_path)

  def OptimizeEmpty(self, contents):
    # Look for .cc files that contain no actual code. There are many of these
    # and they collectively take a while to compile.
    lines = contents.splitlines()

    for line in lines:
      if line.startswith('#') or line.startswith('//'):
        continue
      if re.match(r'namespace .* {', line) or re.match(r'}.*//.*namespace',
                                                       line):
        continue
      if line.strip():
        # There is some actual code - return the unmodified contents.
        return contents

    # If we reach here then we have a .cc file with no actual code. The
    # includes are therefore unneeded and can be removed.
    new_lines = [line for line in lines if not line.startswith('#include')]
    if len(new_lines) < len(lines):
      new_lines.append('')
      new_lines.append('// Includes removed due to no code being generated.')
    return '\n'.join(new_lines)

  def WriteWithComment(self, contents, filename):
    generator_name = "mojom_bindings_generator.py"
    comment = r"// %s is auto generated by %s, do not edit" % (filename,
                                                               generator_name)
    contents = comment + '\n' + '\n' + contents;
    if filename.endswith('.cc'):
      contents = self.OptimizeEmpty(contents)
    self.Write(contents, filename)

  def GenerateFiles(self, args):
    raise NotImplementedError("Subclasses must override/implement this method")

  def GetJinjaParameters(self):
    """Returns default constructor parameters for the jinja environment."""
    return {}

  def GetGlobals(self):
    """Returns global mappings for the template generation."""
    return {}
