# -*- coding: utf-8 -*-

###############################################################################
#   Copyright(c) 2008-2013 pyLoad Team
#   http://www.pyload.org
#
#   This file is part of pyLoad.
#   pyLoad is free software: you can redistribute it and/or modify
#   it under the terms of the GNU Affero General Public License as
#   published by the Free Software Foundation, either version 3 of the
#   License, or (at your option) any later version.
#
#   Subjected to the terms and conditions in LICENSE
#
#   @author: RaNaN, mkaay
###############################################################################

import re
import sys

from os import listdir, makedirs
from os.path import isfile, join, exists, abspath, basename
from sys import version_info
from time import time
from collections import defaultdict

from pyload.lib.SafeEval import const_eval as literal_eval
from pyload.plugins.Base import Base

from new_collections import namedtuple

#TODO: ignores not updatable

# ignore these plugin configs, mainly because plugins were wiped out
IGNORE = (
    "FreakshareNet", "SpeedManager", "ArchiveTo", "ShareCx", ('addons', 'UnRar'),
    'EasyShareCom', 'FlyshareCz'
)

PluginTuple = namedtuple("PluginTuple", "version re deps category user path")

class BaseAttributes(defaultdict):
    """ Dictionary that loads defaults values from Base object """
    def __missing__(self, key):
        attr = "__%s__" % key
        if not hasattr(Base, attr):
            return defaultdict.__missing__(self, key)

        return getattr(Base, attr)


