#!/usr/bin/env python

'''Phosphor DBus Monitor YAML parser and code generator.

The parser workflow is broken down as follows:
  1 - Import YAML files as native python type(s) instance(s).
  2 - Create an instance of the Everything class from the
        native python type instance(s) with the Everything.load
        method.
  3 - The Everything class constructor orchestrates conversion of the
        native python type(s) instances(s) to render helper types.
        Each render helper type constructor imports its attributes
        from the native python type(s) instances(s).
  4 - Present the converted YAML to the command processing method
        requested by the script user.
'''

import os
import sys
import yaml
import mako.lookup
from argparse import ArgumentParser
from sdbusplus.renderer import Renderer
from sdbusplus.namedelement import NamedElement
import sdbusplus.property


class InvalidConfigError(BaseException):
    '''General purpose config file parsing error.'''

    def __init__(self, path, msg):
        '''Display configuration file with the syntax
        error and the error message.'''

        self.config = path
        self.msg = msg


class NotUniqueError(InvalidConfigError):
    '''Within a config file names must be unique.
    Display the config file with the duplicate and
    the duplicate itself.'''

    def __init__(self, path, cls, *names):
        fmt = 'Duplicate {0}: "{1}"'
        super(NotUniqueError, self).__init__(
            path, fmt.format(cls, ' '.join(names)))


def get_index(objs, cls, name, config=None):
    '''Items are usually rendered as C++ arrays and as
    such are stored in python lists.  Given an item name
    its class, and an optional config file filter, find
    the item index.'''

    for i, x in enumerate(objs.get(cls, [])):
        if config and x.configfile != config:
            continue
        if x.name != name:
            continue

        return i
    raise InvalidConfigError(config, 'Could not find name: "{0}"'.format(name))


def exists(objs, cls, name, config=None):
    '''Check to see if an item already exists in a list given
    the item name.'''

    try:
        get_index(objs, cls, name, config)
    except:
        return False

    return True


def add_unique(obj, *a, **kw):
    '''Add an item to one or more lists unless already present,
    with an option to constrain the search to a specific config file.'''

    for container in a:
        if not exists(container, obj.cls, obj.name, config=kw.get('config')):
            container.setdefault(obj.cls, []).append(obj)


class Cast(object):
    '''Decorate an argument by casting it.'''

    def __init__(self, cast, target):
        '''cast is the cast type (static, const, etc...).
           target is the cast target type.'''
        self.cast = cast
        self.target = target

    def __call__(self, arg):
        return '{0}_cast<{1}>({2})'.format(self.cast, self.target, arg)


class Literal(object):
    '''Decorate an argument with a literal operator.'''

    integer_types = [
        'int16',
        'int32',
        'int64',
        'uint16',
        'uint32',
        'uint64'
    ]

    def __init__(self, type):
        self.type = type

    def __call__(self, arg):
        if 'uint' in self.type:
            arg = '{0}ull'.format(arg)
        elif 'int' in self.type:
            arg = '{0}ll'.format(arg)

        if self.type in self.integer_types:
            return Cast('static', '{0}_t'.format(self.type))(arg)
        elif self.type == 'byte':
            return Cast('static', 'uint8_t'.format(self.type))(arg)
        elif self.type == 'double':
            return Cast('static', 'double'.format(self.type))(arg)

        if self.type == 'string':
            return '{0}s'.format(arg)

        return arg


class FixBool(object):
    '''Un-capitalize booleans.'''

    def __call__(self, arg):
        return '{0}'.format(arg.lower())


class Quote(object):
    '''Decorate an argument by quoting it.'''

    def __call__(self, arg):
        return '"{0}"'.format(arg)


class Argument(NamedElement, Renderer):
    '''Define argument type interface.'''

    def __init__(self, **kw):
        self.type = kw.pop('type', None)
        super(Argument, self).__init__(**kw)

    def argument(self, loader, indent):
        raise NotImplementedError


