#!/usr/bin/python
# Copyright (c) 2011, the Dart project authors.  Please see the AUTHORS file
# for details. All rights reserved. Use of this source code is governed by a
# BSD-style license that can be found in the LICENSE file.

import copy
import database
import idlparser
import logging
import os
import os.path

from idlnode import *

_logger = logging.getLogger('databasebuilder')

# Used in source annotations to specify the parent interface declaring
# a displaced declaration. The 'via' attribute specifies the parent interface
# which implements a displaced declaration.
_VIA_ANNOTATION_ATTR_NAME = 'via'

# Used in source annotations to specify the module that the interface was
# imported from.
_MODULE_ANNOTATION_ATTR_NAME = 'module'


class DatabaseBuilderOptions(object):
  """Used in specifying options when importing new interfaces"""

  def __init__(self,
      idl_syntax=idlparser.WEBIDL_SYNTAX,
      idl_defines=[],
      source=None, source_attributes={},
      type_rename_map={},
      rename_operation_arguments_on_merge=False,
      add_new_interfaces=True,
      obsolete_old_declarations=False):
    """Constructor.
    Args:
      idl_syntax -- the syntax of the IDL file that is imported.
      idl_defines -- list of definitions for the idl gcc pre-processor
      source -- the origin of the IDL file, used for annotating the
        database.
      source_attributes -- this map of attributes is used as
        annotation attributes.
      rename_operation_arguments_on_merge -- if True, will rename
        operation arguments when merging using the new name rather
        than the old.
      add_new_interfaces -- when False, if an interface is a new
        addition, it will be ignored.
      obsolete_old_declarations -- when True, if a declaration
        from a certain source is not re-declared, it will be removed.
    """
    self.source = source
    self.source_attributes = source_attributes
    self.idl_syntax = idl_syntax
    self.idl_defines = idl_defines
    self.type_rename_map = type_rename_map
    self.rename_operation_arguments_on_merge = \
        rename_operation_arguments_on_merge
    self.add_new_interfaces = add_new_interfaces
    self.obsolete_old_declarations = obsolete_old_declarations


