# Path: code/common/module/dependency_graph.py
# class DependencyGraph:
#     def __init__(self):
#         self.roots: list = []   # list of (1 or more) root DependencyNodes that form a dependency chain
#         self._node_map: dict = {}    # dictionary of class names to nodes that've been inserted into the graph
#         self._orphaned_node_map: dict = {}   # dictionary of class names (that haven't been inserted into the graph) to list of nodes that require that non-existant class
# 
#     ## Methods
# 
#     def insert(self, cls, dependencies = List[Module]) -> DependencyNode:
#         class_name = cls.__name__
# 
#         ## Don't insert duplicates
#         if (class_name in self._node_map):
#             logger.warn('Unable to insert {}, as it\'s already been added.'.format(class_name))
#             return
# 
#         ## Build initial node & update mappings
#         node = DependencyNode(cls)
#         self._node_map[class_name] = node
# 
#         ## Handle any orphaned children that depend on this class
#         if (class_name in self._orphaned_node_map):
#             orphaned_children = self._orphaned_node_map[class_name]
# 
#             for child in orphaned_children:
#                 node.children.append(child)
#                 child.parents.append(node)
# 
#             del self._orphaned_node_map[class_name]
# 
#         ## Process the dependencies by searching for existing nodes, otherwise populate the orphaned child map
#         dependency: Module
#         for dependency in dependencies:
#             dependency_name = dependency.__name__
# 
#             if (dependency_name in self._node_map):
#                 dependency_node = self._node_map[dependency_name]
# 
#                 node.parents.append(dependency_node)
#                 dependency_node.children.append(node)
#             else:
#                 if (dependency_name in self._orphaned_node_map):
#                     self._orphaned_node_map[dependency_name].append(node)
#                 else:
#                     self._orphaned_node_map[dependency_name] = [node]
# 
#         ## Add it to the list of root nodes
#         if (len(node.parents) == 0 and len(dependencies) == 0):
#             self.roots.append(node)
# 
#         return node
# 
# 
#     def set_graph_loaded_state(self, state: bool):
#         for node in self._node_map.values():
#             node.loaded = state
# 
# Path: code/common/module/module_initialization_container.py
# class ModuleInitializationContainer:
#     def __init__(self, cls, *init_args, **init_kwargs):
#         if(not isclass(cls)):
#             raise RuntimeError("Provided class parameter '{}' isn't actually a class.".format(cls))
# 
#         self.cls = cls
#         self.is_cog = issubclass(cls, commands.Cog)
#         self.init_args = init_args
#         self.init_kwargs = init_kwargs


import os
import sys
import logging
import importlib
from collections import OrderedDict
from pathlib import Path
from functools import reduce
from common import utilities
from common.exceptions import ModuleLoadException
from common.module.module import Module
from .dependency_graph import DependencyGraph
from .module_initialization_container import ModuleInitializationContainer
from discord.ext import commands
            logger.warn('Modules directory doesn\'t exist, so no modules will be loaded.')
            return

        ## Build a list of potential module paths and iterate through it...
        module_directories = os.listdir(self.modules_dir_path)
        for module_directory in module_directories:
            module_path = Path.joinpath(self.modules_dir_path, module_directory)

            ## Note that the entrypoint for the module should share the same name as it's parent folder. For example:
            ## phrases.py is the entrypoint for the phrases/ directory
            module_entrypoint = Path.joinpath(module_path, module_path.name + '.py')
            
            if (module_entrypoint.exists):
                ## Expose the module's root directory to the interpreter, so it can be imported
                sys.path.append(str(module_path))

                ## Attempt to import the module (akin to 'import [name]') and register it normally
                ## NOTE: Modules MUST have a 'main()' function that essentially returns a list containing all the args
                ##       needed by the 'register()' method of this ModuleManager class. At a minimum this list MUST
                ##       contain a reference to the class that serves as an entry point to the module. You should also
                ##       specify whether or not a given module is a cog (for discord.py) or not.
                try:
                    module = importlib.import_module(module_path.name)
                    module_init = module.main()
                except Exception as e:
                    logger.exception("Unable to import module {} on bot.".format(module_path.name), e)
                    del sys.path[-1]    ## Prune back the failed module from the path
                    continue

                ## Filter out any malformed modules

