# -*- coding: utf-8 -*-
#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2019 Yorik van Havre <yorik@uncreated.net>              *
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU Lesser General Public License (LGPL)    *
#*   as published by the Free Software Foundation; either version 2 of     *
#*   the License, or (at your option) any later version.                   *
#*   for detail see the LICENCE text file.                                 *
#*                                                                         *
#*   This program is distributed in the hope that it will be useful,       *
#*   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
#*   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
#*   GNU Library General Public License for more details.                  *
#*                                                                         *
#*   You should have received a copy of the GNU Library General Public     *
#*   License along with this program; if not, write to the Free Software   *
#*   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  *
#*   USA                                                                   *
#*                                                                         *
#***************************************************************************

import os
import re
import shutil
import sys

from PySide import QtCore, QtGui

import FreeCAD
if FreeCAD.GuiUp:
    import FreeCADGui

import addonmanager_utilities as utils
from addonmanager_utilities import translate  # this needs to be as is for pylupdate
from addonmanager_macro import Macro

have_git = False
try:
    import git
    have_git = True
except ImportError:
    pass

have_zip = False
try:
    import zipfile
    have_zip = True
except ImportError:
    pass

have_markdown = False
try:
    import markdown
    have_markdown = True
except ImportError:
    pass

try:
    import StringIO as io
    _stringio = io.StringIO
except ImportError:  # StringIO is not available with python3
    import io
    _stringio = io.BytesIO

#  @package AddonManager_workers
#  \ingroup ADDONMANAGER
#  \brief Multithread workers for the addon manager
#  @{

# Blacklisted addons
macros_blacklist = []

# These addons will print an additional message informing the user
obsolete = []

# These addons will print an additional message informing the user Python2 only
py2only = []

NOGIT = False  # for debugging purposes, set this to True to always use http downloads

NOMARKDOWN = False  # for debugging purposes, set this to True to disable Markdown lib

url_to_branch_map = {} # Added to 0.19.3 to support branches with names other than "master"

"""Multithread workers for the Addon Manager"""


class UpdateWorker(QtCore.QThread):
    """This worker updates the list of available workbenches"""

    info_label = QtCore.Signal(str)
    addon_repo = QtCore.Signal(object)
    progressbar_show = QtCore.Signal(bool)
    done = QtCore.Signal()

    def __init__(self):

        QtCore.QThread.__init__(self)

    def run(self):
        "populates the list of addons"

        self.progressbar_show.emit(True)

        # update info lists
        global obsolete, macros_blacklist, py2only
        u = utils.urlopen("https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/addonflags.json")
        if u:
            p = u.read()
            if sys.version_info.major >= 3 and isinstance(p, bytes):
                p = p.decode("utf-8")
            u.close()
            hit = re.findall(r'"obsolete"[^\{]*?{[^\{]*?"Mod":\[(?P<obsolete>[^\[\]]+?)\]}',
                             p.replace("\n", "").replace(" ", ""))
            if hit:
                obsolete = hit[0].replace('"', "").split(",")
            hit = re.findall(r'"blacklisted"[^\{]*?{[^\{]*?"Macro":\[(?P<blacklisted>[^\[\]]+?)\]}',
                             p.replace("\n", "").replace(" ", ""))
            if hit:
                macros_blacklist = hit[0].replace('"', "").split(",")
            hit = re.findall(r'"py2only"[^\{]*?{[^\{]*?"Mod":\[(?P<py2only>[^\[\]]+?)\]}',
                             p.replace("\n", "").replace(" ", ""))
            if hit:
                py2only = hit[0].replace('"', "").split(",")
        else:
            print("Debug: addon_flags.json not found")

        u = utils.urlopen("https://raw.githubusercontent.com/FreeCAD/FreeCAD-addons/master/.gitmodules")
        if not u:
            self.progressbar_show.emit(False)
            self.done.emit()
            self.stop = True
            return
        p = u.read()
        if sys.version_info.major >= 3 and isinstance(p, bytes):
            p = p.decode("utf-8")
        u.close()
        p = re.findall((r'(?m)\[submodule\s*"(?P<name>.*)"\]\s*'
                        r"path\s*=\s*(?P<path>.+)\s*"
                        r"url\s*=\s*(?P<url>https?://.*)\s*"
                        r"(branch\s*=\s*(?P<branch>.*)\s*)?"), p)
        basedir = FreeCAD.getUserAppDataDir()
        moddir = basedir + os.sep + "Mod"
        repos = []
        # querying official addons
        for name, path, url, _, branch in p:
            self.info_label.emit(name)
            url = url.split(".git")[0]
            addondir = moddir + os.sep + name
            if os.path.exists(addondir) and os.listdir(addondir):
                # make sure the folder exists and it contains files!
                state = 1
            else:
                state = 0
            repos.append([name, url, state])
            if branch is None or len(branch) == 0:
                 branch = "master"
            url_to_branch_map[url] = branch
        # querying custom addons
        customaddons = (FreeCAD.ParamGet("User parameter:BaseApp/Preferences/Addons")
                        .GetString("CustomRepositories", "").split("\n"))
        for url in customaddons:
            if url:
                if " " in url:
                    addon_and_branch = addon.split(" ")
                    url = addon_and_branch[0]
                    branch = addon_and_branch[1]
                else:
                    branch = "master"
                name = url.split("/")[-1]
                if name.lower().endswith(".git"):
                    name = name[:-4]
                addondir = moddir + os.sep + name
                if not os.path.exists(addondir):
                    state = 0
                else:
                    state = 1
                repos.append([name, url, state])
                url_to_branch_map[url] = branch
        if not repos:
            self.info_label.emit(translate("AddonsInstaller", "Unable to download addon list."))
        else:
            repos = sorted(repos, key=lambda s: s[0].lower())
            for repo in repos:
                self.addon_repo.emit(repo)
            self.info_label.emit(translate("AddonsInstaller", "Workbenches list was updated."))
        self.progressbar_show.emit(False)
        self.done.emit()
        self.stop = True


