import dataclasses, datetime, enum, gc, glob, importlib, importlib.util, io
import inspect, os, sys, typing, uuid
from src import Config, EventManager, Exports, IRCBot, Logging, Timers, utils

class ModuleException(Exception):
    pass
class ModuleWarning(Exception):
    pass

class ModuleNotLoadedException(ModuleException):
    pass
class ModuleNotFoundException(ModuleException):
    pass

class ModuleNameCollisionException(ModuleException):
    pass
class ModuleLoadException(ModuleException):
    pass
class ModuleUnloadException(ModuleException):
    pass
class ModuleCannotUnloadException(ModuleException):
    pass

class ModuleNotLoadedWarning(ModuleWarning):
    pass
class ModuleNotLoadableWarning(ModuleWarning):
    pass

class ModuleDependencyNotFulfilled(ModuleException):
    def __init__(self, module, dependency):
        ModuleException.__init__(self, "Dependency for %s not fulfilled: %s"
            % (module, dependency))
        self.module = module
        self.dependency = dependency
class ModuleCircularDependency(ModuleException):
    pass

class ModuleType(enum.Enum):
    FILE = 0
    DIRECTORY = 1

class TryReloadResult(object):
    def __init__(self, success: bool, message: str):
        self.success = success
        self.message = message

@dataclasses.dataclass
class BaseModule(object):
    definition: "ModuleDefinition"
    bot: "IRCBot.Bot"
    events: EventManager.Events
    exports: Exports.Exports
    timers: Timers.Timers
    log: Logging.Log

    def on_load(self):
        pass
    def unload(self):
        pass

    def on_pause(self):
        pass
    def on_resume(self):
        pass

    def data_directory(self, filename: str):
        path, filename = os.path.split(filename)
        path = os.path.join(self.bot.data_directory, "mod-data",
            self.definition.name, path)

        if not os.path.isdir(path):
            os.makedirs(path)
        return os.path.join(path, filename)

@dataclasses.dataclass
class ModuleDefinition(object):
    name: str
    filename: str
    type: ModuleType
    hashflags: typing.List[typing.Tuple[str, typing.Optional[str]]]
    is_core: bool

    def get_dependencies(self):
        dependencies = []
        for key, value in self.hashflags:
            if key == "depends-on":
                dependencies.append(value)
        return sorted(dependencies)

@dataclasses.dataclass
class LoadedModule(object):
    name: str
    title: str
    module: BaseModule
    context: str
    import_name: str
    is_core: bool
    commit: typing.Optional[str] = None

    loaded_at: datetime.datetime = dataclasses.field(
        default_factory=lambda: utils.datetime.utcnow())

class ModuleManager(object):
    def __init__(self,
            events: EventManager.Events,
            exports: Exports.Exports,
            timers: Timers.Timers,
            config: Config.Config,
            log: Logging.Log,
            core_modules: str,
            extra_modules: typing.List[str]):
        self.events = events
        self.exports = exports
        self.config = config
        self.timers = timers
        self.log = log
        self._core_modules = core_modules
        self._extra_modules = extra_modules

        self.modules = {} # type: typing.Dict[str, LoadedModule]

    def _list_modules(self, directory: str, is_core: bool
            ) -> typing.Dict[str, ModuleDefinition]:
        modules = []
        for file_module in glob.glob(os.path.join(directory, "*.py")):
            modules.append(
                self.define_module(ModuleType.FILE, file_module, is_core))

        for directory_module in glob.glob(os.path.join(
                directory, "*", "__init__.py")):
            modules.append(self.define_module(ModuleType.DIRECTORY,
                directory_module, is_core))

        return {definition.name: definition for definition in modules}

    def list_modules(self, whitelist: typing.List[str],
            blacklist: typing.List[str]) -> typing.Dict[str, ModuleDefinition]:
        core_modules = self._list_modules(self._core_modules, True)
        extra_modules: typing.Dict[str, ModuleDefinition] = {}

        for directory in self._extra_modules:
            for name, module in self._list_modules(directory, False).items():
                if (not name in extra_modules and
                        (name in whitelist or
                        (not whitelist and not name in blacklist))):
                    extra_modules[name] = module

        modules = {}
        modules.update(extra_modules)
        modules.update(core_modules)
        return modules

    def define_module(self, type: ModuleType, filename: str, is_core: bool,
            ) -> ModuleDefinition:
        if type == ModuleType.DIRECTORY:
            name = os.path.dirname(filename)
        else:
            name = filename
        name = self._module_name(name)

        return ModuleDefinition(name, filename, type,
            utils.parse.hashflags(filename), is_core)

    def find_module(self, name: str) -> ModuleDefinition:
        type = ModuleType.FILE
        paths = self._module_paths(name)

        for is_core, possible_path in paths:
            if os.path.isdir(possible_path):
                type = ModuleType.DIRECTORY
                possible_path = os.path.join(possible_path, "__init__.py")
            else:
                possible_path = "%s.py" % possible_path

            if os.path.isfile(possible_path):
                return self.define_module(type, possible_path, is_core)

        raise ModuleNotFoundException(name)

    def _module_name(self, path: str) -> str:
        return os.path.ba
sename(path).rsplit(".py", 1)[0].lower()
    def _module_paths(self, name: str) -> typing.List[typing.Tuple[bool, str]]:
        paths = []

        for i, directory in enumerate([self._core_modules]+self._extra_modules):
            paths.append((i==0, os.path.join(directory, name)))
        return paths
    def _import_name(self, name: str, co
ntext: str) -> str:
        return "%s_%s" % (name, context)

    def from_context(self, context: str) -> typing.Optional[LoadedModule]:
        for module in self.modules.values():
            if module.context == context:
                return module
        return None
    def from_name(self, name: str) -> typing.Optional[LoadedModule]:
        name_lower = name.lower()
        for module in self.modules.values():
            if module.name.lower() == name_lower:
                return module
        return None

    def _check_hashflags(self, bot: "IRCBot.Bot", definition: ModuleDefinition
            ) -> None:
        for hashflag, value in definition.hashflags:
            if hashflag == "ignore":
               # nope, ignore this module.
               raise ModuleNotLoadableWarning("module ignored")

            elif hashflag == "require-config" and value:
                if not self.config.get(value.lower(), None):
                    # nope, required config option not present.
                    raise ModuleNotLoadableWarning(
                        "required config not present")

    def _load_module(self, bot: "IRCBot.Bot", definition: ModuleDefinition,
            check_dependency: bool=True) -> LoadedModule:
        if definition.name in self.modules:
            raise ModuleNameCollisionException("Module name '%s' "
                "attempted to be used twice" % definition.name)

        if check_dependency:
            dependencies = definition.get_dependencies()
            for dependency in dependencies:
                if not dependency in self.modules:
                    raise ModuleDependencyNotFulfilled(definition.name,
                        dependency)

        self._check_hashflags(bot, definition)

        context = str(uuid.uuid4())
        import_name = self._import_name(definition.name, context)

        import_spec = importlib.util.spec_from_file_location(import_name,
            definition.filename)
        module = importlib.util.module_from_spec(import_spec)
        sys.modules[import_name] = module
        loader = typing.cast(importlib.abc.Loader, import_spec.loader)
        loader.exec_module(module)

        mo