# %% Imports

import os
import re
import glob
import argparse
import CppHeaderParser
import jinja2

# %% Constants


# Association between member property and PlantUML symbol
MEMBER_PROP_MAP = {
    'private': '-',
    'public': '+',
    'protected': '#'
}

# Links
LINK_TYPE_MAP = {
    'inherit': '<|--',
    'aggregation': 'o--',
    'composition': '*--',
    'dependency': '<..'
}

# Association between object names and objects
# - The first element is the object type name in the CppHeader object
# - The second element is the iterator used to loop over objects
# - The third element is a function returning the corresponding internal object
CONTAINER_TYPE_MAP = [
    ['classes', lambda objs: objs.items(), lambda obj: Class(obj)],
    ['structs', lambda objs: objs.items(), lambda obj: Struct(obj)],
    ['enums', lambda objs: objs, lambda obj: Enum(obj)]
]

# %% Base classes


class Container(object):
    """Base class for C++ objects

    This class defines the basic interface for parsed objects (e.g. class).
    """
    def __init__(self, container_type, name):
        """Class constructor

        Parameters
        ----------
        container_type : str
            String representation of container type (``class``, ``struct`` or
            ``enum``)
        name : str
            Object name
        """
        self._container_type = container_type
        self._name = name
        self._member_list = []
        self._namespace = None

    def get_name(self):
        """Name property accessor

        Returns
        -------
        str
            Object name
        """
        return self._name

    def parse_members(self, header_container):
        """Initialize object from header

        Extract object from CppHeaderParser dictionary representing a class, a
        struct or an enum object.  This extracts the namespace.

        Parameters
        ----------
        header_container : CppClass, CppStruct or CppEnum
            Parsed header for container
        """
        namespace = header_container.get('namespace', None)
        if namespace:
            self._namespace = re.sub(':+$', '', namespace)
        self._do_parse_members(header_container)

    def _do_parse_members(self, header_container):
        """Initialize object from header (abstract method)

        Extract object from CppHeaderParser dictionary representing a class, a
        struct or an enum object.

        Parameters
        ----------
        header_container : CppClass, CppStruct or CppEnum
            Parsed header for container
        """
        raise NotImplementedError(
            'Derived class must implement :func:`_do_parse_members`.')

    def render(self):
        """Render object to string

        Returns
        -------
        str
            String representation of object following the PlantUML syntax
        """
        container_str = self._render_container_def() + ' {\n'
        for member in self._member_list:
            container_str += '\t' + member.render() + '\n'
        container_str += '}\n'
        if self._namespace is not None:
            return wrap_namespace(container_str, self._namespace)
        return container_str

    def comparison_keys(self):
        """Order comparison key between `ClassRelationship` objects

        Use the parent name, the child name then the link type as successive
        keys.

        Returns
        -------
        list
            `operator.attrgetter` objects for successive fields used as keys
        """
        return self._container_type, self._name

    def sort_members(self):
        """Sort container members

        sort the list of members by type and name
        """
        self._member_list.sort(key=lambda obj: obj.comparison_keys())

    def _render_container_def(self):
        """String representation of object definition

        Return the definition line of an object (e.g. "class MyClass").

        Returns
        -------
        str
            Container type and name as string
        """
        return self._container_type + ' ' + self._name

# %% Object member


class ContainerMember(object):
    """Base class for members of `Container` object

    This class defines the basic interface for object members (e.g. class
    variables, etc.)
    """
    def __init__(self, header_member, **kwargs):
        """Constructor

        Parameters
        ----------
        header_member : str
            Member name
        """
        self._name = header_member
        self._type = None

    def render(self):
        """Render object to string (abstract method)

        Returns
        -------
        str
            String representation of object member following the PlantUML
            syntax
        """
        raise NotImplementedError('Derived class must implement `render`.')

    def comparison_keys(self):
        """Order comparison key between `ClassRelationship` objects

        Use the parent name, the child name then the link type as successive
        keys.

        Returns
        -------
        list
            `operator.attrgetter` objects for successive fields used as keys
        """
        if self._type is not None:
            return self._type, self._name
        else:
            return self._name

# %% Class object


