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

import sys
import re

from os import remove, stat
from os.path import join, isfile
from time import time

from module.ConfigParser import IGNORE
from module.network.RequestFactory import getURL
from module.plugins.Hook import threaded, Expose, Hook


class UpdateManager(Hook):
    __name__ = "UpdateManager"
    __version__ = "0.24"
    __description__ = """Check for updates"""
    __config__ = [("activated", "bool", "Activated", True),
                  ("mode", "pyLoad + plugins;plugins only", "Check updates for", "pyLoad + plugins"),
                  ("interval", "int", "Check interval in hours", 8),
                  ("reloadplugins", "bool", "Monitor plugins for code changes (debug mode only)", True),
                  ("nodebugupdate", "bool", "Don't check for updates in debug mode", True)]
    __author_name__ = ("RaNaN", "stickell", "Walter Purcaro")
    __author_mail__ = ("ranan@pyload.org", "l.stickell@yahoo.it", "vuolter@gmail.com")

    SERVER_URL = "http://updatemanager.pyload.org"
    MIN_TIME = 3 * 60 * 60  #: 3h minimum check interval

    event_list = ["pluginConfigChanged"]


    def pluginConfigChanged(self, plugin, name, value):
        if name == "interval" and 0 < value != self.interval:
            self.interval = max(value * 60 * 60, self.MIN_TIME)
            self.initPeriodical()
        elif name == "reloadplugins":
            if self.cb2:
                self.core.scheduler.removeJob(self.cb2)
            if value and self.core.debug:
                self.periodical2()

    def coreReady(self):
        self.pluginConfigChanged(self.__name__, "reloadplugins", self.getConfig("reloadplugins"))

    def setup(self):
        self.cb2 = None
        self.interval = self.MIN_TIME
        self.updating = False
        self.info = {"pyload": False, "version": None, "plugins": False}
        self.mtimes = {}  #: store modification time for each plugin


    def periodical2(self):
        if not self.updating:
            self.autoreloadPlugins()
        self.cb2 = self.core.scheduler.addJob(10, self.periodical2, threaded=True)

    @Expose
    def autoreloadPlugins(self):
        """ reload and reindex all modified plugins """
        modules = filter(
            lambda m: m and (m.__name__.startswith("module.plugins.") or m.__name__.startswith(
                "userplugins.")) and m.__name__.count(".") >= 2, sys.modules.itervalues())

        reloads = []

        for m in modules:
            root, type, name = m.__name__.rsplit(".", 2)
            id = (type, name)
            if type in self.core.pluginManager.plugins:
                f = m.__file__.replace(".pyc", ".py")
                if not isfile(f):
                    continue

                mtime = stat(f).st_mtime

                if id not in self.mtimes:
                    self.mtimes[id] = mtime
                elif self.mtimes[id] < mtime:
                    reloads.append(id)
                    self.mtimes[id] = mtime

        return True if self.core.pluginManager.reloadPlugins(reloads) else False

    @threaded
    def periodical(self):
        if not self.info["pyload"] and not (self.getConfig("nodebugupdate") and self.core.debug):
            self.updating = True
            self.update(onlyplugin=True if self.getConfig("mode") == "plugins only" else False)
            self.updating = False

    def server_response(self):
        try:
            return getURL(self.SERVER_URL, get={'v': self.core.api.getServerVersion()}).splitlines()
        except:
            self.logWarning(_("Not able to connect server to get updates"))

    @Expose
    def updatePlugins(self):
        """ simple wrapper for calling plugin update quickly """
        return self.update(onlyplugin=True)

    @Expose
    def update(self, onlyplugin=False):
        """ check for updates """
        data = self.server_response()
        if not data:
            r = False
        elif data[0] == "None":
            self.logInfo(_("No pyLoad version available"))
            updates = data[1:]
            r = self._updatePlugins(updates)
        elif onlyplugin:
            r = False
        else:
            newversion = data[0]
            self.logInfo(_("***  New pyLoad Version %s available  ***") % newversion)
            self.logInfo(_("***  Get it here: https://github.com/pyload/pyload/releases  ***"))
            r = self.info["pyload"] = True
            self.info["version"] = newversion
        return r

    def _updatePlugins(self, updates):
        """ check for plugin updates """

        if self.info["plugins"]:
            return False  #: plugins were already updated

        updated = []

        vre = re.compile(r'__version__.*=.*("|\')([0-9.]+)')
        url = updates[0]
        schema = updates[1].split("|")
        if 'BLACKLIST' in updates:
            blacklist = updates[updates.index('BLACKLIST') + 1:]
            updates = updates[2:updates.index('BLACKLIST')]
        else:
            blacklist = None
            updates = updates[2:]

        for plugin in updates:
            info = dict(zip(schema, plugin.split("|")))
            filename = info["name"]
            prefix = info["type"]
            version = info["version"]

            if filename.endswith(".pyc"):
                name = filename[:filename.find("_")]
            else:
                name = filename.replace(".py", "")

            #TODO: obsolete in 0.5.0
            if prefix.endswith("s"):
                type = prefix[:-1]
            else:
                type = prefix

            plugins = getattr(self.core.pluginManager, "%sPlugins" % type)

            if name not in plugins or name in IGNORE or (type, name) in IGNORE:
                continue

            oldver = float(plugins[name]["v"])
            newver = float(version)

            if oldver >= newver:
                continue
            else:
                self.logInfo(_("New version of [%(type)s] %(name)s (v%(oldver)s -> v%(newver)s)") % {
                    "type": type,
                    "name": name,
                    "oldver": oldver,
                    "newver": newver
                })

            try:
                content = getURL(url % info)
            except Exception, e:
                self.logError(_("Error when updating plugin %s") % filename, str(e))
                continue

            m = vre.search(content)
            if not m or m.group(2) != version:
                self.logError(_("Error when updating plugin %s") % name, _("Version mismatch"))
                continue

            f = open(join("userplugins", prefix, filename), "wb")
            f.write(content)
            f.close()
            updated.append((prefix, name))

        if blacklist:
            removed = self.removePlugins(map(lambda x: x.split('|'), blacklist))
            for t, n in removed:
                self.logInfo(_("Removed blacklisted plugin: [%(type)s] %(name)s") % {
                    "type": t,
                    "name": n
                })

        if updated:
            reloaded = self.core.pluginManager.reloadPlugins(updated)
            if reloaded:
                self.logInfo(_("Plugins updated and reloaded"))
            else:
                self.logInfo(_("*** Plugins have been updated, pyLoad will be restarted now ***"))
                self.info["plugins"] = True
                self.core.scheduler.addJob(4, self.core.api.restart(), threaded=False)  #: risky, but pyload doesn't let more
            return True
        else:
            self.logInfo(_("No plugin updates available"))
            return False

    @Expose
    def removePlugins(self, type_plugins):
        """ delete plugins under userplugins directory"""
        if not type_plugins:
            return None

        self.logDebug("Request deletion of plugins: %s" % type_plugins)

        removed = []

        for type, name in type_plugins:
            py = join("userplugins", type, name)
            pyc = join("userplugins", type, name.replace('.py', '.pyc'))
            if isfile(py):
                id = (type, name)
                remove(py)
                removed.append(id)
            if isfile(pyc):
                remove(pyc)

        return removed  #: return a list of the plugins successfully removed
