# 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
            del init_kwargs['dependencies']

    ## Methods

    def get_class_callable(self) -> Module:
        '''Returns an invokable object to instantiate the class defined in self.cls'''
        return getattr(self.module, self.name)


class ModuleManager:
    '''
    Manages the modules' lifecycle. Chiefly, the discovery, registration, and installation of modules. It'll also
    support reloading existing modules/cogs too.
    '''

    def __init__(self, bot_controller, bot: commands.Bot):
        self.bot_controller = bot_controller
        self.bot = bot

        modules_dir_path = CONFIG_OPTIONS.get('modules_dir_path')
        if (modules_dir_path):
            self.modules_dir_path = Path(modules_dir_path)
        else:
            self.modules_dir_path = Path.joinpath(
                utilities.get_root_path(),
                CONFIG_OPTIONS.get('modules_dir', 'modules')
            )

        self.modules = OrderedDict()
        self.loaded_modules = {}    # Keep non-cog modules loaded in memory

# 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

            del init_kwargs['dependencies']

    ## Methods

    def get_class_callable(self) -> Module:
        '''Returns an invokable object to instantiate the class defined in self.cls'''
        return getattr(self.module, self.name)


class ModuleManager:
    '''
    Manages the modules' lifecycle. Chiefly, the discovery, registration, and installation of modules. It'll also
    support reloading existing modules/cogs too.
    '''

    def __init__(self, bot_controller, bot: commands.Bot):
        self.bot_controller = bot_controller
        self.bot = bot

        modules_dir_path = CONFIG_OPTIONS.get('modules_dir_path')
        if (modules_dir_path):
            self.modules_dir_path = Path(modules_dir_path)
        else:
            self.modules_dir_path = Path.joinpath(
                utilities.get_root_path(),
                CONFIG_OPTIONS.get('modules_dir', 'modules')
            )

        self.modules = OrderedDict()
        self.loaded_modules = {}    # Keep non-cog modules loaded in memory

        self._dependency_graph = DependencyGraph()