class Class(Container):
    """Representation of C++ class

    This class derived from `Container` specializes the base class to handle
    class definition in C++ headers.

    It supports:

    * abstract and template classes
    * member variables and methods (abstract and static)
    * public, private, protected members (static)
    """
    def __init__(self, header_class):
        """Constructor

        Extract the class name and properties (template, abstract) and
        inheritance.  Then, extract the class members from the header using the
        :func:`parse_members` method.

        Parameters
        ----------
        header_class : list (str, CppClass)
            Parsed header for class object (two-element list where the first
            element is the class name and the second element is a CppClass
            object)
        """
        super().__init__('class', header_class[0])
        self._abstract = header_class[1]['abstract']
        self._template_type = None
        if 'template' in header_class[1]:
            self._template_type = _cleanup_single_line(
                header_class[1]['template'])
        self._inheritance_list = [re.sub('<.*>', '', parent['class'])
                                  for parent in header_class[1]['inherits']]
        self.parse_members(header_class[1])

    def _do_parse_members(self, header_class):
        """Initialize class object from header

        This method extracts class member variables and methods from header.

        Parameters
        ----------
        header_class : CppClass
            Parsed header for class
        """
        member_type_map = [
            ['properties', ClassVariable],
            ['methods', ClassMethod]
        ]
        for member_type, member_type_handler in member_type_map:
            for member_prop in MEMBER_PROP_MAP.keys():
                member_list = header_class[member_type][member_prop]
                for header_member in member_list:
                    self._member_list.append(
                        member_type_handler(header_member, member_prop))

    def build_variable_type_list(self):
        """Get type of member variables

        This function extracts the type of each member variable.  This is used
        to list aggregation relationships between classes.

        Returns
        -------
        list(str)
            List of types (as string) for each member variable
        """
        variable_type_list = []
        for member in self._member_list:
            if isinstance(member, ClassVariable):
                variable_type_list.append(member.get_type())
        return variable_type_list

    def build_inheritance_list(self):
        """Get inheritance list

        Returns
        -------
        list(str)
            List of class names the current class inherits from
        """
        return self._inheritance_list

    def _render_container_def(self):
        """Create the string representation of the class

        Return the class name with template and abstract properties if
        present.  The output string follows the PlantUML syntax.

        Returns
        -------
        str
            String representation of class
        """
        class_str = self._container_type + ' ' + self._name
        if self._abstract:
            class_str = 'abstract ' + class_str
        if self._template_type is not None:
            class_str += ' <{0}>'.format(self._template_type)
        return class_str

# %% Class member


class ClassMember(ContainerMember):
    """Class member (variable and method) representation

    This class is the base class for class members.  The representation
    includes the member type (variable or method), name, scope (``public``,
    ``private`` or ``protected``) and a static flag.

    """
    def __init__(self, class_member, member_scope='private'):
        """Constructor

        Parameters
        ----------
        class_member : CppVariable or CppMethod
            Parsed member object (variable or method)
        member_scope : str
            Member scope property: ``public``, ``private`` or ``protected``
        """
        super().__init__(class_member['name'])
        self._type = None
        self._static = class_member['static']
        self._scope = member_scope
        self._properties = []

    def render(self):
        """Get string representation of member

        The string representation is with the scope indicator and a static
        keyword when the member is static.  It is postfixed by the type (return
        type for class methods) and additional properties (e.g. ``const``
        methods are flagged with the ``query`` property).  The inner part of
        the returned string contains the variable name and signature for
        methods.  This is obtained using the :func:`_render_name` method.

        Returns
        -------
        str
            String representation of member

        """
        if len(self._properties) > 0:
            props = ' {' + ', '.join(self._properties) + '}'
        else:
            props = ''
        vis = MEMBER_PROP_MAP[self._scope] + \
              ('{static} ' if self._static else '')
        member_str = vis + self._render_name() + \
                     (' : ' + self._type if self._type else '') + \
                     props
        return member_str

    def _render_name(self):
        """Get member name

        By default (for member variables), this returns the member name.
        Derived classes can override this to control the name rendering
        (e.g. add the function prototype for member functions)
        """
        return self._name

# %% Class variable


class ClassVariable(ClassMember):
    """Object representation of class member variables

    This class specializes the `ClassMember` object for member variables.
    Additionally to the base class, it stores variable types as strings.  This
    is used to establish aggregation relationships between objects.
    """
    def __init__(self, class_variable, member_scope='private'):
        """Constructor

        Parameters
        ----------
        class_variable : CppVariable
            Parsed class variable object
        member_scope : str
            Scope property to member variable
        """
        assert(isinstance(class_variable,
                          CppHeaderParser.CppHeaderParser.CppVariable))

        super().__init__(class_variable, member_scope)

        self._type = _cleanup_type(class_variable['type'])

    def get_type(self):
        """Variable type accessor

        Returns
        -------
        str
            Variable type as string
        """
        return self._type