class DatabaseBuilder(object):
  def __init__(self, database):
    """DatabaseBuilder is used for importing and merging interfaces into
    the Database"""
    self._database = database
    self._imported_interfaces = []
    self._impl_stmts = []
    self._same_signatures = {}

  def _load_idl_file(self, file_name, import_options):
    """Loads an IDL file intor memory"""
    idl_parser = idlparser.IDLParser(import_options.idl_syntax)

    try:
      f = open(file_name, 'r')
      content = f.read()
      f.close()

      idl_ast = idl_parser.parse(content,
        defines=import_options.idl_defines)
      return IDLFile(idl_ast, file_name)
    except SyntaxError, e:
      raise RuntimeError('Failed to load file %s: %s' % (file_name, e))

  def _resolve_type_defs(self, idl_file):
    type_def_map = {}
    # build map
    for type_def in idl_file.all(IDLTypeDef):
      if type_def.type.id != type_def.id: # sanity check
        type_def_map[type_def.id] = type_def.type.id
    # use the map
    for type_node in idl_file.all(IDLType):
      while type_node.id in type_def_map:
        type_node.id = type_def_map[type_node.id]

  def _strip_ext_attributes(self, idl_file):
    """Strips unuseful extended attributes."""
    for ext_attrs in idl_file.all(IDLExtAttrs):
      # TODO: Decide which attributes are uninteresting.
      pass

  def _split_declarations(self, interface):
    """Splits read-write attributes and operations with optional
    arguments into multiple declarations"""

    # split attributes into setters and getters
    new_attributes = []
    for attribute in interface.attributes:
      if attribute.is_fc_getter or attribute.is_fc_setter:
        new_attributes.append(attribute)
        continue
      getter_attr = copy.deepcopy(attribute)
      getter_attr.is_fc_getter = True
      new_attributes.append(getter_attr)
      if not attribute.is_read_only:
        setter_attr = copy.deepcopy(attribute)
        setter_attr.is_fc_setter = True
        new_attributes.append(setter_attr)
    interface.attributes = new_attributes

    # split operations with optional args into multiple operations
    new_ops = []
    for op in interface.operations:
      for i in range(0, len(op.arguments)):
        if op.arguments[i].is_optional:
          op.arguments[i].is_optional = False
          new_op = copy.deepcopy(op)
          new_op.arguments = new_op.arguments[:i]
          new_ops.append(new_op)
      new_ops.append(op)
    interface.operations = new_ops

  def _rename_types(self, idl_file, import_options):
    """Rename interface and type names with names provided in the
    options. Also clears scopes from scoped names"""
    def rename(idl_node):
      name_parts = idl_node.id.split('::')
      idl_node.id = name_parts[-1]
      if idl_node.id in import_options.type_rename_map:
        idl_node.id =  import_options.type_rename_map[idl_node.id]
    map(rename, idl_file.all(IDLInterface))
    map(rename, idl_file.all(IDLType))

  def _annotate(self, interface, module_name, import_options):
    """Adds @ annotations based on the source and source_attributes
    members of import_options."""

    source = import_options.source
    if not source:
      return

    def add_source_annotation(idl_node):
      annotation = IDLAnnotation(
        copy.deepcopy(import_options.source_attributes))
      idl_node.annotations[source] = annotation
      if ((isinstance(idl_node, IDLInterface) or
           isinstance(idl_node, IDLMember)) and
          idl_node.is_fc_suppressed):
        annotation['suppressed'] = None

    add_source_annotation(interface)
    interface.annotations[source][_MODULE_ANNOTATION_ATTR_NAME] = module_name

    map(add_source_annotation, interface.parents)
    map(add_source_annotation, interface.constants)
    map(add_source_annotation, interface.attributes)
    map(add_source_annotation, interface.operations)
    map(add_source_annotation, interface.snippets)

  def _sign(self, node):
    """Computes a unique signature for the node, for merging purposed, by
    concatenating types and names in the declaration."""
    if isinstance(node, IDLType):
      res = node.id
      if res.startswith('unsigned '):
        res = res[len('unsigned '):]
      if res in self._same_signatures:
        res = self._same_signatures[res]
      return res

    res = []
    if isinstance(node, IDLInterface):
      res = ['interface', node.id]
    elif isinstance(node, IDLParentInterface):
      res = ['parent', self._sign(node.type)]
    elif isinstance(node, IDLOperation):
      res = ['op']
      for special in node.specials:
        res.append(special)
      if node.id is not None:
        res.append(node.id)
      for arg in node.arguments:
        res.append(self._sign(arg.type))
      res.append(self._sign(node.type))
    elif isinstance(node, IDLAttribute):
      res = []
      if node.is_fc_getter:
        res.append('getter')
      elif node.is_fc_setter:
        res.append('setter')
      res.append(node.id)
      res.append(self._sign(node.type))
    elif isinstance(node, IDLConstant):
      res = []
      res.append('const')
      res.append(node.id)
      res.append(node.value)
      res.append(self._sign(node.type))
    else:
      raise TypeError("Can't sign input of type %s" % type(node))
    return ':'.join(res)

  def _build_signatures_map(self, idl_node_list):
    """Creates a hash table mapping signatures to idl_nodes for the
    given list of nodes"""
    res = {}
    for idl_node in idl_node_list:
      sig = self._sign(idl_node)
      if sig is None:
        continue
      if sig in res:
        raise RuntimeError('Warning: Multiple members have the same '
                           'signature: "%s"' % sig)
      res[sig] = idl_node
    return res

  def _get_parent_interfaces(self, interface):
    """Return a list of all the parent interfaces of a given interface"""
    res = []

    def recurse(current_interface):
      if current_interface in res:
        return
      res.append(current_interface)
      for parent in current_interface.parents:
        parent_name = parent.type.id
        if self._database.HasInterface(parent_name):
          recurse(self._database.GetInterface(parent_name))

    recurse(interface)
    return res[1:]

  def _merge_nodes(self, old_list, new_list, import_options):
    """Merges two lists of nodes. Annotates nodes with the source of each
    node.

    Returns:
      True is the old_list has changed.

    Args:
      old_list -- the list to merge into.
      new_list -- list containing more nodes.
      import_options -- controls how merging is done.
    """
    changed = False

    source = import_options.source

    old_signatures_map = self._build_signatures_map(old_list)
    new_signatures_map = self._build_signatures_map(new_list)

    # Merge new items
    for (sig, new_node) in new_signatures_map.items():
      if sig not in old_signatures_map:
        # New node:
        old_list.append(new_node)
        changed = True
      else:
        # Merge old and new nodes:
        old_node = old_signatures_map[sig]
        if (source not in old_node.annotations
            and source in new_node.annotations):
          old_node.annotations[source] = new_node.annotations[source]
          changed = True
        # Maybe rename arguments:
        if isinstance(old_node, IDLOperation):
          for i in range(0, len(old_node.arguments)):
            old_arg_name = old_node.arguments[i].id
            new_arg_name = new_node.arguments[i].id
            if (old_arg_name != new_arg_name
                and (old_arg_name == 'arg'
                     or old_arg_name.endswith('Arg')
                     or import_options.rename_operation_arguments_on_merge)):
              old_node.arguments[i].id = new_arg_name
              changed = True
        # Maybe merge annotations:
        if (isinstance(old_node, IDLAttribute) or
            isinstance(old_node, IDLOperation)):
          for (name, value) in new_node.ext_attrs.items():
            if name in old_node.ext_attrs and old_node.ext_attrs[name] == value:
              pass # Identical
            else:
              old_node.ext_attrs[name] = value
              changed = True

    # Remove annotations on obsolete items from the same source
    if import_options.obsolete_old_declarations:
      for (sig, old_node) in old_signatures_map.items():
        if (source in old_node.annotations
            and sig not in new_signatures_map):
          _logger.warn('%s not available in %s anymore' %
            (sig, source))
          del old_node.annotations[source]
          changed = True

    return changed

  def _merge_interfaces(self, old_interface, new_interface, import_options):
    """Merges the new_interface into the old_interface, annotating the
    interface with the sources of each change."""

    changed = False

    source = import_options.source
    if (source and source not in old_interface.annotations and
        source in new_interface.annotations and
        not new_interface.is_supplemental):
      old_interface.annotations[source] = new_interface.annotations[source]
      changed = True

    def merge_list(what):
      old_list = old_interface.__dict__[what]
      new_list = new_interface.__dict__[what]
      changed = self._merge_nodes(old_list, new_list, import_options)

      # Delete list items with zero remaining annotations.
      if changed and import_options.obsolete_old_declarations:

        def has_annotations(idl_node):
          return len(idl_node.annotations)

        old_interface.__dict__[what] = filter(has_annotations, old_list)

      return changed

    # Smartly merge various declarations:
    if merge_list('parents'):
      changed = True
    if merge_list('constants'):
      changed = True
    if merge_list('attributes'):
      changed = True
    if merge_list('operations'):
      changed = True

    # Snippets are just concatentated:
    if new_interface.snippets:
      old_interface.snippets.extend(
        copy.deepcopy(new_interface.snippets))
      changed = True

    _logger.info('merged interface %s (changed=%s, supplemental=%s)' %
      (old_interface.id, changed, new_interface.is_supplemental))

    return changed

  def _merge_impl_stmt(self, impl_stmt, import_options):
    """Applies "X implements Y" statemetns on the proper places in the
    database"""
    implementor_name = impl_stmt.implementor.id
    implemented_name = impl_stmt.implemented.id
    _logger.info('merging impl stmt %s implements %s' %
                 (implementor_name, implemented_name))

    if implementor_name == implemented_name:
      # After renaming, this might happen (e.g. Window impls
      # AbstractView, but AbstractView was renamed to Window).
      return

    source = import_options.source
    if self._database.HasInterface(implementor_name):
      interface = self._database.GetInterface(implementor_name)
      if interface.parents is None:
        interface.parents = []
      for parent in interface.parents:
        if parent.type.id == implemented_name:
          if source and source not in parent.annotations:
            parent.annotations[source] = IDLAnnotation(
                import_options.source_attributes)
          return
      # not found, so add new one
      parent = IDLParentInterface(None)
      parent.type = IDLType(implemented_name)
      if source:
        parent.annotations[source] = IDLAnnotation(
            import_options.source_attributes)
      interface.parents.append(parent)

  def set_same_signatures(self, signatures_table):
    """Customize signature calculations by providing a custom table
    indicating which types are equivalent"""
    self._same_signatures = signatures_table

  def merge_imported_interfaces(self):
    """Merges all imported interfaces and loads them into the DB."""

    # Step 1: Pre process imported interfaces
    for interface, module_name, import_options in self._imported_interfaces:
      self._split_declarations(interface)
      self._annotate(interface, module_name, import_options)

    # Step 2: Add all new interfaces and merge overlapping ones
    for interface, module_name, import_options in self._imported_interfaces:
      if not self._database.HasInterface(interface.id):
        if import_options.add_new_interfaces:
          self._database.AddInterface(interface)
      elif not interface.is_supplemental:
        old_interface = self._database.GetInterface(interface.id)
        self._merge_interfaces(old_interface, interface,
          import_options)

    # Step 3: Merge in supplemental interfaces
    for interface, module_name, import_options in self._imported_interfaces:
      if (interface.is_supplemental
          and self._database.HasInterface(interface.id)):
        old_interface = self._database.GetInterface(interface.id)
        self._merge_interfaces(old_interface, interface,
          import_options)

    # Step 4: Resolve 'implements' statements
    for impl_stmt, import_options in self._impl_stmts:
      self._merge_impl_stmt(impl_stmt, import_options)

    self._impl_stmts = []
    self._imported_interfaces = []

  def import_idl_file(self, file_path,
            import_options=DatabaseBuilderOptions()):
    """Parses, loads into memory and cleans up and IDL file"""
    idl_file = self._load_idl_file(file_path, import_options)

    self._strip_ext_attributes(idl_file)
    self._resolve_type_defs(idl_file)
    self._rename_types(idl_file, import_options)

    for module in idl_file.modules:
      for interface in module.interfaces:
        _logger.info('importing interface %s/%s (source=%s file=%s)'
          % (module.id, interface.id, import_options.source,
             file_path))
        self._imported_interfaces.append(
          (interface, module.id, import_options))
      for implStmt in module.implementsStatements:
        self._impl_stmts.append((implStmt, import_options))

  def import_idl_directory(self, directory_path,
               import_options=DatabaseBuilderOptions()):
    """Parses, loads into memory and cleans up all IDL files in a given
    directory"""
    if not os.path.exists(directory_path):
      raise RuntimeError('directory not found: %s' % directory_path)

    def visitor(arg, dir_name, names):
      module = dir_name[len(directory_path) + 1:]
      for name in names:
        file_name = os.path.join(dir_name, name)
        (interface, ext) = os.path.splitext(file_name)
        if ext == '.idl' and not name.startswith('._'):
          self.import_idl_file(file_name, import_options)
    os.path.walk(directory_path, visitor, None)

  def fix_displacements(self, source):
    """E.g. In W3C, something is declared on HTMLDocument but in WebKit
    its on Document, so we need to mark that something in HTMLDocument
    with @WebKit(via=Document). The 'via' attribute specifies the
    parent interface that has the declaration."""

    for interface in self._database.GetInterfaces():
      changed = False

      _logger.info('fixing displacements in %s' % interface.id)

      for parent_interface in self._get_parent_interfaces(interface):
        _logger.info('scanning parent %s of %s' %
          (parent_interface.id, interface.id))

        def fix_nodes(local_list, parent_list):
          changed = False
          parent_signatures_map = self._build_signatures_map(
            parent_list)
          for idl_node in local_list:
            sig = self._sign(idl_node)
            if sig in parent_signatures_map:
              parent_member = parent_signatures_map[sig]
              if (source in parent_member.annotations
                  and source not in idl_node.annotations
                  and _VIA_ANNOTATION_ATTR_NAME
                      not in parent_member.annotations[source]):
                idl_node.annotations[source] = IDLAnnotation(
                    {_VIA_ANNOTATION_ATTR_NAME: parent_interface.id})
                changed = True
          return changed

        changed = fix_nodes(interface.constants,
                  parent_interface.constants) or changed
        changed = fix_nodes(interface.attributes,
                  parent_interface.attributes) or changed
        changed = fix_nodes(interface.operations,
                  parent_interface.operations) or changed
      if changed:
        _logger.info('fixed displaced declarations in %s' %
          interface.id)

  def normalize_annotations(self, sources):
    """Makes the IDLs less verbose by removing annotation attributes
    that are identical to the ones defined at the interface level.

    Args:
      sources -- list of source names to normalize."""
    for interface in self._database.GetInterfaces():
      _logger.debug('normalizing annotations for %s' % interface.id)
      for source in sources:
        if (source not in interface.annotations or
            not interface.annotations[source]):
          continue
        top_level_annotation = interface.annotations[source]

        def normalize(idl_node):
          if (source in idl_node.annotations
              and idl_node.annotations[source]):
            annotation = idl_node.annotations[source]
            for name, value in annotation.items():
              if (name in top_level_annotation
                  and value == top_level_annotation[name]):
                del annotation[name]

        map(normalize, interface.parents)
        map(normalize, interface.constants)
        map(normalize, interface.attributes)
        map(normalize, interface.operations)
        map(normalize, interface.snippets)
