#!/usr/bin/env python
# Copyright (c) 2012 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.

"""Nodes for PPAPI IDL AST"""

#
# IDL Node
#
# IDL Node defines the IDLAttribute and IDLNode objects which are constructed
# by the parser as it processes the various 'productions'.  The IDLAttribute
# objects are assigned to the IDLNode's property dictionary instead of being
# applied as children of The IDLNodes, so they do not exist in the final tree.
# The AST of IDLNodes is the output from the parsing state and will be used
# as the source data by the various generators.
#

import sys

from idl_log import ErrOut, InfoOut, WarnOut
from idl_propertynode import IDLPropertyNode
from idl_release import IDLRelease, IDLReleaseMap


# IDLAttribute
#
# A temporary object used by the parsing process to hold an Extended Attribute
# which will be passed as a child to a standard IDLNode.
#
class IDLAttribute(object):
  def __init__(self, name, value):
    self.cls = 'ExtAttribute'
    self.name = name
    self.value = value

  def __str__(self):
    return '%s=%s' % (self.name, self.value)

#
# IDLNode
#
# This class implements the AST tree, providing the associations between
# parents and children.  It also contains a namepsace and propertynode to
# allow for look-ups.  IDLNode is derived from IDLRelease, so it is
# version aware.
#
class IDLNode(IDLRelease):

  # Set of object IDLNode types which have a name and belong in the namespace.
  NamedSet = set(['Enum', 'EnumItem', 'File', 'Function', 'Interface',
                  'Member', 'Param', 'Struct', 'Type', 'Typedef'])

  def __init__(self, cls, filename, lineno, pos, children=None):
    # Initialize with no starting or ending Version
    IDLRelease.__init__(self, None, None)

    self.cls = cls
    self.lineno = lineno
    self.pos = pos
    self._filename = filename
    self._deps = {}
    self.errors = 0
    self.namespace = None
    self.typelist = None
    self.parent = None
    self._property_node = IDLPropertyNode()
    self._unique_releases = None

    # A list of unique releases for this node
    self.releases = None

    # A map from any release, to the first unique release
    self.first_release = None

    # self._children is a list of children ordered as defined
    self._children = []
    # Process the passed in list of children, placing ExtAttributes into the
    # property dictionary, and nodes into the local child list in order.  In
    # addition, add nodes to the namespace if the class is in the NamedSet.
    if children:
      for child in children:
        if child.cls == 'ExtAttribute':
          self.SetProperty(child.name, child.value)
        else:
          self.AddChild(child)

  def __str__(self):
    name = self.GetName()
    if name is None:
      name = ''
    return '%s(%s)' % (self.cls, name)

  def Location(self):
    """Return a file and line number for where this node was defined."""
    return '%s(%d)' % (self._filename, self.lineno)

  def Error(self, msg):
    """Log an error for this object."""
    self.errors += 1
    ErrOut.LogLine(self._filename, self.lineno, 0, ' %s %s' %
                   (str(self), msg))
    filenode = self.GetProperty('FILE')
    if filenode:
      errcnt = filenode.GetProperty('ERRORS')
      if not errcnt:
        errcnt = 0
      filenode.SetProperty('ERRORS', errcnt + 1)

  def Warning(self, msg):
    """Log a warning for this object."""
    WarnOut.LogLine(self._filename, self.lineno, 0, ' %s %s' %
                    (str(self), msg))

  def GetName(self):
    return self.GetProperty('NAME')

  def Dump(self, depth=0, comments=False, out=sys.stdout):
    """Dump this object and its children"""
    if self.cls in ['Comment', 'Copyright']:
      is_comment = True
    else:
      is_comment = False

    # Skip this node if it's a comment, and we are not printing comments
    if not comments and is_comment:
      return

    tab = ''.rjust(depth * 2)
    if is_comment:
      out.write('%sComment\n' % tab)
      for line in self.GetName().split('\n'):
        out.write('%s  "%s"\n' % (tab, line))
    else:
      ver = IDLRelease.__str__(self)
      if self.releases:
        release_list = ': ' # + ' '.join(self.releases)
      else:
        release_list = ': undefined'
      out.write('%s%s%s%s\n' % (tab, self, ver, release_list))
    if self.typelist:
      out.write('%s  Typelist: %s\n' % (tab, self.typelist.GetReleases()[0]))
    properties = self._property_node.GetPropertyList()
    if properties:
      out.write('%s  Properties\n' % tab)
      for p in properties:
        if is_comment and p == 'NAME':
          # Skip printing the name for comments, since we printed above already
          continue
        out.write('%s    %s : %s\n' % (tab, p, self.GetProperty(p)))
    for child in self._children:
      child.Dump(depth+1, comments=comments, out=out)

  def IsA(self, *typelist):
    """Check if node is of a given type."""
    return self.cls in typelist

  def GetListOf(self, *keys):
    """Get a list of objects for the given key(s)."""
    out = []
    for child in self._children:
      if child.cls in keys:
        out.append(child)
    return out

  def GetOneOf(self, *keys):
    """Get an object for the given key(s)."""
    out = self.GetListOf(*keys)
    if out:
      return out[0]
    return None

  def SetParent(self, parent):
    self._property_node.AddParent(parent)
    self.parent = parent

  def AddChild(self, node):
    node.SetParent(self)
    self._children.append(node)

  # Get a list of all children
  def GetChildren(self):
    return self._children

  def GetType(self, release):
    if not self.typelist:
      return None
    return self.typelist.FindRelease(release)

  def GetDeps(self, release, visited=None):
    visited = visited or set()

    # If this release is not valid for this object, then done.
    if not self.IsRelease(release) or self.IsA('Comment', 'Copyright'):
      return set([])

    # If we have cached the info for this release, return the cached value
    deps = self._deps.get(release, None)
    if deps is not None:
      return deps

    # If we are already visited, then return
    if self in visited:
      return set([self])

    # Otherwise, build the dependency list
    visited |= set([self])
    deps = set([self])

    # Get child deps
    for child in self.GetChildren():
      deps |= child.GetDeps(release, visited)
      visited |= set(deps)

    # Get type deps
    typeref = self.GetType(release)
    if typeref:
      deps |= typeref.GetDeps(release, visited)

    self._deps[release] = deps
    return deps

  def GetVersion(self, release):
    filenode = self.GetProperty('FILE')
    if not filenode:
      return None
    return filenode.release_map.GetVersion(release)

  def GetUniqueReleases(self, releases):
    """Return the unique set of first releases corresponding to input

    Since we are returning the corresponding 'first' version for a
    release, we may return a release version prior to the one in the list."""
    my_min, my_max = self.GetMinMax(releases)
    if my_min > releases[-1] or my_max < releases[0]:
      return []

    out = set()
    for rel in releases:
      remapped = self.first_release[rel]
      if not remapped:
        continue
      out |= set([remapped])

    # Cache the most recent set of unique_releases
    self._unique_releases = sorted(out)
    return self._unique_releases

  def LastRelease(self, release):
    # Get the most recent release from the most recently generated set of
    # cached unique releases.
    if self._unique_releases and self._unique_releases[-1] > release:
      return False
    return True

  def GetRelease(self, version):
    filenode = self.GetProperty('FILE')
    if not filenode:
      return None
    return filenode.release_map.GetRelease(version)

  def _GetReleaseList(self, releases, visited=None):
    visited = visited or set()
    if not self.releases:
      # If we are unversionable, then return first available release
      if self.IsA('Comment', 'Copyright', 'Label'):
        self.releases = []
        return self.releases

      # Generate the first and if deprecated within this subset, the
      # last release for this node
      my_min, my_max = self.GetMinMax(releases)

      if my_max != releases[-1]:
        my_releases = set([my_min, my_max])
      else:
        my_releases = set([my_min])

      r = self.GetRelease(self.GetProperty('version'))
      if not r in my_releases:
        my_releases |= set([r])

      # Break cycle if we reference ourselves
      if self in visited:
        return [my_min]

      visited |= set([self])

      # Files inherit all their releases from items in the file
      if self.IsA('AST', 'File'):
        my_releases = set()

      # Visit all children
      child_releases = set()

      # Exclude sibling results from parent visited set
      cur_visits = visited

      for child in self._children:
        child_releases |= set(child._GetReleaseList(releases, cur_visits))
        visited |= set(child_releases)

      # Visit my type
      type_releases = set()
      if self.typelist:
        type_list = self.typelist.GetReleases()
        for typenode in type_list:
          type_releases |= set(typenode._GetReleaseList(releases, cur_visits))

        type_release_list = sorted(type_releases)
        if my_min < type_release_list[0]:
          type_node = type_list[0]
          self.Error('requires %s in %s which is undefined at %s.' % (
              type_node, type_node._filename, my_min))

      for rel in child_releases | type_releases:
        if rel >= my_min and rel <= my_max:
          my_releases |= set([rel])

      self.releases = sorted(my_releases)
    return self.releases

  def BuildReleaseMap(self, releases):
    unique_list = self._GetReleaseList(releases)
    _, my_max = self.GetMinMax(releases)

    self.first_release = {}
    last_rel = None
    for rel in releases:
      if rel in unique_list:
        last_rel = rel
      self.first_release[rel] = last_rel
      if rel == my_max:
        last_rel = None

  def SetProperty(self, name, val):
    self._property_node.SetProperty(name, val)

  def GetProperty(self, name):
    return self._property_node.GetProperty(name)

  def GetPropertyLocal(self, name):
    return self._property_node.GetPropertyLocal(name)

  def NodeIsDevOnly(self):
    """Returns true iff a node is only in dev channel."""
    return self.GetProperty('dev_version') and not self.GetProperty('version')

  def DevInterfaceMatchesStable(self, release):
    """Returns true if an interface has an equivalent stable version."""
    assert(self.IsA('Interface'))
    for child in self.GetListOf('Member'):
      unique = child.GetUniqueReleases([release])
      if not unique or not child.InReleases([release]):
        continue
      if child.NodeIsDevOnly():
        return False
    return True


