#!/usr/bin/python
# Copyright 2011 Google Inc. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Parser for tr-069-style data model .xml files."""

__author__ = 'apenwarr@google.com (Avery Pennarun)'

import os.path
import re
import string
import sys
import xml.etree.ElementTree

import google3
import bup.options


optspec = """
parse-schema.py [-d dir] files...
--
d,output-dir= Directory to write files to
"""

DEFAULT_BASE_CLASS = 'core.Exporter'

chunks = {}
imports = {}


def Log(s):
  sys.stdout.flush()
  sys.stderr.write('%s\n' % s)


def AddChunk(spec, objtype, name, root):
  key = (spec, objtype, name)
  assert not chunks.has_key(key)
  chunks[key] = root


def FixSpec(spec):
  # When a spec refers to tr-xxx-1-0-0 or tr-xxx-1-0, we might have to
  # substitute in tr-xxx-1-0-1 instead (a bugfix revision).  Let's just
  # drop out the third version digit so it's easier to use as a dictionary
  # key.
  return re.sub(r':(tr|wt)-(\d+-\d+-\d+)-\d+$', r':tr-\2', spec)


def NiceSpec(spec):
  spec = re.sub(r'^urn:broadband-forum-org:', '', spec)
  spec = re.sub(r'^urn:google-com:', '', spec)
  spec = re.sub(r'^urn:catawampus-org:', '', spec)
  return spec


def SpecNameForPython(spec):
  spec = NiceSpec(spec)
  spec = re.sub(r'tr-(\d+)-(\d+)-(\d+)', r'tr\1_v\2_\3', spec)
  spec = spec.translate(string.maketrans('-', '_'))
  return spec


def ObjNameForPython(name):
  name = re.sub(r':(\d+)\.(\d+)', r'_v\1_\2', name)
  name = name.replace('-', '_')  # X_EXAMPLE-COM_foo vendor data models
  return name


def Indented(prefix, s):
  s = unicode(s)
  s = re.sub(re.compile(r'^', re.M), prefix, s)
  s = re.sub(re.compile(r'^\s+$', re.M), '', s)
  return s


IMPORT_BUG_FIXES = {
    # bugs in tr-181-2-0-1.  It tries to import *_Device2, which doesn't
    # seem to exist anywhere.
    ('urn:broadband-forum-org:tr-143-1-0', 'component',
     'DownloadDiagnostics_Device2'):
        ('urn:broadband-forum-org:tr-143-1-0', 'component',
         'DownloadDiagnostics'),
    ('urn:broadband-forum-org:tr-143-1-0', 'component',
     'UploadDiagnostics_Device2'):
        ('urn:broadband-forum-org:tr-143-1-0', 'component',
         'UploadDiagnostics'),
}


def ParseImports(into_spec, root):
  from_spec = FixSpec(root.attrib['spec'])
  for node in root:
    if node.tag in ('component', 'model'):
      from_name = node.attrib.get('ref', node.attrib['name'])
      into_name = node.attrib['name']
      from_key = (from_spec, node.tag, from_name)
      into_key = (into_spec, node.tag, into_name)
      if from_key in IMPORT_BUG_FIXES:
        from_key = IMPORT_BUG_FIXES[from_key]
      assert not chunks.has_key(into_key)
      assert not imports.has_key(into_key)
      imports[into_key] = from_key
    elif node.tag == 'dataType':
      continue
    else:
      raise KeyError(node.tag)


def ParseFile(filename):
  Log(filename)
  root = xml.etree.ElementTree.parse(open(filename)).getroot()
  spec = FixSpec(root.attrib['spec'])
  Log(NiceSpec(spec))
  for node in root:
    if node.tag == 'import':
      ParseImports(spec, node)
    elif node.tag in ('component', 'model'):
      name = node.attrib['name']
      Log('%-12s %-9s %s' % (NiceSpec(spec), node.tag, name))
      AddChunk(spec, node.tag, name, (spec, name, node))
    elif node.tag in ('description', 'dataType', 'bibliography'):
      continue
    else:
      Log('skip %s' % node.tag)


def ResolveImports():
  for k, v in sorted(imports.items()):
    prefix = ' %-12s %-9s %-20s ' % (NiceSpec(k[0]), k[1], k[2])
    Log('%s\n=%-12s %-9s %s' % (prefix, NiceSpec(v[0]), v[1], v[2]))
    while v in imports:
      v = imports[v]
      Log('=%-12s %-9s %s' % (NiceSpec(v[0]), v[1], v[2]))
    (into_spec, objtype, into_name) = k
    (from_spec, objtype, from_name) = v
    if objtype in ('component', 'model'):
      AddChunk(into_spec, objtype, into_name,
               chunks[(from_spec, objtype, from_name)])
    else:
      raise KeyError(objtype)


class Object(object):
  """Represents an <object> tag."""

  def __init__(self, model, name, prefix):
    self.model = model
    self.name = re.sub(r'-{i}', '', name)
    self.is_sequence = (self.name != name)
    self.prefix = prefix
    self.params = []
    self.object_sequence = []

  def __str__(self):
    pre = []
    out = []
    parent_class_name = DEFAULT_BASE_CLASS
    if self.model.parent_model_name:
      parent_class = self.FindParentClass()
      if parent_class:
        parent_class_name = '%s.%s' % (self.model.parent_model_name,
                                       parent_class.FullName())
    if parent_class_name.endswith('.'):
      # Only happens for toplevel Model objects
      parent_class_name = parent_class_name[:-1]
    fullname_with_seq = re.sub(r'-{i}', '.{i}', '.'.join(self.prefix[:-1]))
    classname = self.name.translate(string.maketrans('-', '_'))
    pre.append('class %s(%s):' % (classname, parent_class_name))
    classpath = '%s.%s' % (self.model.name, fullname_with_seq)
    if classpath.endswith('.'):
      classpath = classpath[:-1]
    pre.append('  """Represents %s."""' % classpath)
    if self.params or self.object_sequence:
      pre.append('')
      pre.append('  def __init__(self, **defaults):')
      pre.append('    %s.__init__(self, defaults=defaults)'
                 % parent_class_name)
      bits = []
      space = ',\n                '
      if self.params:
        quoted_param_list = ["'%s'" % param for param in self.params]
        quoted_params = (space+'        ').join(quoted_param_list)
        bits.append('params=[%s]' % quoted_params)