class InfoWorker(QtCore.QThread):
    """This worker retrieves the description text of a workbench"""

    addon_repos = QtCore.Signal(object)

    def __init__(self):

        QtCore.QThread.__init__(self)

    def run(self):

        i = 0
        for repo in self.repos:
            url = repo[1]
            u = utils.urlopen(url)
            if not u:
                self.stop = True
                return
            p = u.read()
            if sys.version_info.major >= 3 and isinstance(p, bytes):
                p = p.decode("utf-8")
            u.close()
            desc = re.findall('<meta property="og:description" content="(.*?)"', p)
            if desc:
                desc = desc[0]
            else:
                desc = "Unable to retrieve addon description"
            self.repos[i].append(desc)
            i += 1
            self.addon_repos.emit(self.repos)
        self.stop = True


class CheckWBWorker(QtCore.QThread):
    """This worker checks for available updates for all workbenches"""

    enable = QtCore.Signal(int)
    mark = QtCore.Signal(str)
    addon_repos = QtCore.Signal(object)

    def __init__(self, repos):

        QtCore.QThread.__init__(self)
        self.repos = repos

    def run(self):

        if NOGIT or not have_git:
            self.stop = True
            return
        basedir = FreeCAD.getUserAppDataDir()
        moddir = basedir + os.sep + "Mod"
        upds = []
        gitpython_warning = False
        for repo in self.repos:
            if repo[2] == 1:  # installed
                # print("Checking for updates for", repo[0])
                clonedir = moddir + os.sep + repo[0]
                if os.path.exists(clonedir):
                    # mark as already installed AND already checked for updates
                    self.repos[self.repos.index(repo)][2] = 2
                    if not os.path.exists(clonedir + os.sep + ".git"):
                        # Repair addon installed with raw download
                        bare_repo = git.Repo.clone_from(repo[1], clonedir + os.sep + ".git", bare=True)
                        try:
                            with bare_repo.config_writer() as cw:
                                cw.set("core", "bare", False)
                        except AttributeError:
                            if not gitpython_warning:
                                FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                                       "Outdated GitPython detected, "
                                                                       "consider upgrading with pip.") + "\n")
                                gitpython_warning = True
                            cw = bare_repo.config_writer()
                            cw.set("core", "bare", False)
                            del cw
                        repo = git.Repo(clonedir)
                        repo.head.reset("--hard")
                    gitrepo = git.Git(clonedir)
                    try:
                        gitrepo.fetch()
                    except Exception:
                        print("AddonManager: Unable to fetch git updates for repo", repo[0])
                    else:
                        try:
                            if "git pull" in gitrepo.status():
                                self.mark.emit(repo[0])
                                upds.append(repo[0])
                                # mark as already installed AND already checked for updates AND update available
                                self.repos[self.repos.index(repo)][2] = 3
                        except stderr:
                            FreeCAD.Console.PrintWarning("AddonManager - " + repo[0] + " git status"
                                                         " fatal: this operation must be run in a work tree \n")
        self.addon_repos.emit(self.repos)
        self.enable.emit(len(upds))
        self.stop = True