class TrivialArgument(Argument):
    '''Non-array type arguments.'''

    def __init__(self, **kw):
        self.value = kw.pop('value')
        self.decorators = kw.pop('decorators', [])
        if kw.get('type', None):
            self.decorators.insert(0, Literal(kw['type']))
        if kw.get('type', None) == 'string':
            self.decorators.insert(0, Quote())
        if kw.get('type', None) == 'boolean':
            self.decorators.insert(0, FixBool())

        super(TrivialArgument, self).__init__(**kw)

    def argument(self, loader, indent):
        a = str(self.value)
        for d in self.decorators:
            a = d(a)

        return a


class Metadata(Argument):
    '''Metadata type arguments.'''

    def __init__(self, **kw):
        self.value = kw.pop('value')
        self.decorators = kw.pop('decorators', [])
        if kw.get('type', None) == 'string':
            self.decorators.insert(0, Quote())

        super(Metadata, self).__init__(**kw)

    def argument(self, loader, indent):
        a = str(self.value)
        for d in self.decorators:
            a = d(a)

        return a


class Indent(object):
    '''Help templates be depth agnostic.'''

    def __init__(self, depth=0):
        self.depth = depth

    def __add__(self, depth):
        return Indent(self.depth + depth)

    def __call__(self, depth):
        '''Render an indent at the current depth plus depth.'''
        return 4*' '*(depth + self.depth)


class ConfigEntry(NamedElement):
    '''Base interface for rendered items.'''

    def __init__(self, *a, **kw):
        '''Pop the configfile/class/subclass keywords.'''

        self.configfile = kw.pop('configfile')
        self.cls = kw.pop('class')
        self.subclass = kw.pop(self.cls)
        super(ConfigEntry, self).__init__(**kw)

    def factory(self, objs):
        ''' Optional factory interface for subclasses to add
        additional items to be rendered.'''

        pass

    def setup(self, objs):
        ''' Optional setup interface for subclasses, invoked
        after all factory methods have been run.'''

        pass


