#!/usr/bin/env python
# -*- coding: utf-8 -*-

#***************************************************************************
#*                                                                         *
#*   Copyright (c) 2015 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                                                                   *
#*                                                                         *
#***************************************************************************

from __future__ import print_function

__title__="FreeCAD Addon Manager Module"
__author__ = "Yorik van Havre","Jonathan Wiedemann","Kurt Kremitzki"
__url__ = "http://www.freecadweb.org"

'''
FreeCAD Addon Manager Module

It will fetch its contents from https://github.com/FreeCAD/FreeCAD-addons
You need a working internet connection, and the GitPython package
installed.
'''
import os
import re
import shutil
import stat
import sys
import tempfile

from PySide import QtCore, QtGui
import FreeCAD
import FreeCADGui
if sys.version_info.major < 3:
    import urllib2
else:
    import urllib.request as urllib2

from addonmanager_macro import Macro
from addonmanager_utilities import translate
from addonmanager_utilities import urlopen

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

MACROS_BLACKLIST = ["BOLTS","WorkFeatures","how to install","PartsLibrary","FCGear"]
OBSOLETE = ["assembly2"]

if sys.version_info.major < 3:
    import StringIO as io
    _stringio = io.StringIO
else:
    import io
    _stringio = io.BytesIO


def symlink(source, link_name):
    if os.path.exists(link_name):
        print("macro already exists")
    else:
        os_symlink = getattr(os, "symlink", None)
        if callable(os_symlink):
            os_symlink(source, link_name)
        else:
            import ctypes
            csl = ctypes.windll.kernel32.CreateSymbolicLinkW
            csl.argtypes = (ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint32)
            csl.restype = ctypes.c_ubyte
            flags = 1 if os.path.isdir(source) else 0
            # set the SYMBOLIC_LINK_FLAG_ALLOW_UNPRIVILEGED_CREATE flag
            # (see https://blogs.windows.com/buildingapps/2016/12/02/symlinks-windows-10/#joC5tFKhdXs2gGml.97)
            flags += 2

            if csl(link_name, source, flags) == 0:
                raise ctypes.WinError()


def update_macro_details(old_macro, new_macro):
    """Update a macro with information from another one

    Update a macro with information from another one, supposedly the same but
    from a different source. The first source is supposed to be git, the second
    one the wiki.
    """
    if old_macro.on_git and new_macro.on_git:
        FreeCAD.Console.PrintWarning('The macro "{}" is present twice in github, please report'.format(old_macro.name))
    # We don't report macros present twice on the wiki because a link to a
    # macro is considered as a macro. For example, 'Perpendicular To Wire'
    # appears twice, as of 2018-05-05).
    old_macro.on_wiki = new_macro.on_wiki
    for attr in ['desc', 'url', 'code']:
        if not hasattr(old_macro, attr):
            setattr(old_macro, attr, getattr(new_macro, attr))


def install_macro(macro, macro_repo_dir):
    """Install a macro and all its related files

    Returns True if the macro was installed correctly.

    Parameters
    ----------
    - macro: a addonmanager_macro.Macro instance
    """
    if not macro.code:
        return False
    macro_dir = FreeCAD.getUserMacroDir(True)
    if not os.path.isdir(macro_dir):
        try:
            os.makedirs(macro_dir)
        except OSError:
            return False
    macro_path = os.path.join(macro_dir, macro.filename)
    if sys.version_info.major < 3:
        # In python2 the code is a bytes object.
        mode = 'wb'
    else:
        mode = 'w'
    try:
        with open(macro_path, mode) as macrofile:
            macrofile.write(macro.code)
    except IOError:
        return False
    # Copy related files, which are supposed to be given relative to
    # macro.src_filename.
    base_dir = os.path.dirname(macro.src_filename)
    for other_file in macro.other_files:
        dst_dir = os.path.join(macro_dir, os.path.dirname(other_file))
        if not os.path.isdir(dst_dir):
            try:
                os.makedirs(dst_dir)
            except OSError:
                return False
        src_file = os.path.join(base_dir, other_file)
        dst_file = os.path.join(macro_dir, other_file)
        try:
            shutil.copy(src_file, dst_file)
        except IOError:
            return False
    return True