# %% Class method


class ClassMethod(ClassMember):
    """Class member method representation

    This class extends `ClassMember` for member methods.  It stores additional
    method properties (abstract, destructor flag, input parameter types).
    """
    def __init__(self, class_method, member_scope):
        """Constructor

        The method name and additional properties are extracted from the parsed
        header.

        * A list of parameter types is stored to retain the function signature.
        * The ``~`` character is appended to destructor methods.
        * ``const`` methods are flagged with the ``query`` property.

        Parameters
        ----------
        class_method : CppMethod
            Parsed class member method
        member_scope : str
            Scope of the member method

        """
        assert(isinstance(class_method,
                          CppHeaderParser.CppHeaderParser.CppMethod))

        super().__init__(class_method, member_scope)

        self._type = _cleanup_type(class_method['returns'])
        if class_method['returns_pointer']:
            self._type += '*'
        elif class_method['returns_reference']:
            self._type += '&'
        self._abstract = class_method['pure_virtual']
        if class_method['destructor']:
            self._name = '~' + self._name
        if class_method['const']:
            self._properties.append('query')
        self._param_list = []
        for param in class_method['parameters']:
            self._param_list.append([_cleanup_type(param['type']),
                                     param['name']])

    def _render_name(self):
        """Internal rendering of method name

        This method extends the base :func:`ClassMember._render_name` method by
        adding the method signature to the returned string.

        Returns
        -------
        str
            The method name (prefixed with the ``abstract`` keyword when
            appropriate) and signature
        """
        assert(not self._static or not self._abstract)

        method_str = ('{abstract} ' if self._abstract else '') + \
                     self._name + '(' + \
                     ', '.join(' '.join(it).strip()
                               for it in self._param_list) + ')'

        return method_str

# %% Struct object


class Struct(Class):
    """Representation of C++ struct objects

    This class derived is almost identical to `Class`, the only difference
    being the container type name ("struct" instead of "class").
    """
    def __init__(self, header_struct):
        """Class constructor

        Parameters
        ----------
        header_struct : list (str, CppStruct)
            Parsed header for struct object (two-element list where the first
            element is the structure name and the second element is a CppStruct
            object)
        """
        super().__init__(header_struct[0])
        super(Class).__init__('struct')

# %% Enum object


class Enum(Container):
    """Class representing enum objects

    This class defines a simple object inherited from the base `Container`
    class.  It simply lists enumerated values.
    """
    def __init__(self, header_enum):
        """Constructor

        Parameters
        ----------
        header_enum : CppEnum
            Parsed CppEnum object
        """
        super().__init__('enum', header_enum.get('name', 'empty'))
        self.parse_members(header_enum)

    def _do_parse_members(self, header_enum):
        """Extract enum values from header

        Parameters
        ----------
        header_enum : CppEnum
            Parsed `CppEnum` object
        """
        for value in header_enum.get('values', []):
            self._member_list.append(EnumValue(value['name']))


class EnumValue(ContainerMember):
    """Class representing values in enum object

    This class only contains the name of the enum value (the actual integer
    value is ignored).
    """
    def __init__(self, header_value, **kwargs):
        """Constructor

        Parameters
        ----------
        header_value : str
            Name of enum member
        """
        super().__init__(header_value)

    def render(self):
        """Rendering to string

        This method simply returns the variable name

        Returns
        -------
        str
            The enumeration element name
        """
        return self._name

# %% Class connections