# Path: code/common/module/dependency_graph.py
# class DependencyGraph:
#     def __init__(self):
#         self.roots: list = []   # list of (1 or more) root DependencyNodes that form a dependency chain
#         self._node_map: dict = {}    # dictionary of class names to nodes that've been inserted into the graph
#         self._orphaned_node_map: dict = {}   # dictionary of class names (that haven't been inserted into the graph) to list of nodes that require that non-existant class
# 
#     ## Methods
# 
#     def insert(self, cls, dependencies = List[Module]) -> DependencyNode:
#         class_name = cls.__name__
# 
#         ## Don't insert duplicates
#         if (class_name in self._node_map):
#             logger.warn('Unable to insert {}, as it\'s already been added.'.format(class_name))
#             return
# 
#         ## Build initial node & update mappings
#         node = DependencyNode(cls)
#         self._node_map[class_name] = node
# 
#         ## Handle any orphaned children that depend on this class
#         if (class_name in self._orphaned_node_map):
#             orphaned_children = self._orphaned_node_map[class_name]
# 
#             for child in orphaned_children:
#                 node.children.append(child)
#                 child.parents.append(node)
# 
#             del self._orphaned_node_map[class_name]
# 
#         ## Process the dependencies by searching for existing nodes, otherwise populate the orphaned child map
#         dependency: Module
#         for dependency in dependencies:
#             dependency_name = dependency.__name__
# 
#             if (dependency_name in self._node_map):
#                 dependency_node = self._node_map[dependency_name]
# 
#                 node.parents.append(dependency_node)
#                 dependency_node.children.append(node)
#             else:
#                 if (dependency_name in self._orphaned_node_map):
#                     self._orphaned_node_map[dependency_name].append(node)
#                 else:
#                     self._orphaned_node_map[dependency_name] = [node]
# 
#         ## Add it to the list of root nodes
#         if (len(node.parents) == 0 and len(dependencies) == 0):
#             self.roots.append(node)
# 
#         return node
# 
# 
#     def set_graph_loaded_state(self, state: bool):
#         for node in self._node_map.values():
#             node.loaded = state
# 
# Path: code/common/module/module_initialization_container.py
# class ModuleInitializationContainer:
#     def __init__(self, cls, *init_args, **init_kwargs):
#         if(not isclass(cls)):
#             raise RuntimeError("Provided class parameter '{}' isn't actually a class.".format(cls))
# 
#         self.cls = cls
#         self.is_cog = issubclass(cls, commands.Cog)
#         self.init_args = init_args
#         self.init_kwargs = init_kwargs


# Path: code/common/module/module_manager.py
import os
import sys
import logging
import importlib
from collections import OrderedDict
from pathlib import Path
from functools import reduce
from common import utilities
from common.exceptions import ModuleLoadException
from common.module.module import Module
from .dependency_graph import DependencyGraph
from .module_initialization_container import ModuleInitializationContainer
from discord.ext import commands

            logger.warn('Modules directory doesn\'t exist, so no modules will be loaded.')
            return

        ## Build a list of potential module paths and iterate through it...
        module_directories = os.listdir(self.modules_dir_path)
        for module_directory in module_directories:
            module_path = Path.joinpath(self.modules_dir_path, module_directory)

            ## Note that the entrypoint for the module should share the same name as it's parent folder. For example:
            ## phrases.py is the entrypoint for the phrases/ directory
            module_entrypoint = Path.joinpath(module_path, module_path.name + '.py')
            
            if (module_entrypoint.exists):
                ## Expose the module's root directory to the interpreter, so it can be imported
                sys.path.append(str(module_path))

                ## Attempt to import the module (akin to 'import [name]') and register it normally
                ## NOTE: Modules MUST have a 'main()' function that essentially returns a list containing all the args
                ##       needed by the 'register()' method of this ModuleManager class. At a minimum this list MUST
                ##       contain a reference to the class that serves as an entry point to the module. You should also
                ##       specify whether or not a given module is a cog (for discord.py) or not.
                try:
                    module = importlib.import_module(module_path.name)
                    module_init = module.main()
                except Exception as e:
                    logger.exception("Unable to import module {} on bot.".format(module_path.name), e)
                    del sys.path[-1]    ## Prune back the failed module from the path
                    continue

                ## Filter out any malformed modules

                if (not isinstance(module_init, ModuleInitializationContainer) and type(module_init) != bool):