class FillMacroListWorker(QtCore.QThread):
    """This worker populates the list of macros"""

    add_macro_signal = QtCore.Signal(Macro)
    info_label_signal = QtCore.Signal(str)
    progressbar_show = QtCore.Signal(bool)

    def __init__(self, repo_dir):

        QtCore.QThread.__init__(self)
        self.repo_dir = repo_dir
        self.macros = []

    def run(self):
        """Populates the list of macros"""

        self.retrieve_macros_from_git()
        self.retrieve_macros_from_wiki()
        [self.add_macro_signal.emit(m) for m in sorted(self.macros, key=lambda m: m.name.lower())]
        if self.macros:
            self.info_label_signal.emit(translate("AddonsInstaller", "List of macros successfully retrieved."))
        self.progressbar_show.emit(False)
        self.stop = True

    def retrieve_macros_from_git(self):
        """Retrieve macros from FreeCAD-macros.git

        Emits a signal for each macro in
        https://github.com/FreeCAD/FreeCAD-macros.git
        """

        if not have_git:
            self.info_label_signal.emit("GitPython not installed! Cannot retrieve macros from Git")
            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                   "GitPython not installed! Cannot retrieve macros from git")+"\n")
            return

        self.info_label_signal.emit("Downloading list of macros from git...")
        try:
            git.Repo.clone_from("https://github.com/FreeCAD/FreeCAD-macros.git", self.repo_dir)
        except Exception:
            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                   "Something went wrong with the Git Macro Retrieval, "
                                                   "possibly the Git executable is not in the path") + "\n")
        for dirpath, _, filenames in os.walk(self.repo_dir):
            if ".git" in dirpath:
                continue
            for filename in filenames:
                if filename.lower().endswith(".fcmacro"):
                    macro = Macro(filename[:-8])  # Remove ".FCMacro".
                    macro.on_git = True
                    macro.src_filename = os.path.join(dirpath, filename)
                    self.macros.append(macro)

    def retrieve_macros_from_wiki(self):
        """Retrieve macros from the wiki

        Read the wiki and emit a signal for each found macro.
        Reads only the page https://www.freecadweb.org/wiki/Macros_recipes
        """

        self.info_label_signal.emit("Downloading list of macros from the FreeCAD wiki...")
        self.progressbar_show.emit(True)
        u = utils.urlopen("https://www.freecadweb.org/wiki/Macros_recipes")
        if not u:
            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                   "Appears to be an issue connecting to the Wiki, "
                                                   "therefore cannot retrieve Wiki macro list at this time") + "\n")
            return
        p = u.read()
        u.close()
        if sys.version_info.major >= 3 and isinstance(p, bytes):
            p = p.decode("utf-8")
        macros = re.findall('title="(Macro.*?)"', p)
        macros = [mac for mac in macros if ("translated" not in mac)]
        for mac in macros:
            macname = mac[6:]  # Remove "Macro ".
            macname = macname.replace("&amp;", "&")
            if (macname not in macros_blacklist) and ("recipes" not in macname.lower()):
                macro = Macro(macname)
                macro.on_wiki = True
                self.macros.append(macro)