class ClassRelationship(object):
    """Base object for class relationships

    This class defines the common structure of class relationship objects.
    This includes a parent/child pair and a relationship type (e.g. inheritance
    or aggregation).
    """
    def __init__(self, link_type, c_parent, c_child, flag_use_namespace=False):
        """Constructor

        Parameters
        ----------
        link_type : str
            Relationship type: ``inherit`` or ``aggregation``
        c_parent : str
            Name of parent class
        c_child : str
            Name of child class
        """
        self._parent = c_parent.get_name()
        self._child = c_child.get_name()
        self._link_type = link_type
        self._parent_namespace = c_parent._namespace or None
        self._child_namespace = c_child._namespace or None
        self._flag_use_namespace = flag_use_namespace

    def comparison_keys(self):
        """Order comparison key between `ClassRelationship` objects

        Compare alphabetically based on the parent name, the child name then
        the link type.

        Returns
        -------
        list
            `operator.attrgetter` objects for successive fields used as keys
        """
        return self._parent, self._child, self._link_type

    def _render_name(self, class_name, class_namespace, flag_use_namespace):
        """Render class name with namespace prefix if necessary

        Parameters
        ----------
        class_name : str
           Name of the class
        class_namespace : str
            Namespace or None if the class is defined in the default namespace
        flag_use_namespace : bool
            When False, do not use the namespace

        Returns
        -------
        str
            Class name with appropriate prefix for use with link rendering
        """
        if not flag_use_namespace:
            return class_name

        if class_namespace is None:
            prefix = '.'
        else:
            prefix = class_namespace + '.'
        return prefix + class_name

    def render(self):
        """Render class relationship to string

        This method generically appends the parent name, a rendering of the
        link type (obtained from the :func:`_render_link_type` method) and the
        child object name.

        Returns
        -------
        str
            The string representation of the class relationship following the
            PlantUML syntax
        """
        link_str = ''

        # Wrap the link in namespace block (if both parent and child are in the
        # same namespace)
        namespace_wrap = None
        if self._parent_namespace == self._child_namespace and \
           self._parent_namespace is not None:
            namespace_wrap = self._parent_namespace

        # Prepend the namespace to the class name
        flag_render_namespace = self._flag_use_namespace and not namespace_wrap
        parent_str = self._render_name(self._parent, self._parent_namespace,
                                       flag_render_namespace)
        child_str = self._render_name(self._child, self._child_namespace,
                                      flag_render_namespace)

        # Link string
        link_str += parent_str + ' ' + self._render_link_type() + \
                    ' ' + child_str + '\n'

        if namespace_wrap is not None:
            return wrap_namespace(link_str, namespace_wrap)
        return link_str

    def _render_link_type(self):
        """Internal representation of link

        The string representation is obtained from the `LINK_TYPE_MAP`
        constant.

        Returns
        -------
        str
            The link between parent and child following the PlantUML syntax
        """
        return LINK_TYPE_MAP[self._link_type]

# %% Class inheritance


class ClassInheritanceRelationship(ClassRelationship):
    """Representation of inheritance relationships

    This module extends the base `ClassRelationship` class by setting the link
    type to ``inherit``.
    """
    def __init__(self, c_parent, c_child, **kwargs):
        """Constructor

        Parameters
        ----------
        c_parent : str
            Parent class
        c_child : str
            Derived class
        kwargs : dict
            Additional parameters passed to parent class
        """
        super().__init__('inherit', c_parent, c_child, **kwargs)

# %% Class aggregation


class ClassAggregationRelationship(ClassRelationship):
    """Representation of aggregation relationships

    This module extends the base `ClassRelationship` class by setting the link
    type to ``aggregation``.  It also keeps a count of aggregation, which is
    displayed near the arrow when using PlantUML.

    Aggregation relationships are simplified to represent the presence of a
    variable type (possibly within a container such as a list) in a class
    definition.
    """
    def __init__(self, c_object, c_container, c_count=1,
                 rel_type='aggregation', **kwargs):
        """Constructor

        Parameters
        ----------
        c_object : str
            Class corresponding to the type of the member variable in the
            aggregation relationship
        c_container : str
            Child (or client) class of the aggregation relationship
        c_count : int
            The number of members of ``c_container`` that are of type (possibly
            through containers) ``c_object``
        rel_type : str
            Relationship type: ``aggregation`` or ``composition``
        kwargs : dict
            Additional parameters passed to parent class
        """
        super().__init__(rel_type, c_object, c_container, **kwargs)
        self._count = c_count

    def _render_link_type(self):
        """Internal link rendering

        This method overrides the default link rendering defined in
        :func:`ClassRelationship._render_link_type` to include a count near the
        end of the arrow.
        """
        count_str = '' if self._count == 1 else '"%d" ' % self._count
        return count_str + LINK_TYPE_MAP[self._link_type]

# %% Class dependency


class ClassDependencyRelationship(ClassRelationship):
    """Dependency relationship

    Dependencies occur when member methods depend on an object of another class
    in the diagram.
    """
    def __init__(self, c_parent, c_child, **kwargs):
        """Constructor

        Parameters
        ----------
        c_parent : str
            Class corresponding to the type of the member variable in the
            dependency relationship
        c_child : str
            Child (or client) class of the dependency relationship
        kwargs : dict
            Additional parameters passed to parent class
        """
        super().__init__('dependency', c_parent, c_child, **kwargs)