#
# IDLFile
#
# A specialized version of IDLNode which tracks errors and warnings.
#
class IDLFile(IDLNode):
  def __init__(self, name, children, errors=0):
    attrs = [IDLAttribute('NAME', name),
             IDLAttribute('ERRORS', errors)]
    if not children:
      children = []
    IDLNode.__init__(self, 'File', name, 1, 0, attrs + children)
    # TODO(teravest): Why do we set release map like this here? This looks
    # suspicious...
    self.release_map = IDLReleaseMap([('M13', 1.0, 'stable')])


#
# Tests
#
def StringTest():
  errors = 0
  name_str = 'MyName'
  text_str = 'MyNode(%s)' % name_str
  name_node = IDLAttribute('NAME', name_str)
  node = IDLNode('MyNode', 'no file', 1, 0, [name_node])
  if node.GetName() != name_str:
    ErrOut.Log('GetName returned >%s< not >%s<' % (node.GetName(), name_str))
    errors += 1
  if node.GetProperty('NAME') != name_str:
    ErrOut.Log('Failed to get name property.')
    errors += 1
  if str(node) != text_str:
    ErrOut.Log('str() returned >%s< not >%s<' % (str(node), text_str))
    errors += 1
  if not errors:
    InfoOut.Log('Passed StringTest')
  return errors


def ChildTest():
  errors = 0
  child = IDLNode('child', 'no file', 1, 0)
  parent = IDLNode('parent', 'no file', 1, 0, [child])

  if child.parent != parent:
    ErrOut.Log('Failed to connect parent.')
    errors += 1

  if [child] != parent.GetChildren():
    ErrOut.Log('Failed GetChildren.')
    errors += 1

  if child != parent.GetOneOf('child'):
    ErrOut.Log('Failed GetOneOf(child)')
    errors += 1

  if parent.GetOneOf('bogus'):
    ErrOut.Log('Failed GetOneOf(bogus)')
    errors += 1

  if not parent.IsA('parent'):
    ErrOut.Log('Expecting parent type')
    errors += 1

  parent = IDLNode('parent', 'no file', 1, 0, [child, child])
  if [child, child] != parent.GetChildren():
    ErrOut.Log('Failed GetChildren2.')
    errors += 1

  if not errors:
    InfoOut.Log('Passed ChildTest')
  return errors


def Main():
  errors = StringTest()
  errors += ChildTest()

  if errors:
    ErrOut.Log('IDLNode failed with %d errors.' % errors)
    return  -1
  return 0

if __name__ == '__main__':
  sys.exit(Main())