class ShowWorker(QtCore.QThread):
    """This worker retrieves info of a given workbench"""

    info_label = QtCore.Signal(str)
    addon_repos = QtCore.Signal(object)
    progressbar_show = QtCore.Signal(bool)

    def __init__(self, repos, idx):

        # repos is a list of [name, url, installbit, descr]
        #   name      : Addon name
        #   url       : Addon repository location
        #   installbit: 0 = Addon is not installed
        #               1 = Addon is installed
        #               2 = Addon is installed and checked for available updates (none pending)
        #               3 = Addon is installed and has a pending update
        #   descr     : Addon description

        QtCore.QThread.__init__(self)
        self.repos = repos
        self.idx = idx

    def run(self):

        self.progressbar_show.emit(True)
        self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))

        if len(self.repos[self.idx]) == 4:
            desc = self.repos[self.idx][3]
        else:
            u = None
            url = self.repos[self.idx][1]
            if url in url_to_branch_map:
                branch = url_to_branch_map[url]
            else:
                branch = "master"
            self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + " " + str(url))
            desc = ""
            regex = utils.get_readme_regex(url)
            if regex:
                # extract readme from html via regex
                readmeurl = utils.get_readme_html_url(url,branch)
                if not readmeurl:
                    print("Debug: README not found for", url)
                u = utils.urlopen(readmeurl)
                if not u:
                    print("Debug: README not found at", readmeurl)
                u = utils.urlopen(readmeurl)
                if u:
                    p = u.read()
                    if sys.version_info.major >= 3 and isinstance(p, bytes):
                        p = p.decode("utf-8")
                    u.close()
                    readme = re.findall(regex, p, flags=re.MULTILINE | re.DOTALL)
                    if readme:
                        desc = readme[0]
                else:
                    print("Debug: README not found at", readmeurl)
            else:
                # convert raw markdown using lib
                readmeurl = utils.get_readme_url(url,branch)
                if not readmeurl:
                    print("Debug: README not found for", url)
                u = utils.urlopen(readmeurl)
                if u:
                    p = u.read()
                    if sys.version_info.major >= 3 and isinstance(p, bytes):
                        p = p.decode("utf-8")
                    u.close()
                    desc = utils.fix_relative_links(p, readmeurl.rsplit("/README.md")[0])
                    if have_markdown and not NOMARKDOWN:
                        desc = markdown.markdown(desc, extensions=["md_in_html"])
                    else:
                        message = """
<div style="width: 100%; text-align:center;background: #91bbe0;">
    <strong style="color: #FFFFFF;">
"""
                        message += translate("AddonsInstaller", "Raw markdown displayed")
                        message += "</strong><br/><br/>"
                        message += translate("AddonsInstaller", "Python Markdown library is missing.")
                        message += "<br/></div><hr/><pre>" + desc + "</pre>"
                        desc = message
                else:
                    print("Debug: README not found at", readmeurl)
            if desc == "":
                # fall back to the description text
                u = utils.urlopen(url)
                if not u:
                    self.progressbar_show.emit(False)
                    self.stop = True
                    return
                p = u.read()
                if sys.version_info.major >= 3 and isinstance(p, bytes):
                    p = p.decode("utf-8")
                u.close()
                descregex = utils.get_desc_regex(url)
                if descregex:
                    desc = re.findall(descregex, p)
                    if desc:
                        desc = desc[0]
            if not desc:
                desc = "Unable to retrieve addon description"
            self.repos[self.idx].append(desc)
            self.addon_repos.emit(self.repos)
        # Addon is installed so lets check if it has an update
        if self.repos[self.idx][2] == 1:
            upd = False
            # checking for updates
            if not NOGIT and have_git:
                repo = self.repos[self.idx]
                clonedir = FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + repo[0]
                if os.path.exists(clonedir):
                    if not os.path.exists(clonedir + os.sep + ".git"):
                        # Repair addon installed with raw download
                        bare_repo = git.Repo.clone_from(repo[1], clonedir + os.sep + ".git", bare=True)
                        try:
                            with bare_repo.config_writer() as cw:
                                cw.set("core", "bare", False)
                        except AttributeError:
                            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                                   "Outdated GitPython detected, "
                                                                   "consider upgrading with pip.") + "\n")
                            cw = bare_repo.config_writer()
                            cw.set("core", "bare", False)
                            del cw
                        repo = git.Repo(clonedir)
                        repo.head.reset("--hard")
                    gitrepo = git.Git(clonedir)
                    gitrepo.fetch()
                    if "git pull" in gitrepo.status():
                        upd = True
            # If there is an update pending, lets user know via the UI
            if upd:
                message = """
<div style="width: 100%;text-align: center;background: #75AFFD;">
    <br/>
    <strong style="background: #397FF7;color: #FFFFFF;">
"""
                message += translate("AddonsInstaller", "An update is available for this addon.")
                message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="'
                message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
                # mark as already installed AND already checked for updates AND update is available
                self.repos[self.idx][2] = 3
            # If there isn't, indicate that this addon is already installed
            else:
                message = """
<div style="width: 100%;text-align: center;background: #C1FEB2;">
    <br/>
    <strong style="background: #00B629;color: #FFFFFF;">
"""
                message += translate("AddonsInstaller", "This addon is already installed.")
                message += "</strong><br/></div><hr/>" + desc
                message += '<br/><br/>Addon repository: <a href="'
                message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
                self.repos[self.idx][2] = 2  # mark as already installed AND already checked for updates
            # Let the user know the install path for this addon
            message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
            message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
            self.addon_repos.emit(self.repos)
        elif self.repos[self.idx][2] == 2:
            message = """
<div style="width: 100%;text-align: center;background: #C1FEB2;">
    <br/>
    <strong style="background: #00B629;color: #FFFFFF;">
"""
            message += translate("AddonsInstaller", "This addon is already installed.")
            message += "</strong><br></div><hr/>" + desc
            message += '<br/><br/>Addon repository: <a href="'
            message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
            message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
            message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
        elif self.repos[self.idx][2] == 3:
            message = """
<div style="width: 100%;text-align: center;background: #75AFFD;">
    <br/>
    <strong style="background: #397FF7;color: #FFFFFF;">
"""
            message += translate("AddonsInstaller", "An update is available for this addon.")
            message += "</strong><br/></div><hr/>" + desc + '<br/><br/>Addon repository: <a href="'
            message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + "</a>"
            message += "<br/>" + translate("AddonInstaller", "Installed location") + ": "
            message += FreeCAD.getUserAppDataDir() + os.sep + "Mod" + os.sep + self.repos[self.idx][0]
        else:
            message = desc + '<br/><br/>Addon repository: <a href="'
            message += self.repos[self.idx][1] + '">' + self.repos[self.idx][1] + '</a>'

        # If the Addon is obsolete, let the user know through the Addon UI
        if self.repos[self.idx][0] in obsolete:
            message = """
<div style="width: 100%; text-align:center; background: #FFB3B3;">
    <strong style="color: #FFFFFF; background: #FF0000;">
"""
            message += translate("AddonsInstaller", "This addon is marked as obsolete") + "</strong><br/><br/>"
            message += translate("AddonsInstaller",
                                 "This usually means it is no longer maintained, "
                                 "and some more advanced addon in this list "
                                 "provides the same functionality.") + "<br/></div><hr/>" + desc

        # If the Addon is Python 2 only, let the user know through the Addon UI
        if self.repos[self.idx][0] in py2only:
            message = """
<div style="width: 100%; text-align:center; background: #ffe9b3;">
    <strong style="color: #FFFFFF; background: #ff8000;">
"""
            message += translate("AddonsInstaller", "This addon is marked as Python 2 Only") + "</strong><br/><br/>"
            message += translate("AddonsInstaller",
                                 "This workbench may no longer be maintained and "
                                 "installing it on a Python 3 system will more than "
                                 "likely result in errors at startup or while in use.")
            message += "<br/></div><hr/>" + desc

        self.info_label.emit(message)
        self.progressbar_show.emit(False)
        self.mustLoadImages = True
        label = self.loadImages(message, self.repos[self.idx][1], self.repos[self.idx][0])
        if label:
            self.info_label.emit(label)
        self.stop = True

    def stopImageLoading(self):
        "this stops the image loading process and allow the thread to terminate earlier"

        self.mustLoadImages = False

    def loadImages(self, message, url, wbName):
        "checks if the given page contains images and downloads them"

        # QTextBrowser cannot display online images. So we download them
        # here, and replace the image link in the html code with the
        # downloaded version

        imagepaths = re.findall("<img.*?src=\"(.*?)\"", message)
        if imagepaths:
            storedimages = []
            store = os.path.join(FreeCAD.getUserAppDataDir(), "AddonManager", "Images")
            if not os.path.exists(store):
                os.makedirs(store)
            for path in imagepaths:
                if not self.mustLoadImages:
                    return None
                origpath = path
                if "?" in path:
                    # remove everything after the ?
                    path = path.split("?")[0]
                if not path.startswith("http"):
                    path = utils.getserver(url) + path
                name = path.split("/")[-1]
                if name and path.startswith("http"):
                    storename = os.path.join(store, name)
                    if len(storename) >= 260:
                        remainChars = 259 - (len(store) + len(wbName) + 1)
                        storename = os.path.join(store, wbName+name[-remainChars:])
                    if not os.path.exists(storename):
                        try:
                            u = utils.urlopen(path)
                            imagedata = u.read()
                            u.close()
                        except Exception:
                            print("AddonManager: Debug: Error retrieving image from", path)
                        else:
                            f = open(storename, "wb")
                            f.write(imagedata)
                            f.close()

                            # resize the image to 300x300px if needed
                            img = QtGui.QImage(storename)
                            if (img.width() > 300) or (img.height() > 300):
                                pix = QtGui.QPixmap()
                                pix = pix.fromImage(img.scaled(300, 300,
                                                               QtCore.Qt.KeepAspectRatio,
                                                               QtCore.Qt.FastTransformation))
                                pix.save(storename, "jpeg", 100)
                    message = message.replace("src=\""+origpath, "src=\"file:///"+storename.replace("\\", "/"))
            # print(message)
            return message
        return None