# %% Diagram class


class Diagram(object):
    """UML diagram object

    This class lists the objects in the set of files considered, and the
    relationships between object.

    The main interface to the `Diagram` object is via the ``create_*`` and
    ``add_*`` methods.  The former parses objects and builds relationship lists
    between the different parsed objects.  The latter only parses objects and
    does not builds relationship lists.

    Each method has versions for file and string inputs and folder string lists
    and file lists inputs.
    """
    def __init__(self, template_file=None, flag_dep=False):
        """Constructor

        The `Diagram` class constructor simply initializes object lists.  It
        does not create objects or relationships.
        """
        self._flag_dep = flag_dep
        self.clear()
        loader_list = []
        if template_file is not None:
            loader_list.append(jinja2.FileSystemLoader(
                os.path.abspath(os.path.dirname(template_file))))
            self._template_file = os.path.basename(template_file)
        else:
            self._template_file = 'default.puml'
        loader_list.append(jinja2.PackageLoader('hpp2plantuml', 'templates'))
        self._env = jinja2.Environment(loader=jinja2.ChoiceLoader(
            loader_list), keep_trailing_newline=True)

    def clear(self):
        """Reinitialize object"""
        self._objects = []
        self._inheritance_list = []
        self._aggregation_list = []
        self._dependency_list = []

    def _sort_list(input_list):
        """Sort list using `ClassRelationship` comparison

        Parameters
        ----------
        input_list : list(ClassRelationship)
            Sort list using the :func:`ClassRelationship.comparison_keys`
            comparison function
        """
        input_list.sort(key=lambda obj: obj.comparison_keys())

    def sort_elements(self):
        """Sort elements in diagram

        Sort the objects and relationship links.  Objects are sorted using the
        :func:`Container.comparison_keys` comparison function and list are
        sorted using the `_sort_list` helper function.
        """
        self._objects.sort(key=lambda obj: obj.comparison_keys())
        for obj in self._objects:
            obj.sort_members()
        Diagram._sort_list(self._inheritance_list)
        Diagram._sort_list(self._aggregation_list)
        Diagram._sort_list(self._dependency_list)

    def _build_helper(self, input, build_from='string', flag_build_lists=True,
                      flag_reset=False):
        """Helper function to initialize a `Diagram` object from parsed headers

        Parameters
        ----------
        input : CppHeader or str or list(CppHeader) or list(str)
            Input of arbitrary type.  The processing depends on the
            ``build_from`` parameter
        build_from : str
            Determines the type of the ``input`` variable:

            * ``string``: ``input`` is a string containing C++ header code
            * ``file``: ``input`` is a filename to parse
            * ``string_list``: ``input`` is a list of strings containing C++
              header code
            * ``file_list``: ``input`` is a list of filenames to parse

        flag_build_lists : bool
            When True, relationships lists are built and the objects in the
            diagram are sorted, otherwise, only object parsing is performed
        flag_reset : bool
            If True, the object is initialized (objects and relationship lists
            are cleared) prior to parsing objects, otherwise, new objects are
            appended to the list of existing ones
        """
        if flag_reset:
            self.clear()
        if build_from in ('string', 'file'):
            self.parse_objects(input, build_from)
        elif build_from in ('string_list', 'file_list'):
            build_from_single = re.sub('_list$', '', build_from)
            for single_input in input:
                self.parse_objects(single_input, build_from_single)
        if flag_build_lists:
            self.build_relationship_lists()
            self.sort_elements()

    def create_from_file(self, header_file):
        """Initialize `Diagram` object from header file

        Wrapper around the :func:`_build_helper` function, with ``file`` input,
        building the relationship lists and with object reset.
        """
        self._build_helper(header_file, build_from='file',
                           flag_build_lists=True, flag_reset=True)

    def create_from_file_list(self, file_list):
        """Initialize `Diagram` object from list of header files

        Wrapper around the :func:`_build_helper` function, with ``file_list``
        input, building the relationship lists and with object reset.
        """
        self._build_helper(file_list, build_from='file_list',
                           flag_build_lists=True, flag_reset=True)

    def add_from_file(self, header_file):
        """Augment `Diagram` object from header file

        Wrapper around the :func:`_build_helper` function, with ``file`` input,
        skipping building of the relationship lists and without object reset
        (new objects are added to the object).
        """
        self._build_helper(header_file, build_from='file',
                           flag_build_lists=False, flag_reset=False)

    def add_from_file_list(self, file_list):
        """Augment `Diagram` object from list of header files

        Wrapper around the :func:`_build_helper` function, with ``file_list``
        input, skipping building of the relationship lists and without object
        reset (new objects are added to the object).
        """
        self._build_helper(file_list, build_from='file_list',
                           flag_build_lists=False, flag_reset=False)

    def create_from_string(self, header_string):
        """Initialize `Diagram` object from header string

        Wrapper around the :func:`_build_helper` function, with ``string``
        input, building the relationship lists and with object reset.
        """
        self._build_helper(header_string, build_from='string',
                           flag_build_lists=True, flag_reset=True)

    def create_from_string_list(self, string_list):
        """Initialize `Diagram` object from list of header strings

        Wrapper around the :func:`_build_helper` function, with ``string_list``
        input, skipping building of the relationship lists and with object
        reset.
        """
        self._build_helper(string_list, build_from='string_list',
                           flag_build_lists=True, flag_reset=True)

    def add_from_string(self, header_string):
        """Augment `Diagram` object from header string

        Wrapper around the :func:`_build_helper` function, with ``string``
        input, skipping building of the relationship lists and without object
        reset (new objects are added to the object).
        """
        self._build_helper(header_string, build_from='string',
                           flag_build_lists=False, flag_reset=False)

    def add_from_string_list(self, string_list):
        """Augment `Diagram` object from list of header strings

        Wrapper around the :func:`_build_helper` function, with ``string_list``
        input, building the relationship lists and without object reset (new
        objects are added to the object).
        """
        self._build_helper(string_list, build_from='string_list',
                           flag_build_lists=False, flag_reset=False)

    def build_relationship_lists(self):
        """Build inheritance and aggregation lists from parsed objects

        This method successively calls the :func:`build_inheritance_list` and
        :func:`build_aggregation_list` methods.
        """
        self.build_inheritance_list()
        self.build_aggregation_list()
        if self._flag_dep:
            self.build_dependency_list()

    def parse_objects(self, header_file, arg_type='string'):
        """Parse objects

        This method parses file of string inputs using the CppHeaderParser
        module and extracts internal objects for rendering.

        Parameters
        ----------
        header_file : str
            A string containing C++ header code or a filename with C++ header
            code
        arg_type : str
            It set to ``string``, ``header_file`` is considered to be a string,
            otherwise, it is assumed to be a filename
        """
        # Parse header file
        parsed_header = CppHeaderParser.CppHeader(header_file,
                                                  argType=arg_type)
        for container_type, container_iterator, \
            container_handler in CONTAINER_TYPE_MAP:
            objects = parsed_header.__getattribute__(container_type)
            for obj in container_iterator(objects):
                self._objects.append(container_handler(obj))

    def _make_class_list(self):
        """Build list of classes

        Returns
        -------
        list(dict)
            Each entry is a dictionary with keys ``name`` (class name) and
            ``obj`` the instance of the `Class` class
        """
        return [{'name': obj.get_name(), 'obj': obj}
                for obj in self._objects if isinstance(obj, Class)]

    def build_inheritance_list(self):
        """Build list of inheritance between objects

        This method lists all the inheritance relationships between objects
        contained in the `Diagram` object (external relationships are ignored).

        The implementation establishes a list of available classes and loops
        over objects to obtain their inheritance.  When parent classes are in
        the list of available classes, a `ClassInheritanceRelationship` object
        is added to the list.
        """
        self._inheritance_list = []
        # Build list of classes in diagram
        class_list_obj = self._make_class_list()
        class_list = [c['name'] for c in class_list_obj]
        flag_use_namespace = any([c['obj']._namespace for c in class_list_obj])

        # Create relationships

        # Inheritance
        for obj in self._objects:
            obj_name = obj.get_name()
            if isinstance(obj, Class):
                for parent in obj.build_inheritance_list():
                    if parent in class_list:
                        parent_obj = class_list_obj[
                            class_list.index(parent)]['obj']
                        self._inheritance_list.append(
                            ClassInheritanceRelationship(
                                parent_obj, obj,
                                flag_use_namespace=flag_use_namespace))

    def build_aggregation_list(self):
        """Build list of aggregation relationships

        This method loops over objects and finds members with type
        corresponding to other classes defined in the `Diagram` object (keeping
        a count of occurrences).

        The procedure first builds an internal dictionary of relationships
        found, augmenting the count using the :func:`_augment_comp` function.
        In a second phase, `ClassAggregationRelationship` objects are created
        for each relationships, using the calculated count.
        """
        self._aggregation_list = []
        # Build list of classes in diagram
        # Build list of classes in diagram
        class_list_obj = self._make_class_list()
        class_list = [c['name'] for c in class_list_obj]
        flag_use_namespace = any([c['obj']._namespace for c in class_list_obj])

        # Build member type list
        variable_type_list = {}
        for obj in self._objects:
            obj_name = obj.get_name()
            if isinstance(obj, Class):
                variable_type_list[obj_name] = obj.build_variable_type_list()
        # Create aggregation links
        aggregation_counts = {}

        for child_class in class_list:
            if child_class in variable_type_list.keys():
                var_types = variable_type_list[child_class]
                for var_type in var_types:
                    for parent in class_list:
                        if re.search(r'\b' + parent + r'\b', var_type):
                            rel_type = 'composition'
                            if '{}*'.format(parent) in var_type:
                                rel_type = 'aggregation'
                            self._augment_comp(aggregation_counts, parent,
                                               child_class, rel_type=rel_type)
        for obj_class, obj_comp_list in aggregation_counts.items():
            for comp_parent, rel_type, comp_count in obj_comp_list:
                obj_class_idx = class_list.index(obj_class)
                obj_class_obj = class_list_obj[obj_class_idx]['obj']
                comp_parent_idx = class_list.index(comp_parent)
                comp_parent_obj = class_list_obj[comp_parent_idx]['obj']
                self._aggregation_list.append(
                    ClassAggregationRelationship(
                        obj_class_obj, comp_parent_obj, comp_count,
                        rel_type=rel_type,
                        flag_use_namespace=flag_use_namespace))

    def build_dependency_list(self):
        """Build list of dependency between objects

        This method lists all the dependency relationships between objects
        contained in the `Diagram` object (external relationships are ignored).

        The implementation establishes a list of available classes and loops
        over objects, list their methods adds a dependency relationship when a
        method takes an object as input.
        """

        self._dependency_list = []
        # Build list of classes in diagram
        class_list_obj = self._make_class_list()
        class_list = [c['name'] for c in class_list_obj]
        flag_use_namespace = any([c['obj']._namespace for c in class_list_obj])

        # Create relationships

        # Add all objects name to list
        objects_name = []
        for obj in self._objects:
            objects_name.append(obj.get_name())

        # Dependency
        for obj in self._objects:
            if isinstance(obj, Class):
                for member in obj._member_list:
                    # Check if the member is a method
                    if isinstance(member, ClassMethod):
                        for method in member._param_list:
                            index = ValueError
                            try:
                                # Check if the method param type is a Class
                                # type
                                index = [re.search(o, method[0]) is not None
                                         for o in objects_name].index(True)
                            except ValueError:
                                pass
                            if index != ValueError and \
                               method[0] != obj.get_name():
                                depend_obj = self._objects[index]

                                self._dependency_list.append(
                                    ClassDependencyRelationship(
                                        depend_obj, obj,
                                        flag_use_namespace=flag_use_namespace))

    def _augment_comp(self, c_dict, c_parent, c_child, rel_type='aggregation'):
        """Increment the aggregation reference count

        If the aggregation relationship is not in the list (``c_dict``), then
        add a new entry with count 1.  If the relationship is already in the
        list, then increment the count.

        Parameters
        ----------
        c_dict : dict
            List of aggregation relationships.  For each dictionary key, a pair
            of (str, int) elements: string and number of occurrences
        c_parent : str
            Parent class name
        c_child : str
            Child class name
        rel_type : str
            Relationship type: ``aggregation`` or ``composition``
        """
        if c_child not in c_dict:
            c_dict[c_child] = [[c_parent, rel_type, 1], ]
        else:
            parent_list = [c[:2] for c in c_dict[c_child]]
            if [c_parent, rel_type] not in parent_list:
                c_dict[c_child].append([c_parent, rel_type, 1])
            else:
                c_idx = parent_list.index([c_parent, rel_type])
                c_dict[c_child][c_idx][2] += 1

    def render(self):
        """Render full UML diagram

        The string returned by this function should be ready to use with the
        PlantUML program.  It includes all the parsed objects with their
        members, and the inheritance and aggregation relationships extracted
        from the list of objects.

        Returns
        -------
        str
            String containing the full string representation of the `Diagram`
            object, including objects and object relationships
        """
        template = self._env.get_template(self._template_file)
        return template.render(objects=self._objects,
                               inheritance_list=self._inheritance_list,
                               aggregation_list=self._aggregation_list,
                               dependency_list=self._dependency_list,
                               flag_dep=self._flag_dep)