class PluginManager:
    ROOT = "pyload.plugins."
    LOCALROOT = "localplugins."
    TYPES = ("crypter", "hoster", "accounts", "addons", "network", "internal")

    BUILTIN = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s?(True|False|None|[0-9x.]+)', re.I)
    SINGLE = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*(?:r|u|_)?((?:(?<!")"(?!")|\'|\().*(?:(?<!")"(?!")|\'|\)))',
                        re.I)
    # note the nongreedy character: that means we can not embed list and dicts
    MULTI = re.compile(r'__(?P<attr>[a-z0-9_]+)__\s*=\s*((?:\{|\[|"{3}).*?(?:"""|\}|\]))', re.DOTALL | re.M | re.I)

    NO_MATCH = re.compile(r'^no_match$')

    def __init__(self, core):
        self.core = core

        #self.config = self.core.config
        self.log = core.log

        self.plugins = {}
        self.modules = {} # cached modules
        self.history = [] # match history to speedup parsing (type, name)
        self.user_context = {} # plugins working with user context
        self.createIndex()

        #register for import addon
        sys.meta_path.append(self)

    def logDebug(self, type, plugin, msg):
        self.log.debug("Plugin %s | %s: %s" % (type, plugin, msg))

    def createIndex(self):
        """create information for all plugins available"""
        # add to path, so we can import from userplugins
        sys.path.append(abspath(""))

        if not exists("userplugins"):
            makedirs("userplugins")
        if not exists(join("userplugins", "__init__.py")):
            f = open(join("userplugins", "__init__.py"), "wb")
            f.close()

        a = time()
        for type in self.TYPES:
            self.plugins[type] = self.parse(type)

        self.log.debug("Created index of plugins in %.2f ms", (time() - a) * 1000)

    def parse(self, folder, home=None):
        """  Analyze and parses all plugins in folder """
        plugins = {}
        if home:
            pfolder = join("userplugins", folder)
            if not exists(pfolder):
                makedirs(pfolder)
            if not exists(join(pfolder, "__init__.py")):
                f = open(join(pfolder, "__init__.py"), "wb")
                f.close()

        else:
            pfolder = join(pypath, "pyload", "plugins", folder)

        for f in listdir(pfolder):
            if (isfile(join(pfolder, f)) and f.endswith(".py") or f.endswith("_25.pyc") or f.endswith(
                    "_26.pyc") or f.endswith("_27.pyc")) and not f.startswith("_"):
                if f.endswith("_25.pyc") and version_info[0:2] != (2, 5):
                    continue
                elif f.endswith("_26.pyc") and version_info[0:2] != (2, 6):
                    continue
                elif f.endswith("_27.pyc") and version_info[0:2] != (2, 7):
                    continue

                # replace suffix and version tag
                name = f[:-3]
                if name[-1] == ".": name = name[:-4]

                plugin = self.parsePlugin(join(pfolder, f), folder, name, home)
                if plugin:
                    plugins[name] = plugin

        if not home:
            temp = self.parse(folder, plugins)
            plugins.update(temp)

        return plugins

    def parseAttributes(self, filename, name, folder=""):
        """ Parse attribute dict from plugin"""
        data = open(filename, "rb")
        content = data.read()
        data.close()

        attrs = BaseAttributes()
        for m in self.BUILTIN.findall(content) + self.SINGLE.findall(content) + self.MULTI.findall(content):
            #replace gettext function and eval result
            try:
                attrs[m[0]] = literal_eval(m[-1].replace("_(", "("))
            except:
                self.logDebug(folder, name, "Error when parsing: %s" % m[-1])
                self.core.print_exc()

            if not hasattr(Base, "__%s__" % m[0]):
                if m[0] != "type": #TODO remove type from all plugins, its not needed
                    self.logDebug(folder, name, "Unknown attribute '%s'" % m[0])

        return attrs

    def parsePlugin(self, filename, folder, name, home=None):
        """  Parses a plugin from disk, folder means plugin type in this context. Also sets config.

        :arg home: dict with plugins, of which the found one will be matched against (according version)
        :returns PluginTuple"""

        attrs = self.parseAttributes(filename, name, folder)
        if not attrs: return

        version = 0

        if "version" in attrs:
            try:
                version = float(attrs["version"])
            except ValueError:
                self.logDebug(folder, name, "Invalid version %s" % attrs["version"])
                version = 9 #TODO remove when plugins are fixed, causing update loops
        else:
            self.logDebug(folder, name, "No version attribute")

        # home contains plugins from pyload root
        if home and name in home:
            if home[name].version >= version:
                return

        if name in IGNORE or (folder, name) in IGNORE:
            return

        if "pattern" in attrs and attrs["pattern"]:
            try:
                plugin_re = re.compile(attrs["pattern"], re.I)
            except:
                self.logDebug(folder, name, "Invalid regexp pattern '%s'" % attrs["pattern"])
                plugin_re = self.NO_MATCH
        else:
            plugin_re = self.NO_MATCH

        deps = attrs["dependencies"]
        category = attrs["category"] if folder == "addons" else ""

        # create plugin tuple
        plugin = PluginTuple(version, plugin_re, deps, category, bool(home), filename)

        # These have none or their own config
        if folder in ("internal", "accounts", "network"):
            return plugin

        if folder == "addons" and "config" not in attrs and not attrs["internal"]:
            attrs["config"] = (["activated", "bool", "Activated", False],)

        if "config" in attrs and attrs["config"] is not None:
            config = attrs["config"]
            desc = attrs["description"]
            expl = attrs["explanation"]

            # Convert tuples to list
            config = [list(x) for x in config]

            if folder == "addons" and not attrs["internal"]:
                for item in config:
                    if item[0] == "activated": break
                else: # activated flag missing
                    config.insert(0, ("activated", "bool", "Activated", False))

            # Everything that is no addon and user_context=True, is added to dict
            if folder != "addons" or attrs["user_context"]:
                self.user_context[name] = True

            try:
                self.core.config.addConfigSection(name, name, desc, expl, config)
            except:
                self.logDebug(folder, name, "Invalid config  %s" % config)

        return plugin


    def parseUrls(self, urls):
        """parse plugins for given list of urls, separate to crypter and hoster"""

        res = {"hoster": [], "crypter": []} # tupels of (url, plugin)

        for url in urls:
            if type(url) not in (str, unicode, buffer):
                self.log.debug("Parsing invalid type %s" % type(url))
                continue

            found = False

            for ptype, name in self.history:
                if self.plugins[ptype][name].re.match(url):
                    res[ptype].append((url, name))
                    found = (ptype, name)
                    break # need to exit this loop first

            if found:  # found match
                if self.history[0] != found: #update history
                    self.history.remove(found)
                    self.history.insert(0, found)
                continue

            for ptype in ("crypter", "hoster"):
                for name, plugin in self.plugins[ptype].iteritems():
                    if plugin.re.match(url):
                        res[ptype].append((url, name))
                        self.history.insert(0, (ptype, name))
                        del self.history[10:] # cut down to size of 10
                        found = True
                        break

            if not found:
                res["hoster"].append((url, "BasePlugin"))

        return res["hoster"], res["crypter"]

    def getPlugins(self, type):
        return self.plugins.get(type, None)

    def findPlugin(self, name, pluginlist=("hoster", "crypter")):
        for ptype in pluginlist:
            if name in self.plugins[ptype]:
                return ptype, self.plugins[ptype][name]
        return None, None

    def getPluginModule(self, name):
        """ Decprecated: return plugin module from hoster|crypter"""
        self.log.debug("Deprecated method: .getPluginModule()")
        type, plugin = self.findPlugin(name)
        return self.loadModule(type, name)

    def getPluginClass(self, name):
        """ return plugin class from hoster|crypter, always the not overwritten one """
        type, plugin = self.findPlugin(name)
        return self.loadClass(type, name)

    #  MultiHoster will overwrite this
    getPlugin = getPluginClass

    def loadAttributes(self, type, name):
        plugin = self.plugins[type][name]
        return self.parseAttributes(plugin.path, name, type)

    def loadModule(self, type, name):
        """ Returns loaded module for plugin

        :param type: plugin type, subfolder of module.plugins
        :param name:
        """
        plugins = self.plugins[type]
        if name in plugins:
            if (type, name) in self.modules: return self.modules[(type, name)]
            try:
                # convert path to python recognizable import
                path = basename(plugins[name].path).replace(".pyc", "").replace(".py", "")
                module = __import__(self.ROOT + "%s.%s" % (type, path), globals(), locals(), path)
                self.modules[(type, name)] = module # cache import, maybe unneeded
                return module
            except Exception, e:
                self.log.error(_("Error importing %(name)s: %(msg)s") % {"name": name, "msg": str(e)})
                self.core.print_exc()

    def loadClass(self, type, name):
        """Returns the class of a plugin with the same name"""
        module = self.loadModule(type, name)
        if module: return getattr(module, name)

    def find_module(self, fullname, path=None):
        #redirecting imports if necesarry
        if fullname.startswith(self.ROOT) or fullname.startswith(self.LOCALROOT): #separate pyload plugins
            if fullname.startswith(self.LOCALROOT):
                user = 1
            else:
                user = 0 #used as bool and int

            split = fullname.split(".")
            if len(split) != 4 - user: return
            type, name = split[2 - user:4 - user]

            if type in self.plugins and name in self.plugins[type]:
                #userplugin is a newer version
                if not user and self.plugins[type][name].user:
                    return self
                    #imported from userdir, but pyloads is newer
                if user and not self.plugins[type][name].user:
                    return self

        # TODO: Remove when all plugin imports are adapted
        if "module" in fullname:
            return self


    def load_module(self, name, replace=True):
        if name not in sys.modules:  #could be already in modules

            # TODO: only temporary
            if name.endswith("module"):
                # name = "pyload."
                name = name.replace(".module", "")
                self.log.debug("Old import reference detected, use %s" % name)
                replace = False
                return __import__("pyload")
            if name.startswith("module"):
                name = name.replace("module", "pyload")
                self.log.debug("Old import reference detected, use %s" % name)
                replace = False

            if replace:
                if self.ROOT in name:
                    newname = name.replace(self.ROOT, self.LOCALROOT)
                else:
                    newname = name.replace(self.LOCALROOT, self.ROOT)
            else:
                newname = name

            base, plugin = newname.rsplit(".", 1)

            self.log.debug("Redirected import %s -> %s" % (name, newname))

            module = __import__(newname, globals(), locals(), [plugin])
            #inject under new an old name
            sys.modules[name] = module
            sys.modules[newname] = module

        return sys.modules[name]

    def reloadPlugins(self, type_plugins):
        """ reloads and reindexes plugins """
        if not type_plugins: return False

        self.log.debug("Request reload of plugins: %s" % type_plugins)

        as_dict = {}
        for t, n in type_plugins:
            if t in as_dict:
                as_dict[t].append(n)
            else:
                as_dict[t] = [n]

        # we do not reload addons or internals, would cause to much side effects
        if "addons" in as_dict or "internal" in as_dict:
            return False

        for type in as_dict.iterkeys():
            for plugin in as_dict[type]:
                if plugin in self.plugins[type]:
                    if (type, plugin) in self.modules:
                        self.log.debug("Reloading %s" % plugin)
                        reload(self.modules[(type, plugin)])

        # index re-creation
        for type in ("crypter", "container", "hoster", "captcha", "accounts"):
            self.plugins[type] = self.parse(type)

        if "accounts" in as_dict: #accounts needs to be reloaded
            self.core.accountManager.initPlugins()
            self.core.scheduler.addJob(0, self.core.accountManager.getAccountInfos)

        return True

    def isUserPlugin(self, plugin):
        """ A plugin suitable for multiple user """
        return plugin in self.user_context

    def isPluginType(self, plugin, type):
        return plugin in self.plugins[type]

    def getCategory(self, plugin):
        if plugin in self.plugins["addons"]:
            return self.plugins["addons"][plugin].category or "addon"

    def loadIcon(self, name):
        """ load icon for single plugin, base64 encoded"""
        pass

    def checkDependencies(self, type, name):
        """ Check deps for given plugin

        :return: List of unfullfilled dependencies
        """
        pass