class Path(ConfigEntry):
    '''Path/metadata association.'''

    def __init__(self, *a, **kw):
        super(Path, self).__init__(**kw)

        if self.name['meta'].upper() != self.name['meta']:
            raise InvalidConfigError(
                self.configfile,
                'Metadata tag "{0}" must be upper case.'.format(
                    self.name['meta']))

    def factory(self, objs):
        '''Create path and metadata elements.'''

        args = {
            'class': 'pathname',
            'pathname': 'element',
            'name': self.name['path']
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'meta',
            'meta': 'element',
            'name': self.name['meta']
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        super(Path, self).factory(objs)

    def setup(self, objs):
        '''Resolve path and metadata names to indices.'''

        self.path = get_index(
            objs, 'pathname', self.name['path'])
        self.meta = get_index(
            objs, 'meta', self.name['meta'])

        super(Path, self).setup(objs)


class Property(ConfigEntry):
    '''Property/interface/metadata association.'''

    def __init__(self, *a, **kw):
        super(Property, self).__init__(**kw)

        if self.name['meta'].upper() != self.name['meta']:
            raise InvalidConfigError(
                self.configfile,
                'Metadata tag "{0}" must be upper case.'.format(
                    self.name['meta']))

    def factory(self, objs):
        '''Create interface, property name and metadata elements.'''

        args = {
            'class': 'interface',
            'interface': 'element',
            'name': self.name['interface']
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'propertyname',
            'propertyname': 'element',
            'name': self.name['property']
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'meta',
            'meta': 'element',
            'name': self.name['meta']
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        super(Property, self).factory(objs)

    def setup(self, objs):
        '''Resolve interface, property and metadata to indices.'''

        self.interface = get_index(
            objs, 'interface', self.name['interface'])
        self.prop = get_index(
            objs, 'propertyname', self.name['property'])
        self.meta = get_index(
            objs, 'meta', self.name['meta'])

        super(Property, self).setup(objs)


class Instance(ConfigEntry):
    '''Property/Path association.'''

    def __init__(self, *a, **kw):
        super(Instance, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve elements to indices.'''

        self.interface = get_index(
            objs, 'interface', self.name['property']['interface'])
        self.prop = get_index(
            objs, 'propertyname', self.name['property']['property'])
        self.propmeta = get_index(
            objs, 'meta', self.name['property']['meta'])
        self.path = get_index(
            objs, 'pathname', self.name['path']['path'])
        self.pathmeta = get_index(
            objs, 'meta', self.name['path']['meta'])

        super(Instance, self).setup(objs)

class PathInstance(ConfigEntry):
    '''Path association.'''

    def __init__(self, *a, **kw):
        super(PathInstance, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve elements to indices.'''
        self.path = self.name['path']['path']
        self.pathmeta = self.name['path']['meta']
        super(PathInstance, self).setup(objs)

class Group(ConfigEntry):
    '''Pop the members keyword for groups.'''

    def __init__(self, *a, **kw):
        self.members = kw.pop('members')
        super(Group, self).__init__(**kw)


class ImplicitGroup(Group):
    '''Provide a factory method for groups whose members are
    not explicitly declared in the config files.'''

    def __init__(self, *a, **kw):
        super(ImplicitGroup, self).__init__(**kw)

    def factory(self, objs):
        '''Create group members.'''

        factory = Everything.classmap(self.subclass, 'element')
        for m in self.members:
            args = {
                'class': self.subclass,
                self.subclass: 'element',
                'name': m
            }

            obj = factory(configfile=self.configfile, **args)
            add_unique(obj, objs)
            obj.factory(objs)

        super(ImplicitGroup, self).factory(objs)


class GroupOfPaths(ImplicitGroup):
    '''Path group config file directive.'''

    def __init__(self, *a, **kw):
        super(GroupOfPaths, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            path = get_index(
                objs, 'pathname', x['path'])
            meta = get_index(
                objs, 'meta', x['meta'])
            return (path, meta)

        self.members = map(
            map_member,
            self.members)

        super(GroupOfPaths, self).setup(objs)


class GroupOfProperties(ImplicitGroup):
    '''Property group config file directive.'''

    def __init__(self, *a, **kw):
        self.type = kw.pop('type')
        self.datatype = sdbusplus.property.Property(
            name=kw.get('name'),
            type=self.type).cppTypeName

        super(GroupOfProperties, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            iface = get_index(
                objs, 'interface', x['interface'])
            prop = get_index(
                objs, 'propertyname', x['property'])
            meta = get_index(
                objs, 'meta', x['meta'])

            return (iface, prop, meta)

        self.members = map(
            map_member,
            self.members)

        super(GroupOfProperties, self).setup(objs)


class GroupOfInstances(ImplicitGroup):
    '''A group of property instances.'''

    def __init__(self, *a, **kw):
        super(GroupOfInstances, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            path = get_index(objs, 'pathname', x['path']['path'])
            pathmeta = get_index(objs, 'meta', x['path']['meta'])
            interface = get_index(
                objs, 'interface', x['property']['interface'])
            prop = get_index(objs, 'propertyname', x['property']['property'])
            propmeta = get_index(objs, 'meta', x['property']['meta'])
            instance = get_index(objs, 'instance', x)

            return (path, pathmeta, interface, prop, propmeta, instance)

        self.members = map(
            map_member,
            self.members)

        super(GroupOfInstances, self).setup(objs)

class GroupOfPathInstances(ImplicitGroup):
    '''A group of path instances.'''

    def __init__(self, *a, **kw):
        super(GroupOfPathInstances, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            path = get_index(objs, 'pathname', x['path']['path'])
            pathmeta = get_index(objs, 'meta', x['path']['meta'])
            pathinstance = get_index(objs, 'pathinstance', x)
            return (path, pathmeta, pathinstance)

        self.members = map(
            map_member,
            self.members)

        super(GroupOfPathInstances, self).setup(objs)


class HasPropertyIndex(ConfigEntry):
    '''Handle config file directives that require an index to be
    constructed.'''

    def __init__(self, *a, **kw):
        self.paths = kw.pop('paths')
        self.properties = kw.pop('properties')
        super(HasPropertyIndex, self).__init__(**kw)

    def factory(self, objs):
        '''Create a group of instances for this index.'''

        members = []
        path_group = get_index(
            objs, 'pathgroup', self.paths, config=self.configfile)
        property_group = get_index(
            objs, 'propertygroup', self.properties, config=self.configfile)

        for path in objs['pathgroup'][path_group].members:
            for prop in objs['propertygroup'][property_group].members:
                member = {
                    'path': path,
                    'property': prop,
                }
                members.append(member)

        args = {
            'members': members,
            'class': 'instancegroup',
            'instancegroup': 'instance',
            'name': '{0} {1}'.format(self.paths, self.properties)
        }

        group = GroupOfInstances(configfile=self.configfile, **args)
        add_unique(group, objs, config=self.configfile)
        group.factory(objs)

        super(HasPropertyIndex, self).factory(objs)

    def setup(self, objs):
        '''Resolve path, property, and instance groups.'''

        self.instances = get_index(
            objs,
            'instancegroup',
            '{0} {1}'.format(self.paths, self.properties),
            config=self.configfile)
        self.paths = get_index(
            objs,
            'pathgroup',
            self.paths,
            config=self.configfile)
        self.properties = get_index(
            objs,
            'propertygroup',
            self.properties,
            config=self.configfile)
        self.datatype = objs['propertygroup'][self.properties].datatype
        self.type = objs['propertygroup'][self.properties].type

        super(HasPropertyIndex, self).setup(objs)

class HasPathIndex(ConfigEntry):
    '''Handle config file directives that require an index to be
    constructed.'''

    def __init__(self, *a, **kw):
        self.paths = kw.pop('paths')
        super(HasPathIndex, self).__init__(**kw)

    def factory(self, objs):
        '''Create a group of instances for this index.'''

        members = []
        path_group = get_index(
            objs, 'pathgroup', self.paths, config=self.configfile)

        for path in objs['pathgroup'][path_group].members:
            member = {
                'path': path,
            }
            members.append(member)

        args = {
            'members': members,
            'class': 'pathinstancegroup',
            'pathinstancegroup': 'pathinstance',
            'name': '{0}'.format(self.paths)
        }

        group = GroupOfPathInstances(configfile=self.configfile, **args)
        add_unique(group, objs, config=self.configfile)
        group.factory(objs)

        super(HasPathIndex, self).factory(objs)

    def setup(self, objs):
        '''Resolve path and instance groups.'''

        self.pathinstances = get_index(
            objs,
            'pathinstancegroup',
            '{0}'.format(self.paths),
            config=self.configfile)
        self.paths = get_index(
            objs,
            'pathgroup',
            self.paths,
            config=self.configfile)
        super(HasPathIndex, self).setup(objs)

class PropertyWatch(HasPropertyIndex):
    '''Handle the property watch config file directive.'''

    def __init__(self, *a, **kw):
        self.callback = kw.pop('callback', None)
        super(PropertyWatch, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve optional callback.'''

        if self.callback:
            self.callback = get_index(
                objs,
                'callback',
                self.callback,
                config=self.configfile)

        super(PropertyWatch, self).setup(objs)

class PathWatch(HasPathIndex):
    '''Handle the path watch config file directive.'''

    def __init__(self, *a, **kw):
        self.pathcallback = kw.pop('pathcallback', None)
        super(PathWatch, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve optional callback.'''
        if self.pathcallback:
            self.pathcallback = get_index(
                objs,
                'pathcallback',
                self.pathcallback,
                config=self.configfile)
        super(PathWatch, self).setup(objs)


class Callback(HasPropertyIndex):
    '''Interface and common logic for callbacks.'''

    def __init__(self, *a, **kw):
        super(Callback, self).__init__(**kw)

class PathCallback(HasPathIndex):
    '''Interface and common logic for callbacks.'''

    def __init__(self, *a, **kw):
        super(PathCallback, self).__init__(**kw)

class ConditionCallback(ConfigEntry, Renderer):
    '''Handle the journal callback config file directive.'''

    def __init__(self, *a, **kw):
        self.condition = kw.pop('condition')
        self.instance = kw.pop('instance')
        self.defer = kw.pop('defer', None)
        super(ConditionCallback, self).__init__(**kw)

    def factory(self, objs):
        '''Create a graph instance for this callback.'''

        args = {
            'configfile': self.configfile,
            'members': [self.instance],
            'class': 'callbackgroup',
            'callbackgroup': 'callback',
            'name': [self.instance]
        }

        entry = CallbackGraphEntry(**args)
        add_unique(entry, objs, config=self.configfile)

        super(ConditionCallback, self).factory(objs)

    def setup(self, objs):
        '''Resolve condition and graph entry.'''

        self.graph = get_index(
            objs,
            'callbackgroup',
            [self.instance],
            config=self.configfile)

        self.condition = get_index(
            objs,
            'condition',
            self.name,
            config=self.configfile)

        super(ConditionCallback, self).setup(objs)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'conditional.mako.cpp',
            c=self,
            indent=indent)


class Condition(HasPropertyIndex):
    '''Interface and common logic for conditions.'''

    def __init__(self, *a, **kw):
        self.callback = kw.pop('callback')
        self.defer = kw.pop('defer', None)
        super(Condition, self).__init__(**kw)

    def factory(self, objs):
        '''Create a callback instance for this conditional.'''

        args = {
            'configfile': self.configfile,
            'condition': self.name,
            'class': 'callback',
            'callback': 'conditional',
            'instance': self.callback,
            'name': self.name,
            'defer': self.defer
        }

        callback = ConditionCallback(**args)
        add_unique(callback, objs, config=self.configfile)
        callback.factory(objs)

        super(Condition, self).factory(objs)


class CountCondition(Condition, Renderer):
    '''Handle the count condition config file directive.'''

    def __init__(self, *a, **kw):
        self.countop = kw.pop('countop')
        self.countbound = kw.pop('countbound')
        self.op = kw.pop('op')
        self.bound = kw.pop('bound')
        self.oneshot = TrivialArgument(
            type='boolean',
            value=kw.pop('oneshot', False))
        super(CountCondition, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve type.'''

        super(CountCondition, self).setup(objs)
        self.bound = TrivialArgument(
            type=self.type,
            value=self.bound)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'count.mako.cpp',
            c=self,
            indent=indent)


class Journal(Callback, Renderer):
    '''Handle the journal callback config file directive.'''

    def __init__(self, *a, **kw):
        self.severity = kw.pop('severity')
        self.message = kw.pop('message')
        super(Journal, self).__init__(**kw)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'journal.mako.cpp',
            c=self,
            indent=indent)


class Elog(Callback, Renderer):
    '''Handle the elog callback config file directive.'''

    def __init__(self, *a, **kw):
        self.error = kw.pop('error')
        self.metadata = [Metadata(**x) for x in kw.pop('metadata', {})]
        super(Elog, self).__init__(**kw)

    def construct(self, loader, indent):
        with open('errors.hpp', 'a') as fd:
            fd.write(
                self.render(
                    loader,
                    'errors.mako.hpp',
                    c=self))
        return self.render(
            loader,
            'elog.mako.cpp',
            c=self,
            indent=indent)

class Event(Callback, Renderer):
    '''Handle the event callback config file directive.'''

    def __init__(self, *a, **kw):
        self.eventName = kw.pop('eventName')
        self.eventMessage = kw.pop('eventMessage')
        super(Event, self).__init__(**kw)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'event.mako.cpp',
            c=self,
            indent=indent)

class EventPath(PathCallback, Renderer):
    '''Handle the event path callback config file directive.'''

    def __init__(self, *a, **kw):
        self.eventType = kw.pop('eventType')
        super(EventPath, self).__init__(**kw)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'eventpath.mako.cpp',
            c=self,
            indent=indent)

class ElogWithMetadata(Callback, Renderer):
    '''Handle the elog_with_metadata callback config file directive.'''

    def __init__(self, *a, **kw):
        self.error = kw.pop('error')
        self.metadata = kw.pop('metadata')
        super(ElogWithMetadata, self).__init__(**kw)

    def construct(self, loader, indent):
        with open('errors.hpp', 'a') as fd:
            fd.write(
                self.render(
                    loader,
                    'errors.mako.hpp',
                    c=self))
        return self.render(
            loader,
            'elog_with_metadata.mako.cpp',
            c=self,
            indent=indent)


class ResolveCallout(Callback, Renderer):
    '''Handle the 'resolve callout' callback config file directive.'''

    def __init__(self, *a, **kw):
        self.callout = kw.pop('callout')
        super(ResolveCallout, self).__init__(**kw)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'resolve_errors.mako.cpp',
            c=self,
            indent=indent)


class Method(ConfigEntry, Renderer):
    '''Handle the method callback config file directive.'''

    def __init__(self, *a, **kw):
        self.service = kw.pop('service')
        self.path = kw.pop('path')
        self.interface = kw.pop('interface')
        self.method = kw.pop('method')
        self.args = [TrivialArgument(**x) for x in kw.pop('args', {})]
        super(Method, self).__init__(**kw)

    def factory(self, objs):
        args = {
            'class': 'interface',
            'interface': 'element',
            'name': self.service
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'pathname',
            'pathname': 'element',
            'name': self.path
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'interface',
            'interface': 'element',
            'name': self.interface
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        args = {
            'class': 'propertyname',
            'propertyname': 'element',
            'name': self.method
        }
        add_unique(ConfigEntry(
            configfile=self.configfile, **args), objs)

        super(Method, self).factory(objs)

    def setup(self, objs):
        '''Resolve elements.'''

        self.service = get_index(
            objs,
            'interface',
            self.service)

        self.path = get_index(
            objs,
            'pathname',
            self.path)

        self.interface = get_index(
            objs,
            'interface',
            self.interface)

        self.method = get_index(
            objs,
            'propertyname',
            self.method)

        super(Method, self).setup(objs)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'method.mako.cpp',
            c=self,
            indent=indent)


class CallbackGraphEntry(Group):
    '''An entry in a traversal list for groups of callbacks.'''

    def __init__(self, *a, **kw):
        super(CallbackGraphEntry, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            return get_index(
                objs, 'callback', x, config=self.configfile)

        self.members = map(
            map_member,
            self.members)

        super(CallbackGraphEntry, self).setup(objs)

class PathCallbackGraphEntry(Group):
    '''An entry in a traversal list for groups of callbacks.'''

    def __init__(self, *a, **kw):
        super(PathCallbackGraphEntry, self).__init__(**kw)

    def setup(self, objs):
        '''Resolve group members.'''

        def map_member(x):
            return get_index(
                objs, 'pathcallback', x, config=self.configfile)

        self.members = map(
            map_member,
            self.members)

        super(PathCallbackGraphEntry, self).setup(objs)

class GroupOfCallbacks(ConfigEntry, Renderer):
    '''Handle the callback group config file directive.'''

    def __init__(self, *a, **kw):
        self.members = kw.pop('members')
        super(GroupOfCallbacks, self).__init__(**kw)

    def factory(self, objs):
        '''Create a graph instance for this group of callbacks.'''

        args = {
            'configfile': self.configfile,
            'members': self.members,
            'class': 'callbackgroup',
            'callbackgroup': 'callback',
            'name': self.members
        }

        entry = CallbackGraphEntry(**args)
        add_unique(entry, objs, config=self.configfile)

        super(GroupOfCallbacks, self).factory(objs)

    def setup(self, objs):
        '''Resolve graph entry.'''

        self.graph = get_index(
            objs, 'callbackgroup', self.members, config=self.configfile)

        super(GroupOfCallbacks, self).setup(objs)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'callbackgroup.mako.cpp',
            c=self,
            indent=indent)

class GroupOfPathCallbacks(ConfigEntry, Renderer):
    '''Handle the callback group config file directive.'''

    def __init__(self, *a, **kw):
        self.members = kw.pop('members')
        super(GroupOfPathCallbacks, self).__init__(**kw)

    def factory(self, objs):
        '''Create a graph instance for this group of callbacks.'''

        args = {
            'configfile': self.configfile,
            'members': self.members,
            'class': 'pathcallbackgroup',
            'pathcallbackgroup': 'pathcallback',
            'name': self.members
        }

        entry = PathCallbackGraphEntry(**args)
        add_unique(entry, objs, config=self.configfile)
        super(GroupOfPathCallbacks, self).factory(objs)

    def setup(self, objs):
        '''Resolve graph entry.'''

        self.graph = get_index(
            objs, 'callbackpathgroup', self.members, config=self.configfile)

        super(GroupOfPathCallbacks, self).setup(objs)

    def construct(self, loader, indent):
        return self.render(
            loader,
            'callbackpathgroup.mako.cpp',
            c=self,
            indent=indent)

class Everything(Renderer):
    '''Parse/render entry point.'''

    @staticmethod
    def classmap(cls, sub=None):
        '''Map render item class and subclass entries to the appropriate
        handler methods.'''
        class_map = {
            'path': {
                'element': Path,
            },
            'pathgroup': {
                'path': GroupOfPaths,
            },
            'propertygroup': {
                'property': GroupOfProperties,
            },
            'property': {
                'element': Property,
            },
            'watch': {
                'property': PropertyWatch,
            },
            'pathwatch': {
                'path': PathWatch,
            },
            'instance': {
                'element': Instance,
            },
            'pathinstance': {
                'element': PathInstance,
            },
            'callback': {
                'journal': Journal,
                'elog': Elog,
                'elog_with_metadata': ElogWithMetadata,
                'event': Event,
                'group': GroupOfCallbacks,
                'method': Method,
                'resolve callout': ResolveCallout,
            },
            'pathcallback': {
                'eventpath': EventPath,
                'grouppath': GroupOfPathCallbacks,
            },
            'condition': {
                'count': CountCondition,
            },
        }

        if cls not in class_map:
            raise NotImplementedError('Unknown class: "{0}"'.format(cls))
        if sub not in class_map[cls]:
            raise NotImplementedError('Unknown {0} type: "{1}"'.format(
                cls, sub))

        return class_map[cls][sub]

    @staticmethod
    def load_one_yaml(path, fd, objs):
        '''Parse a single YAML file.  Parsing occurs in three phases.
        In the first phase a factory method associated with each
        configuration file directive is invoked.  These factory
        methods generate more factory methods.  In the second
        phase the factory methods created in the first phase
        are invoked.  In the last phase a callback is invoked on
        each object created in phase two.  Typically the callback
        resolves references to other configuration file directives.'''

        factory_objs = {}
        for x in yaml.safe_load(fd.read()) or {}:

            # Create factory object for this config file directive.
            cls = x['class']
            sub = x.get(cls)
            if cls == 'group':
                cls = '{0}group'.format(sub)

            factory = Everything.classmap(cls, sub)
            obj = factory(configfile=path, **x)

            # For a given class of directive, validate the file
            # doesn't have any duplicate names (duplicates are
            # ok across config files).
            if exists(factory_objs, obj.cls, obj.name, config=path):
                raise NotUniqueError(path, cls, obj.name)

            factory_objs.setdefault(cls, []).append(obj)
            objs.setdefault(cls, []).append(obj)

        for cls, items in factory_objs.items():
            for obj in items:
                # Add objects for template consumption.
                obj.factory(objs)

    @staticmethod
    def load(args):
        '''Aggregate all the YAML in the input directory
        into a single aggregate.'''

        objs = {}
        yaml_files = filter(
            lambda x: x.endswith('.yaml'),
            os.listdir(args.inputdir))

        yaml_files.sort()

        for x in yaml_files:
            path = os.path.join(args.inputdir, x)
            with open(path, 'r') as fd:
                Everything.load_one_yaml(path, fd, objs)

        # Configuration file directives reference each other via
        # the name attribute; however, when rendered the reference
        # is just an array index.
        #
        # At this point all objects have been created but references
        # have not been resolved to array indices.  Instruct objects
        # to do that now.
        for cls, items in objs.items():
            for obj in items:
                obj.setup(objs)

        return Everything(**objs)

    def __init__(self, *a, **kw):
        self.pathmeta = kw.pop('path', [])
        self.paths = kw.pop('pathname', [])
        self.meta = kw.pop('meta', [])
        self.pathgroups = kw.pop('pathgroup', [])
        self.interfaces = kw.pop('interface', [])
        self.properties = kw.pop('property', [])
        self.propertynames = kw.pop('propertyname', [])
        self.propertygroups = kw.pop('propertygroup', [])
        self.instances = kw.pop('instance', [])
        self.pathinstances = kw.pop('pathinstance', [])
        self.instancegroups = kw.pop('instancegroup', [])
        self.pathinstancegroups = kw.pop('pathinstancegroup', [])
        self.watches = kw.pop('watch', [])
        self.pathwatches = kw.pop('pathwatch', [])
        self.callbacks = kw.pop('callback', [])
        self.pathcallbacks = kw.pop('pathcallback', [])
        self.callbackgroups = kw.pop('callbackgroup', [])
        self.pathcallbackgroups = kw.pop('pathcallbackgroup', [])
        self.conditions = kw.pop('condition', [])

        super(Everything, self).__init__(**kw)

    def generate_cpp(self, loader):
        '''Render the template with the provided data.'''
        # errors.hpp is used by generated.hpp to included any error.hpp files
        open('errors.hpp', 'w+')

        with open(args.output, 'w') as fd:
            fd.write(
                self.render(
                    loader,
                    args.template,
                    meta=self.meta,
                    properties=self.properties,
                    propertynames=self.propertynames,
                    interfaces=self.interfaces,
                    paths=self.paths,
                    pathmeta=self.pathmeta,
                    pathgroups=self.pathgroups,
                    propertygroups=self.propertygroups,
                    instances=self.instances,
                    pathinstances=self.pathinstances,
                    watches=self.watches,
                    pathwatches=self.pathwatches,
                    instancegroups=self.instancegroups,
                    pathinstancegroups=self.pathinstancegroups,
                    callbacks=self.callbacks,
                    pathcallbacks=self.pathcallbacks,
                    callbackgroups=self.callbackgroups,
                    pathcallbackgroups=self.pathcallbackgroups,
                    conditions=self.conditions,
                    indent=Indent()))

if __name__ == '__main__':
    script_dir = os.path.dirname(os.path.realpath(__file__))
    valid_commands = {
        'generate-cpp': 'generate_cpp',
    }

    parser = ArgumentParser(
        description='Phosphor DBus Monitor (PDM) YAML '
        'scanner and code generator.')

    parser.add_argument(
        "-o", "--out", dest="output",
        default='generated.cpp',
        help="Generated output file name and path.")
    parser.add_argument(
        '-t', '--template', dest='template',
        default='generated.mako.hpp',
        help='The top level template to render.')
    parser.add_argument(
        '-p', '--template-path', dest='template_search',
        default=script_dir,
        help='The space delimited mako template search path.')
    parser.add_argument(
        '-d', '--dir', dest='inputdir',
        default=os.path.join(script_dir, 'example'),
        help='Location of files to process.')
    parser.add_argument(
        'command', metavar='COMMAND', type=str,
        choices=valid_commands.keys(),
        help='%s.' % " | ".join(valid_commands.keys()))

    args = parser.parse_args()

    if sys.version_info < (3, 0):
        lookup = mako.lookup.TemplateLookup(
            directories=args.template_search.split(),
            disable_unicode=True)
    else:
        lookup = mako.lookup.TemplateLookup(
            directories=args.template_search.split())
    try:
        function = getattr(
            Everything.load(args),
            valid_commands[args.command])
        function(lookup)
    except InvalidConfigError as e:
        sys.stdout.write('{0}: {1}\n\n'.format(e.config, e.msg))
        raise