# %% Cleanup object type string


def _cleanup_type(type_str):
    """Cleanup string representing a C++ type

    Cleanup simply consists in removing spaces before a ``*`` character and
    preventing multiple successive spaces in the string.

    Parameters
    ----------
    type_str : str
        A string representing a C++ type definition

    Returns
    -------
    str
        The type string after cleanup
    """
    return re.sub(r'[ ]+([*&])', r'\1',
                  re.sub(r'(\s)+', r'\1', type_str))

# %% Single line version of string


def _cleanup_single_line(input_str):
    """Cleanup string representing a C++ type

    Remove line returns

    Parameters
    ----------
    input_str : str
        A string possibly spreading multiple lines

    Returns
    -------
    str
        The type string in a single line
    """
    return re.sub(r'\s+', ' ', re.sub(r'(\r)?\n', ' ', input_str))

# %% Expand wildcards in file list


def expand_file_list(input_files):
    """Find all files in list (expanding wildcards)

    This function uses `glob` to find files matching each string in the input
    list.

    Parameters
    ----------
    input_files : list(str)
        List of strings representing file names and possibly including
        wildcards

    Returns
    -------
    list(str)
        List of filenames (with wildcards expanded).  Each element contains the
        name of an existing file
    """
    file_list = []
    for input_file in input_files:
        file_list += glob.glob(input_file)
    return file_list