class GetMacroDetailsWorker(QtCore.QThread):
    """Retrieve the macro details for a macro"""

    info_label = QtCore.Signal(str)
    progressbar_show = QtCore.Signal(bool)

    def __init__(self, macro):

        QtCore.QThread.__init__(self)
        self.macro = macro

    def run(self):

        self.progressbar_show.emit(True)
        self.info_label.emit(translate("AddonsInstaller", "Retrieving description..."))
        if not self.macro.parsed and self.macro.on_git:
            self.info_label.emit(translate("AddonsInstaller", "Retrieving info from git"))
            self.macro.fill_details_from_file(self.macro.src_filename)
        if not self.macro.parsed and self.macro.on_wiki:
            self.info_label.emit(translate("AddonsInstaller", "Retrieving info from wiki"))
            mac = self.macro.name.replace(" ", "_")
            mac = mac.replace("&", "%26")
            mac = mac.replace("+", "%2B")
            url = "https://www.freecadweb.org/wiki/Macro_" + mac
            self.macro.fill_details_from_wiki(url)
        if self.macro.is_installed():
            already_installed_msg = ('<strong style=\"background: #00B629;\">'
                                     + translate("AddonsInstaller", "This macro is already installed.")
                                     + '</strong><br>')
        else:
            already_installed_msg = ""
        message = (already_installed_msg
                   + "<h1>"+self.macro.name+"</h1>"
                   + self.macro.desc
                   + "<br/><br/>Macro location: <a href=\""
                   + self.macro.url
                   + "\">"
                   + self.macro.url
                   + "</a>")
        self.info_label.emit(message)
        self.progressbar_show.emit(False)
        self.stop = True


