import os, inspect, collections

pList = { }
mList = collections.defaultdict(list)

class NoSuchPluginError(Exception): pass

def plugin(cls):
    #Using inspect, figure out what file is calling this function (using @plugin)
    clsFile = inspect.stack()[1][1]
    
    #Split the path, because it will be in the form "plugins" + os.sep + "*.py"
    modFile = clsFile.split(os.sep)[1]
    
    #Append the class name to the list indexed by module file name
    mList[modFile].append(cls.__name__.lower())
    
    #Set class (plugin) name to the class (plugin) reference
    pList[cls.__name__.lower()] = cls
    
    #return cls, since decorators must return the "new" type
    return cls

def refresh(pluginName = None):
    """
    Refreshes the list of module/class pairs, and plugin/class pairs.
    
    If pluginName is not None, it can raise NoSuchPluginError.
    Regardless of the value of pluginName, it can raise any error a python file can raise, samme as using import.
    """
    
    #if we are asked to do a general refresh of list
    if pluginName is None:
        #we were asked to refresh, so clear out our lists so we can start fresh
        pList.clear()
        mList.clear()
        
        #load every module in the plugins package
        _files = [os.path.join("plugins", f) for f in os.listdir("plugins") if f != "__init__.py" and f.endswith(".py") and not os.path.isdir(os.path.join("plugins",f))]
        
        #for file (module) in the file list
        for f in _files:
            #Set "plugin" in the environment so that @plugin decorators work correctly
            env = {"plugin":plugin}
            
            #Execute the file. The file will automatically update the list of plugins, due to the @plugin decorator
            execfile(f, env, env)
    else:
        #We're trying to refresh a module, so use _reload instead.
        found = __reload(pluginName)
        #if it wasn't found, try refreshing all modules and try again
        if not found:
            refresh()
            found = __reload(pluginName)
            
            #if it's still not found, raise an error.
            if not found: 
                raise NoSuchPluginError()


def __reload(pluginName):
    #iterate over each "file", "module" pair in mList
    for f,classes in mList:
        #if the requested
        if pluginName in classes:
            #Set "plugin" in the environment so that @plugin decorators work correctly
            env = {"plugin":plugin}
            
            #Execute the file. The file will automatically update the list of plugins, due to the @plugin decorator
            execfile(f, env, env)
            
            #return True, indicating we have found and reloaded the module.
            return True
    
    #Else, we have not found it, so return False.
    return False

def getPlugin(name):
    """ Helper function to get a plugin's class. """
    try:
        return pList[name.lower()]
    except KeyError:
        return None

refresh()