def wrap_namespace(input_str, namespace):
    """Wrap string in namespace

    Parameters
    ----------
    input_str : str
        String containing PlantUML code
    namespace : str
       Namespace name

    Returns
    -------
    str
        ``input_str`` wrapped in ``namespace`` block
    """
    return 'namespace {} {{\n'.format(namespace) + \
        '\n'.join([re.sub('^', '\t', line)
                   for line in input_str.splitlines()]) + \
        '\n}\n'

# %% Main function


def CreatePlantUMLFile(file_list, output_file=None, **diagram_kwargs):
    """Create PlantUML file from list of header files

    This function parses a list of C++ header files and generates a file for
    use with PlantUML.

    Parameters
    ----------
    file_list : list(str)
        List of filenames (possibly, with wildcards resolved with the
        :func:`expand_file_list` function)
    output_file : str
        Name of the output file
    diagram_kwargs : dict
        Additional parameters passed to :class:`Diagram` constructor
    """
    if isinstance(file_list, str):
        file_list_c = [file_list, ]
    else:
        file_list_c = file_list
    diag = Diagram(**diagram_kwargs)
    diag.create_from_file_list(list(set(expand_file_list(file_list_c))))
    diag_render = diag.render()

    if output_file is None:
        print(diag_render)
    else:
        with open(output_file, 'wt') as fid:
            fid.write(diag_render)