def remove_macro(macro):
    """Remove a macro and all its related files

    Returns True if the macro was removed correctly.

    Parameters
    ----------
    - macro: a addonmanager_macro.Macro instance
    """
    if not macro.is_installed():
        # Macro not installed, nothing to do.
        return True
    macro_dir = FreeCAD.getUserMacroDir(True)
    macro_path = os.path.join(macro_dir, macro.filename)
    macro_path_with_macro_prefix = os.path.join(macro_dir, 'Macro_' + macro.filename)
    if os.path.exists(macro_path):
        os.remove(macro_path)
    elif os.path.exists(macro_path_with_macro_prefix):
        os.remove(macro_path_with_macro_prefix)
    # Remove related files, which are supposed to be given relative to
    # macro.src_filename.
    for other_file in macro.other_files:
        dst_file = os.path.join(macro_dir, other_file)
        remove_directory_if_empty(os.path.dirname(dst_file))
        os.remove(dst_file)
    return True


def remove_directory_if_empty(dir):
    """Remove the directory if it is empty

    Directory FreeCAD.getUserMacroDir(True) will not be removed even if empty.
    """
    if dir == FreeCAD.getUserMacroDir(True):
        return
    if not os.listdir(dir):
        os.rmdir(dir)


class AddonsInstaller(QtGui.QDialog):

    def __init__(self):
        QtGui.QDialog.__init__(self)
        self.repos = []
        self.macros = []
        self.macro_repo_dir = tempfile.mkdtemp()

        self.setObjectName("AddonsInstaller")
        self.resize(626, 404)
        self.verticalLayout = QtGui.QVBoxLayout(self)
        self.tabWidget = QtGui.QTabWidget()
        self.verticalLayout.addWidget(self.tabWidget)
        self.listWorkbenches = QtGui.QListWidget()
        self.listWorkbenches.setIconSize(QtCore.QSize(16,16))
        self.tabWidget.addTab(self.listWorkbenches,"")
        self.listMacros = QtGui.QListWidget()
        self.listMacros.setSortingEnabled(False)
        self.listMacros.setIconSize(QtCore.QSize(16,16))
        self.tabWidget.addTab(self.listMacros,"")
        self.labelDescription = QtGui.QLabel()
        self.labelDescription.setMinimumSize(QtCore.QSize(0, 75))
        self.labelDescription.setAlignment(QtCore.Qt.AlignLeading|QtCore.Qt.AlignLeft|QtCore.Qt.AlignTop)
        self.labelDescription.setWordWrap(True)
        self.verticalLayout.addWidget(self.labelDescription)
        self.doUpdate = []

        self.progressBar = QtGui.QProgressBar(self)
        #self.progressBar.setProperty("value", 24)
        self.progressBar.setObjectName("progressBar")
        #self.progressBar.hide()
        self.progressBar.setRange(0,0)
        self.verticalLayout.addWidget(self.progressBar)

        self.horizontalLayout = QtGui.QHBoxLayout()
        spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum)
        self.horizontalLayout.addItem(spacerItem)

        self.buttonExecute = QtGui.QPushButton()
        icon = QtGui.QIcon.fromTheme("execute")
        self.buttonExecute.setIcon(icon)
        self.buttonExecute.setEnabled(False)
        self.horizontalLayout.addWidget(self.buttonExecute)

        self.buttonCheck = QtGui.QPushButton()
        icon = QtGui.QIcon.fromTheme("reload",QtGui.QIcon(":/icons/view-refresh.svg"))
        self.buttonCheck.setIcon(icon)
        self.horizontalLayout.addWidget(self.buttonCheck)
        self.buttonCheck.hide()
        self.buttonInstall = QtGui.QPushButton()
        icon = QtGui.QIcon.fromTheme("download")
        self.buttonInstall.setIcon(icon)
        self.horizontalLayout.addWidget(self.buttonInstall)
        self.buttonRemove = QtGui.QPushButton()
        icon = QtGui.QIcon.fromTheme("edit-delete")
        self.buttonRemove.setIcon(icon)
        self.horizontalLayout.addWidget(self.buttonRemove)
        self.buttonCancel = QtGui.QPushButton()
        icon = QtGui.QIcon.fromTheme("cancel")
        self.buttonCancel.setIcon(icon)
        self.buttonCancel.setDefault(True)
        self.horizontalLayout.addWidget(self.buttonCancel)
        self.verticalLayout.addLayout(self.horizontalLayout)

        self.retranslateUi()

        QtCore.QObject.connect(self.buttonExecute, QtCore.SIGNAL("clicked()"), self.executemacro)
        QtCore.QObject.connect(self.buttonCancel, QtCore.SIGNAL("clicked()"), self.reject)
        QtCore.QObject.connect(self.buttonInstall, QtCore.SIGNAL("clicked()"), self.install)
        QtCore.QObject.connect(self.buttonRemove, QtCore.SIGNAL("clicked()"), self.remove)
        QtCore.QObject.connect(self.labelDescription, QtCore.SIGNAL("linkActivated(QString)"), self.showlink)
        QtCore.QObject.connect(self.listWorkbenches, QtCore.SIGNAL("currentRowChanged(int)"), self.show)
        QtCore.QObject.connect(self.tabWidget, QtCore.SIGNAL("currentChanged(int)"), self.switchtab)
        QtCore.QObject.connect(self.listMacros, QtCore.SIGNAL("currentRowChanged(int)"), self.show_macro)
        QtCore.QObject.connect(self.buttonCheck, QtCore.SIGNAL("clicked()"), self.check_updates)
        QtCore.QMetaObject.connectSlotsByName(self)

        self.update()

        if not NOGIT:
            try:
                import git
            except:
                self.buttonCheck.hide()
            else:
                self.buttonCheck.show()

        # center the dialog over the FreeCAD window
        mw = FreeCADGui.getMainWindow()
        self.move(mw.frameGeometry().topLeft() + mw.rect().center() - self.rect().center())


    def reject(self):
        # ensure all threads are finished before closing
        oktoclose = True
        for worker in ["update_worker","check_worker","show_worker","showmacro_worker",
                       "macro_worker","install_worker"]:
            if hasattr(self,worker):
                thread = getattr(self,worker)
                if thread:
                    if not thread.isFinished():
                        oktoclose = False
        if oktoclose:
            if hasattr(self,"install_worker"):
                QtGui.QMessageBox.information(self, translate("AddonsInstaller","Addon manager"), translate("AddonsInstaller","Please restart FreeCAD for changes to take effect."))
            shutil.rmtree(self.macro_repo_dir,onerror=self.remove_readonly)
            QtGui.QDialog.reject(self)

    def retranslateUi(self):
        self.setWindowTitle(translate("AddonsInstaller","Addon manager"))
        self.labelDescription.setText(translate("AddonsInstaller", "Downloading addon list..."))
        self.buttonExecute.setText(translate("AddonsInstaller", "Execute"))
        self.buttonExecute.setToolTip(translate("AddonsInstaller", "This button runs the selected macro (which must be installed first)"))
        self.buttonCheck.setToolTip(translate("AddonsInstaller", "Check for available updates"))
        self.buttonCancel.setText(translate("AddonsInstaller", "Close"))
        self.buttonInstall.setText(translate("AddonsInstaller", "Install / update"))
        self.buttonRemove.setText(translate("AddonsInstaller", "Remove"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.listWorkbenches), translate("AddonsInstaller", "Workbenches"))
        self.tabWidget.setTabText(self.tabWidget.indexOf(self.listMacros), translate("AddonsInstaller", "Macros"))

    def update(self):
        self.listWorkbenches.clear()
        self.buttonExecute.setEnabled(False)
        self.repos = []
        self.info_worker = InfoWorker()
        self.info_worker.addon_repos.connect(self.update_repos)
        self.update_worker = UpdateWorker()
        self.update_worker.info_label.connect(self.set_information_label)
        self.update_worker.addon_repo.connect(self.add_addon_repo)
        self.update_worker.progressbar_show.connect(self.show_progress_bar)
        self.update_worker.start()

    def check_updates(self):
        if self.tabWidget.currentIndex() == 0:
            if not self.doUpdate:
                self.check_worker = CheckWBWorker(self.repos)
                self.check_worker.mark.connect(self.mark)
                self.check_worker.info_label.connect(self.set_information_label)
                self.check_worker.progressbar_show.connect(self.show_progress_bar)
                self.check_worker.start()
            else:
                self.install(self.doUpdate)

    def add_addon_repo(self, addon_repo):
        self.repos.append(addon_repo)
        if addon_repo[2] == 1 :
            self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(addon_repo[0]) + str(" (Installed)")))
        else:
            self.listWorkbenches.addItem("        "+str(addon_repo[0]))

    def set_information_label(self, label):
        self.labelDescription.setText(label)
        if self.listWorkbenches.isVisible():
            self.listWorkbenches.setFocus()
        else:
            self.listMacros.setFocus()

    def show(self,idx):
        if self.repos and idx >= 0:
            self.show_worker = ShowWorker(self.repos, idx)
            self.show_worker.info_label.connect(self.set_information_label)
            self.show_worker.addon_repos.connect(self.update_repos)
            self.show_worker.progressbar_show.connect(self.show_progress_bar)
            self.show_worker.start()

    def show_macro(self,idx):
        if self.macros and idx >= 0:
            self.showmacro_worker = GetMacroDetailsWorker(self.macros[idx])
            self.showmacro_worker.info_label.connect(self.set_information_label)
            self.showmacro_worker.progressbar_show.connect(self.show_progress_bar)
            self.showmacro_worker.start()

    def switchtab(self,idx):
        if idx == 1:
            if not self.macros:
                self.listMacros.clear()
                self.macros = []
                self.macro_worker = FillMacroListWorker(self.macro_repo_dir)
                self.macro_worker.add_macro_signal.connect(self.add_macro)
                self.macro_worker.info_label_signal.connect(self.set_information_label)
                self.macro_worker.progressbar_show.connect(self.show_progress_bar)
                self.macro_worker.start()
            self.buttonCheck.setEnabled(False)
        else:
            self.buttonCheck.setEnabled(True)

    def update_repos(self, repos):
        self.repos = repos

    def add_macro(self, macro):
        if macro in self.macros:
            # The macro is already in the list of macros.
            old_macro = self.macros[self.macros.index(macro)]
            update_macro_details(old_macro, macro)
        else:
            self.macros.append(macro)
            if macro.is_installed():
                self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme('dialog-ok'), macro.name + str(' (Installed)')))
            else:
                self.listMacros.addItem("        "+macro.name)

    def showlink(self,link):
        """opens a link with the system browser"""
        #print("clicked: ",link)
        QtGui.QDesktopServices.openUrl(QtCore.QUrl(link, QtCore.QUrl.TolerantMode))

    def install(self,repos=None):
        if self.tabWidget.currentIndex() == 0:
            # Tab "Workbenches".
            idx = None
            if repos:
                idx = []
                for repo in repos:
                    for i,r in enumerate(self.repos):
                        if r[0] == repo:
                            idx.append(i)
            else:
                idx = self.listWorkbenches.currentRow()
            if idx != None:
                if hasattr(self,"install_worker"):
                    if self.install_worker.isRunning():
                        return
                self.install_worker = InstallWorker(self.repos, idx)
                self.install_worker.info_label.connect(self.set_information_label)
                self.install_worker.progressbar_show.connect(self.show_progress_bar)
                self.install_worker.start()
        elif self.tabWidget.currentIndex() == 1:
            # Tab "Macros".
            macro = self.macros[self.listMacros.currentRow()]
            if install_macro(macro, self.macro_repo_dir):
                self.labelDescription.setText(translate("AddonsInstaller", "Macro successfully installed. The macro is now available from the Macros dialog."))
            else:
                self.labelDescription.setText(translate("AddonsInstaller", "Unable to install"))
        self.update_status(soft=True)

    def show_progress_bar(self, state):
        if state == True:
            self.listWorkbenches.setEnabled(False)
            self.listMacros.setEnabled(False)
            self.buttonExecute.setEnabled(False)
            self.buttonInstall.setEnabled(False)
            self.buttonRemove.setEnabled(False)
            self.buttonCheck.setEnabled(False)
            self.progressBar.show()
        else:
            self.progressBar.hide()
            self.listWorkbenches.setEnabled(True)
            self.listMacros.setEnabled(True)
            self.buttonExecute.setEnabled(False)
            self.buttonInstall.setEnabled(True)
            self.buttonRemove.setEnabled(True)
            if self.tabWidget.currentIndex() == 0:
                self.buttonCheck.setEnabled(True)
            if self.listWorkbenches.isVisible():
                self.listWorkbenches.setFocus()
            else:
                self.listMacros.setFocus()
                self.buttonExecute.setEnabled(True)

    def executemacro(self):
        if self.tabWidget.currentIndex() == 1:
            # Tab "Macros".
            macro = self.macros[self.listMacros.currentRow()]
            if not macro.is_installed():
                # Macro not installed, nothing to do.
                return
            macro_path = os.path.join(FreeCAD.getUserMacroDir(True), macro.filename)
            if os.path.exists(macro_path):
                macro_path = macro_path.replace("\\","/")

                FreeCADGui.open(str(macro_path))
                self.hide()
                FreeCADGui.SendMsgToActiveView("Run")
        else:
            self.buttonExecute.setEnabled(False)

    def remove_readonly(self, func, path, _):
        "Remove read only file."
        os.chmod(path, stat.S_IWRITE)
        func(path)

    def remove(self):
        if self.tabWidget.currentIndex() == 0:
            # Tab "Workbenches".
            idx = self.listWorkbenches.currentRow()
            basedir = FreeCAD.getUserAppDataDir()
            moddir = basedir + os.sep + "Mod"
            clonedir = moddir + os.sep + self.repos[idx][0]
            if os.path.exists(clonedir):
                shutil.rmtree(clonedir, onerror=self.remove_readonly)
                self.labelDescription.setText(translate("AddonsInstaller", "Addon successfully removed. Please restart FreeCAD"))
            else:
                self.labelDescription.setText(translate("AddonsInstaller", "Unable to remove this addon"))
        elif self.tabWidget.currentIndex() == 1:
            # Tab "Macros".
            macro = self.macros[self.listMacros.currentRow()]
            if remove_macro(macro):
                self.labelDescription.setText(translate('AddonsInstaller', 'Macro successfully removed.'))
            else:
                self.labelDescription.setText(translate('AddonsInstaller', 'Macro could not be removed.'))
        self.update_status(soft=True)

    def update_status(self,soft=False):

        "Updates the list of wbs/macros. If soft is true, items are not recreated (and therefore display text no triggered)"

        moddir = FreeCAD.getUserAppDataDir() + os.sep + "Mod"
        if soft:
            for i in range(self.listWorkbenches.count()):
                txt = self.listWorkbenches.item(i).text().strip()
                if txt.endswith(" (Installed)"):
                    txt = txt[:-12]
                elif txt.endswith(" (Update available)"):
                    txt = txt[:-19]
                if os.path.exists(os.path.join(moddir,txt)):
                    self.listWorkbenches.item(i).setText(txt+" (Installed)")
                    self.listWorkbenches.item(i).setIcon(QtGui.QIcon.fromTheme("dialog-ok"))
                else:
                    self.listWorkbenches.item(i).setText("        "+txt)
                    self.listWorkbenches.item(i).setIcon(QtGui.QIcon())
            for i in range(self.listMacros.count()):
                txt = self.listMacros.item(i).text().strip()
                if txt.endswith(" (Installed)"):
                    txt = txt[:-12]
                elif txt.endswith(" (Update available)"):
                    txt = txt[:-19]
                if os.path.exists(os.path.join(moddir,txt)):
                    self.listMacros.item(i).setText(txt+" (Installed)")
                    self.listMacros.item(i).setIcon(QtGui.QIcon.fromTheme("dialog-ok"))
                else:
                    self.listMacros.item(i).setText("        "+txt)
                    self.listMacros.item(i).setIcon(QtGui.QIcon())
        else:
            self.listWorkbenches.clear()
            self.listMacros.clear()
            for wb in self.repos:
                if os.path.exists(os.path.join(moddir,wb[0])):
                    self.listWorkbenches.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme("dialog-ok"),str(wb[0]) + str(" (Installed)")))
                    wb[2] = 1
                else:
                    self.listWorkbenches.addItem("        "+str(wb[0]))
                    wb[2] = 0
            for macro in self.macros:
                if macro.is_installed():
                    self.listMacros.addItem(QtGui.QListWidgetItem(QtGui.QIcon.fromTheme('dialog-ok'), macro.name + str(' (Installed)')))
                else:
                    self.listMacros.addItem("        "+macro.name)

    def mark(self,repo):
        for i in range(self.listWorkbenches.count()):
            w = self.listWorkbenches.item(i)
            if w.text().startswith(str(repo)):
                w.setText(str(repo) + str(" (Update available)"))
                w.setIcon(QtGui.QIcon.fromTheme("reload"))
                if not repo in self.doUpdate:
                    self.doUpdate.append(repo)