class InstallWorker(QtCore.QThread):
    "This worker installs a workbench"

    info_label = QtCore.Signal(str)
    progressbar_show = QtCore.Signal(bool)
    mark_recompute = QtCore.Signal(str)

    def __init__(self, repos, idx):

        QtCore.QThread.__init__(self)
        self.idx = idx
        self.repos = repos

    def run(self):
        "installs or updates the selected addon"

        if not have_git:
            self.info_label.emit("GitPython not found.")
            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                   "GitPython not found. Using standard download instead.") + "\n")
            if not have_zip:
                self.info_label.emit("no zip support.")
                FreeCAD.Console.PrintError(translate("AddonsInstaller",
                                                     "Your version of python doesn't appear to support ZIP "
                                                     "files. Unable to proceed.") + "\n")
                return

        if not isinstance(self.idx, list):
            self.idx = [self.idx]
        for idx in self.idx:
            if idx < 0:
                return
            if not self.repos:
                return
            basedir = FreeCAD.getUserAppDataDir()
            moddir = basedir + os.sep + "Mod"
            if not os.path.exists(moddir):
                os.makedirs(moddir)
            clonedir = moddir + os.sep + self.repos[idx][0]
            self.progressbar_show.emit(True)
            if os.path.exists(clonedir):
                self.info_label.emit("Updating module...")
                if sys.version_info.major > 2 and str(self.repos[idx][0]) in py2only:
                    FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                           "User requested updating a Python 2 workbench on "
                                                           "a system running Python 3 - ") +
                                                 str(self.repos[idx][0])+"\n")
                if have_git:
                    if not os.path.exists(clonedir + os.sep + ".git"):
                        # Repair addon installed with raw download
                        bare_repo = git.Repo.clone_from(self.repos[idx][1], clonedir + os.sep + ".git", bare=True)
                        try:
                            with bare_repo.config_writer() as cw:
                                cw.set("core", "bare", False)
                        except AttributeError:
                            FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                                   "Outdated GitPython detected, consider "
                                                                   "upgrading with pip.") + "\n")
                            cw = bare_repo.config_writer()
                            cw.set("core", "bare", False)
                            del cw
                        repo = git.Repo(clonedir)
                        repo.head.reset("--hard")
                    repo = git.Git(clonedir)
                    try:
                        answer = repo.pull() + "\n\n" + translate("AddonsInstaller",
                                                                  "Workbench successfully updated. "
                                                                  "Please restart FreeCAD to apply the changes.")
                    except Exception:
                        print("Error updating module", self.repos[idx][1], " - Please fix manually")
                        answer = repo.status()
                        print(answer)
                    else:
                        # Update the submodules for this repository
                        repo_sms = git.Repo(clonedir)
                        for submodule in repo_sms.submodules:
                            submodule.update(init=True, recursive=True)
                else:
                    answer = self.download(self.repos[idx][1], clonedir) + "\n\n"
                    answer += translate("AddonsInstaller",
                                        "Workbench successfully updated. Please "
                                        "restart FreeCAD to apply the changes.")
            else:
                self.info_label.emit("Checking module dependencies...")
                depsok, answer = self.check_dependencies(self.repos[idx][1])
                if depsok:
                    if sys.version_info.major > 2 and str(self.repos[idx][0]) in py2only:
                        FreeCAD.Console.PrintWarning(translate("AddonsInstaller",
                                                               "User requested installing a Python 2 "
                                                               "workbench on a system running Python 3 - ") +
                                                     str(self.repos[idx][0]) + "\n")
                    if have_git:
                        self.info_label.emit("Cloning module...")
                        repo = git.Repo.clone_from(self.repos[idx][1], clonedir)

                        # Make sure to clone all the submodules as well
                        if repo.submodules:
                            repo.submodule_update(recursive=True)
                    else:
                        self.info_label.emit("Downloading module...")
                        self.download(self.repos[idx][1], clonedir)
                    answer = translate("AddonsInstaller",
                                       "Workbench successfully installed. Please restart "
                                       "FreeCAD to apply the changes.")
            # symlink any macro contained in the module to the macros folder
            macro_dir = FreeCAD.getUserMacroDir(True)
            if not os.path.exists(macro_dir):
                os.makedirs(macro_dir)
            if os.path.exists(clonedir):
                for f in os.listdir(clonedir):
                    if f.lower().endswith(".fcmacro"):
                        print("copying macro:", f)
                        utils.symlink(os.path.join(clonedir, f), os.path.join(macro_dir, f))
                        FreeCAD.ParamGet('User parameter:Plugins/' +
                                         self.repos[idx][0]).SetString("destination", clonedir)
                        answer += "\n\n" + translate("AddonsInstaller",
                                                     "A macro has been installed and is available "
                                                     "under Macro -> Macros menu")
                        answer += ":\n<b>" + f + "</b>"
            self.progressbar_show.emit(False)
            self.info_label.emit(answer)
            self.mark_recompute.emit(self.repos[idx][0])
        self.stop = True

    def check_dependencies(self, baseurl):
        "checks if the repo contains a metadata.txt and check its contents"

        ok = True
        message = ""
        depsurl = baseurl.replace("github.com", "raw.githubusercontent.com")
        if not depsurl.endswith("/"):
            depsurl += "/"
        depsurl += "master/metadata.txt"
        mu = utils.urlopen(depsurl)
        if mu:
            # metadata.txt found
            depsfile = mu.read()
            mu.close()

            # urllib2 gives us a bytelike object instead of a string. Have to consider that
            try:
                depsfile = depsfile.decode("utf-8")
            except AttributeError:
                pass

            deps = depsfile.split("\n")
            for line in deps:
                if line.startswith("workbenches="):
                    depswb = line.split("=")[1].split(",")
                    for wb in depswb:
                        if wb.strip():
                            if not wb.strip() in FreeCADGui.listWorkbenches().keys():
                                if not wb.strip()+"Workbench" in FreeCADGui.listWorkbenches().keys():
                                    ok = False
                                    message += translate("AddonsInstaller", "Missing workbench") + ": " + wb + ", "
                elif line.startswith("pylibs="):
                    depspy = line.split("=")[1].split(",")
                    for pl in depspy:
                        if pl.strip():
                            try:
                                __import__(pl.strip())
                            except ImportError:
                                ok = False
                                message += translate("AddonsInstaller", "Missing python module") + ": " + pl + ", "
                elif line.startswith("optionalpylibs="):
                    opspy = line.split("=")[1].split(",")
                    for pl in opspy:
                        if pl.strip():
                            try:
                                __import__(pl.strip())
                            except ImportError:
                                message += translate("AddonsInstaller",
                                                     "Missing optional python module (doesn't prevent installing)")
                                message += ": " + pl + ", "
        if message and (not ok):
            message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench")
            message += ": <b>" + message + "</b>. "
            message += translate("AddonsInstaller", "Please install the missing components first.")
        return ok, message

    def download(self, baseurl, clonedir):
        "downloads and unzip a zip version from a git repo"

        bakdir = None
        if os.path.exists(clonedir):
            bakdir = clonedir+".bak"
            if os.path.exists(bakdir):
                shutil.rmtree(bakdir)
            os.rename(clonedir, bakdir)
        os.makedirs(clonedir)
        if baseurl in url_to_branch_map:
            branch = url_to_branch_map[baseurl]
        else:
            branch = "master"
        zipurl = utils.get_zip_url(baseurl, branch)
        if not zipurl:
            return translate("AddonsInstaller", "Error: Unable to locate zip from") + " " + baseurl
        try:
            print("Downloading "+zipurl)
            u = utils.urlopen(zipurl)
        except Exception:
            return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
        if not u:
            return translate("AddonsInstaller", "Error: Unable to download") + " " + zipurl
        zfile = _stringio()
        zfile.write(u.read())
        zfile = zipfile.ZipFile(zfile)
        master = zfile.namelist()[0]  # github will put everything in a subfolder
        zfile.extractall(clonedir)
        u.close()
        zfile.close()
        for filename in os.listdir(clonedir+os.sep+master):
            shutil.move(clonedir+os.sep+master+os.sep+filename, clonedir+os.sep+filename)
        os.rmdir(clonedir+os.sep+master)
        if bakdir:
            shutil.rmtree(bakdir)
        return translate("AddonsInstaller", "Successfully installed") + " " + zipurl


class CheckSingleWorker(QtCore.QThread):
    """Worker to check for updates for a single addon"""

    updateAvailable = QtCore.Signal(bool)

    def __init__(self, name):

        QtCore.QThread.__init__(self)
        self.name = name

    def run(self):

        if not have_git:
            return
        FreeCAD.Console.PrintLog("Checking for available updates of the "+self.name+" addon\n")
        addondir = os.path.join(FreeCAD.getUserAppDataDir(), "Mod", self.name)
        if os.path.exists(addondir):
            if os.path.exists(addondir + os.sep + ".git"):
                gitrepo = git.Git(addondir)
                try:
                    gitrepo.fetch()
                    if "git pull" in gitrepo.status():
                        self.updateAvailable.emit(True)
                        return
                except Exception:
                    # can fail for any number of reasons, ex. not being online
                    pass
        self.updateAvailable.emit(False)

#  @}