# %% Command line interface


def main():
    """Command line interface

    This function is a command-line interface to the
    :func:`hpp2plantuml.CreatePlantUMLFile` function.

    Arguments are read from the command-line, run with ``--help`` for help.
    """
    parser = argparse.ArgumentParser(description='hpp2plantuml tool.')
    parser.add_argument('-i', '--input-file', dest='input_files',
                        action='append', metavar='HEADER-FILE', required=True,
                        help='input file (must be quoted' +
                        ' when using wildcards)')
    parser.add_argument('-o', '--output-file', dest='output_file',
                        required=False, default=None, metavar='FILE',
                        help='output file')
    parser.add_argument('-d', '--enable-dependency', dest='flag_dep',
                        required=False, default=False, action='store_true',
                        help='Extract dependency relationships from method ' +
                        'arguments')
    parser.add_argument('-t', '--template-file', dest='template_file',
                        required=False, default=None, metavar='JINJA-FILE',
                        help='path to jinja2 template file')
    parser.add_argument('--version', action='version',
                        version='%(prog)s ' + '0.6')
    args = parser.parse_args()
    if len(args.input_files) > 0:
        CreatePlantUMLFile(args.input_files, args.output_file,
                           template_file=args.template_file,
                           flag_dep=args.flag_dep)

# %% Standalone mode


if __name__ == '__main__':
    main()