class UpdateWorker(QtCore.QThread):

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

    def __init__(self):
        QtCore.QThread.__init__(self)

    def run(self):
        "populates the list of addons"
        self.progressbar_show.emit(True)
        u = urlopen("https://github.com/FreeCAD/FreeCAD-addons")
        p = u.read()
        if sys.version_info.major >= 3 and isinstance(p, bytes):
            p = p.decode("utf-8")
        u.close()
        p = p.replace("\n"," ")
        p = re.findall("octicon-file-submodule(.*?)message",p)
        basedir = FreeCAD.getUserAppDataDir()
        moddir = basedir + os.sep + "Mod"
        repos = []
        for l in p:
            #name = re.findall("data-skip-pjax=\"true\">(.*?)<",l)[0]
            name = re.findall("title=\"(.*?) @",l)[0]
            self.info_label.emit(name)
            #url = re.findall("title=\"(.*?) @",l)[0]
            try:
                url = "https://github.com/" + re.findall("href=\"\/(.*?)\/tree",l)[0]
            except:
                pass
            else:
                addondir = moddir + os.sep + name
                #print ("found:",name," at ",url)
                if not os.path.exists(addondir):
                    state = 0
                else:
                    state = 1
                repos.append([name,url,state])
        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.stop = True


class InfoWorker(QtCore.QThread):
    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 = urlopen(url)
            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):
    info_label = QtCore.Signal(str)
    mark = QtCore.Signal(str)
    progressbar_show = QtCore.Signal(bool)

    def __init__(self,repos):
        QtCore.QThread.__init__(self)
        self.repos = repos

    def run(self):
        if NOGIT:
            self.stop = True
            return
        try:
            import git
        except:
            self.stop = True
            return
        self.progressbar_show.emit(True)
        basedir = FreeCAD.getUserAppDataDir()
        moddir = basedir + os.sep + "Mod"
        self.info_label.emit(translate("AddonsInstaller", "Checking for new versions..."))
        upds = []
        gitpython_warning = False
        for repo in self.repos:
            if repo[2] == 1: #installed
                self.info_label.emit(translate("AddonsInstaller","Checking repo")+" "+repo[0]+"...")
                clonedir = moddir + 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:
                            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)
                    gitrepo.fetch()
                    if "git pull" in gitrepo.status():
                        self.mark.emit(repo[0])
                        upds.append(repo[0])
        self.progressbar_show.emit(False)
        if upds:
            self.info_label.emit(str(len(upds))+" "+translate("AddonsInstaller", "update(s) available")+": "+",".join(upds)+". "+translate("AddonsInstaller","Press the update button again to update them all at once."))
        else:
            self.info_label.emit(translate("AddonsInstaller","Everything is up to date"))
        self.stop = True


class FillMacroListWorker(QtCore.QThread):
    """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
        """
        try:
            import git
        except ImportError:
            self.info_label_signal.emit("GitPython not installed! Cannot retrieve macros from git")
            FreeCAD.Console.PrintWarning(translate('AddonInstaller', 'GitPython not installed! Cannot retrieve macros from git')+"\n")
            return

        self.info_label_signal.emit('Downloading list of macros for git...')
        git.Repo.clone_from('https://github.com/FreeCAD/FreeCAD-macros.git', self.repo_dir)
        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...")
        self.progressbar_show.emit(True)
        u = urlopen("https://www.freecadweb.org/wiki/Macros_recipes")
        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):

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

    def __init__(self, repos, idx):
        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:
            url = self.repos[self.idx][1]
            self.info_label.emit(translate("AddonsInstaller", "Retrieving info from") + ' ' + str(url))
            u = urlopen(url)
            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]
                if self.repos[self.idx][0] in OBSOLETE:
                    desc += " <b>This add-on is marked as obsolete</b> - This usually means it is no longer maintained, and some more advanced add-on in this list provides the same functionality."
            else:
                desc = "Unable to retrieve addon description"
            self.repos[self.idx].append(desc)
            self.addon_repos.emit(self.repos)
        if self.repos[self.idx][2] == 1:
            upd = False
            # checking for updates
            if not NOGIT:
                try:
                    import git
                except:
                    pass
                else:
                    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 upd:
                message = "<strong>" + translate("AddonsInstaller", "An update is available for this addon.") + "</strong><br>" + desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>'
            else:
                message = "<strong>" + translate("AddonsInstaller", "This addon is already installed.") + "</strong><br>" + desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>'
        else:
            message = desc + ' - <a href="' + self.repos[self.idx][1] + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">' + self.repos[self.idx][1] + '</span></a>'
        self.info_label.emit( message )
        self.progressbar_show.emit(False)
        self.stop = True


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>'
                    + translate("AddonsInstaller", "This addon is already installed.")
                    + '</strong><br>')
        else:
            already_installed_msg = ''
        message = (already_installed_msg
                + self.macro.desc
                + ' - <a href="'
                + self.macro.url
                + '"><span style="word-wrap: break-word;width:15em;text-decoration: underline; color:#0000ff;">'
                + self.macro.url
                + '</span></a>')
        self.info_label.emit(message)
        self.progressbar_show.emit(False)
        self.stop = True


class InstallWorker(QtCore.QThread):

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

    def __init__(self, repos, idx):
        QtCore.QThread.__init__(self)
        self.idx = idx
        self.repos = repos

    def run(self):
        "installs or updates the selected addon"
        git = None
        try:
            import git
        except Exception as e:
            self.info_label.emit("GitPython not found.")
            print(e)
            FreeCAD.Console.PrintWarning(translate("AddonsInstaller","GitPython not found. Using standard download instead.")+"\n")
            try:
                import zipfile
            except:
                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
            try:
                import StringIO as io
            except ImportError: # StringIO is not available with python3
                import io
        if not isinstance(self.idx,list):
            self.idx = [self.idx]
        for idx in self.idx:
            if idx < 0:
                return
            if not self.repos:
                return
            if NOGIT:
                git = None
            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 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)
                    answer = repo.pull()

                    # 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)
            else:
                self.info_label.emit("Checking module dependencies...")
                depsok,answer = self.checkDependencies(self.repos[idx][1])
                if depsok:
                    if git:
                        self.info_label.emit("Cloning module...")
                        repo = git.Repo.clone_from(self.repos[idx][1], clonedir, branch='master')

                        # 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)
                        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 += translate("AddonsInstaller", "A macro has been installed and is available the Macros menu") + ": <b>"
                        answer += f + "</b>"
            self.progressbar_show.emit(False)
            self.info_label.emit(answer)
        self.stop = True

    def checkDependencies(self,baseurl):
        "checks if the repo contains a metadata.txt and check its contents"
        import FreeCADGui
        ok = True
        message = ""
        depsurl = baseurl.replace("github.com","raw.githubusercontent.com")
        if not depsurl.endswith("/"):
            depsurl += "/"
        depsurl += "master/metadata.txt"
        try:
            mu = urlopen(depsurl)
        except urllib2.HTTPError:
            # no metadata.txt, we just continue without deps checking
            pass
        else:
            # 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 l in deps:
                if l.startswith("workbenches="):
                    depswb = l.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 l.startswith("pylibs="):
                    depspy = l.split("=")[1].split(",")
                    for pl in depspy:
                        if pl.strip():
                            try:
                                __import__(pl.strip())
                            except:
                                ok = False
                                message += translate("AddonsInstaller","Missing python module") +": " + pl + ", "
                elif l.startswith("optionalpylibs="):
                    opspy = l.split("=")[1].split(",")
                    for pl in opspy:
                        if pl.strip():
                            try:
                                __import__(pl.strip())
                            except:
                                message += translate("AddonsInstaller","Missing optional python module (doesn't prevent installing)") +": " + pl + ", "
        if message and (not ok):
            message = translate("AddonsInstaller", "Some errors were found that prevent to install this workbench") + ": <b>" + message + "</b>. "
            message += translate("AddonsInstaller","Please install the missing components first.")
        return ok, message

    def download(self,giturl,clonedir):
        "downloads and unzip from github"
        import zipfile
        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)
        zipurl = giturl+"/archive/master.zip"
        try:
            print("Downloading "+zipurl)
            u = urlopen(zipurl)
        except:
            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


def launchAddonMgr():
    # first use dialog
    readWarning = FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').GetBool('readWarning',False)
    if not readWarning:
        if QtGui.QMessageBox.warning(None,"FreeCAD",translate("AddonsInstaller", "The addons that can be installed here are not officially part of FreeCAD, and are not reviewed by the FreeCAD team. Make sure you know what you are installing!"), QtGui.QMessageBox.Cancel | QtGui.QMessageBox.Ok) != QtGui.QMessageBox.StandardButton.Cancel:
            FreeCAD.ParamGet('User parameter:Plugins/addonsRepository').SetBool('readWarning',True)
            readWarning = True

    if readWarning:
        dialog = AddonsInstaller()
        dialog.exec_()
