#!/usr/bin/python3

import os
import sys
import subprocess
import sqlite3
import io
import atexit
import wget
import shutil
import sisyphus
from collections import OrderedDict
from PyQt5 import QtCore, QtGui, QtWidgets, uic


class Sisyphus(QtWidgets.QMainWindow):
    def __init__(self):
        super(Sisyphus, self).__init__()
        uic.loadUi('/usr/share/sisyphus/ui/sisyphus.ui', self)
        self.centerOnScreen()
        self.show()

        self.filterApplications = OrderedDict([
            ('Search by Name', 'pn'),
            ('Search by Category', 'cat'),
            ('Search by Description', 'descr')
        ])
        self.applicationFilter.addItems(self.filterApplications.keys())
        self.applicationFilter.setCurrentText('Search by Name')
        self.applicationFilter.currentIndexChanged.connect(self.setApplicationFilter)
        Sisyphus.applicationView = self.filterApplications['Search by Name']

        self.filterDatabases = OrderedDict([
            ('All Packages', 'all'),
            ('Installed Packages', 'installed'),
            ('Available Packages', 'installable'),
            ('Upgradable Packages', 'upgradable')
        ])
        self.databaseFilter.addItems(self.filterDatabases.keys())
        self.databaseFilter.setCurrentText('All Packages')
        self.databaseFilter.currentIndexChanged.connect(self.setDatabaseFilter)
        Sisyphus.databaseView = self.filterDatabases['All Packages']

        Sisyphus.searchTerm = "'%%'"

        self.databaseTable.clicked.connect(self.rowClicked)

        self.inputBox.textEdited.connect(self.searchDatabase)

        self.settingsButton.clicked.connect(self.showMirrorWindow)
        self.licenseButton.clicked.connect(self.showLicenseWindow)

        sys.stdout = MainWorker(workerOutput=self.updateStatusBox) # capture stdout

        self.updateWorker = MainWorker()
        self.updateThread = QtCore.QThread()
        self.updateWorker.moveToThread(self.updateThread)
        self.updateWorker.started.connect(self.showProgressBar)
        self.updateThread.started.connect(self.updateWorker.startUpdate)
        self.updateThread.finished.connect(self.jobDone)
        self.updateWorker.finished.connect(self.updateThread.quit)

        self.installButton.clicked.connect(self.packageInstall)
        self.installWorker = MainWorker()
        self.installThread = QtCore.QThread()
        self.installWorker.moveToThread(self.installThread)
        self.installWorker.started.connect(self.showProgressBar)
        self.installWorker.started.connect(self.clearProgressBox)
        self.installThread.started.connect(self.installWorker.startInstall)
        self.installWorker.workerOutput.connect(self.updateStatusBox)
        self.installThread.finished.connect(self.jobDone)
        self.installWorker.finished.connect(self.installThread.quit)

        self.uninstallButton.clicked.connect(self.packageUninstall)
        self.uninstallWorker = MainWorker()
        self.uninstallThread = QtCore.QThread()
        self.uninstallWorker.moveToThread(self.uninstallThread)
        self.uninstallWorker.started.connect(self.showProgressBar)
        self.uninstallWorker.started.connect(self.clearProgressBox)
        self.uninstallThread.started.connect(self.uninstallWorker.startUninstall)
        self.uninstallWorker.workerOutput.connect(self.updateStatusBox)
        self.uninstallThread.finished.connect(self.jobDone)
        self.uninstallWorker.finished.connect(self.uninstallThread.quit)

        self.upgradeButton.clicked.connect(self.systemUpgrade)
        self.upgradeWorker = MainWorker()
        self.upgradeThread = QtCore.QThread()
        self.upgradeWorker.moveToThread(self.upgradeThread)
        self.upgradeWorker.started.connect(self.showProgressBar)
        self.upgradeWorker.started.connect(self.clearProgressBox)
        self.upgradeThread.started.connect(self.upgradeWorker.startUpgrade)
        self.upgradeWorker.workerOutput.connect(self.updateStatusBox)
        self.upgradeThread.finished.connect(self.jobDone)
        self.upgradeWorker.finished.connect(self.upgradeThread.quit)

        self.orphansButton.clicked.connect(self.orphansRemove)
        self.orphansWorker = MainWorker()
        self.orphansThread = QtCore.QThread()
        self.orphansWorker.moveToThread(self.orphansThread)
        self.orphansWorker.started.connect(self.showProgressBar)
        self.orphansWorker.started.connect(self.clearProgressBox)
        self.orphansThread.started.connect(self.orphansWorker.cleanOrphans)
        self.orphansWorker.workerOutput.connect(self.updateStatusBox)
        self.orphansThread.finished.connect(self.jobDone)
        self.orphansWorker.finished.connect(self.orphansThread.quit)

        self.updateSystem()
        self.progressBar.hide()

        self.exitButton.clicked.connect(self.sisyphusExit)

    def centerOnScreen(self):
        resolution = QtWidgets.QDesktopWidget().screenGeometry()
        self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
                  (resolution.height() / 2) - (self.frameSize().height() / 2))

    def rowClicked(self):
        Sisyphus.pkgSelect = len(self.databaseTable.selectionModel().selectedRows())
        self.showPackageCount()

    def showPackageCount(self):
        self.statusBar().showMessage("Found: %d, Selected: %d packages" % (Sisyphus.pkgCount, Sisyphus.pkgSelect))

    def setApplicationFilter(self):
        Sisyphus.applicationView = self.filterApplications[self.applicationFilter.currentText()]
        self.loadDatabase()

    def setDatabaseFilter(self):
        Sisyphus.databaseView = self.filterDatabases[self.databaseFilter.currentText()]
        Sisyphus.SELECT = self.databaseFilter.currentText()
        self.loadDatabase()

    def loadDatabase(self):
        noVirtual = "AND cat NOT LIKE 'virtual'"
        self.SELECTS = OrderedDict([
            ('all', '''SELECT
                i.category AS cat,
                i.name as pn,
                i.version as iv,
                IFNULL(a.version, 'None') AS av,
                d.description AS descr
                FROM local_packages AS i LEFT OUTER JOIN remote_packages as a
                ON i.category = a.category
                AND i.name = a.name
                AND i.slot = a.slot
				LEFT JOIN remote_descriptions AS d ON i.name = d.name AND i.category = d.category
                WHERE %s LIKE %s %s
                UNION
                SELECT
                a.category AS cat,
                a.name as pn,
                IFNULL(i.version, 'None') AS iv,
                a.version as av,
                d.description AS descr
                FROM remote_packages AS a LEFT OUTER JOIN local_packages AS i
                ON a.category = i.category
                AND a.name = i.name
                AND a.slot = i.slot
				LEFT JOIN remote_descriptions AS d ON a.name = d.name AND a.category = d.category
                WHERE %s LIKE %s %s
            ''' % (Sisyphus.applicationView, Sisyphus.searchTerm, noVirtual, Sisyphus.applicationView, Sisyphus.searchTerm, noVirtual)),
            ('installed', '''SELECT
                i.category AS cat,
                i.name AS pn,
                i.version AS iv,
                a.version as av,
                d.description AS descr
                FROM local_packages AS i
                LEFT JOIN remote_packages AS a
                ON i.category = a.category
                AND i.name = a.name
                AND i.slot = a.slot
				LEFT JOIN remote_descriptions AS d ON i.name = d.name AND i.category = d.category
                WHERE %s LIKE %s %s
            ''' % (Sisyphus.applicationView, Sisyphus.searchTerm, noVirtual)),
            ('installable', '''SELECT
                a.category AS cat,
                a.name AS pn,
                i.version as iv,
                a.version AS av,
                d.description AS descr
                FROM remote_packages AS a
                LEFT JOIN local_packages AS i
                ON a.category = i.category
                AND a.name = i.name
                AND a.slot = i.slot
				LEFT JOIN remote_descriptions AS d ON a.name = d.name AND a.category = d.category
                WHERE %s LIKE %s %s
                AND iv IS NULL
            ''' % (Sisyphus.applicationView, Sisyphus.searchTerm, noVirtual)),
            ('upgradable', '''SELECT
                i.category AS cat,
                i.name AS pn,
                i.version as iv,
                a.version AS av,
                d.description AS descr
                FROM local_packages AS i
                INNER JOIN remote_packages AS a
                ON i.category = a.category
                AND i.name = a.name
                AND i.slot = a.slot
				LEFT JOIN remote_descriptions AS d ON i.name = d.name AND i.category = d.category
                WHERE %s LIKE %s %s
                AND iv <> av
            ''' % (Sisyphus.applicationView, Sisyphus.searchTerm, noVirtual)),
        ])
        with sqlite3.connect(sisyphus.filesystem.localDatabase) as db:
            cursor = db.cursor()
            cursor.execute('%s' % (self.SELECTS[Sisyphus.databaseView]))
            rows = cursor.fetchall()
            Sisyphus.pkgCount = len(rows)
            Sisyphus.pkgSelect = 0
            model = QtGui.QStandardItemModel(len(rows), 5)
            model.setHorizontalHeaderLabels(['Category', 'Name', 'Installed Version', 'Available Version', 'Description'])
            for row in rows:
                indx = rows.index(row)
                for column in range(0, 5):
                    item = QtGui.QStandardItem("%s" % (row[column]))
                    model.setItem(indx, column, item)
            self.databaseTable.setModel(model)
            self.showPackageCount()

    def searchDatabase(self):
        search = self.inputBox.text()
        Sisyphus.searchTerm = "'%" + search + "%'"
        self.loadDatabase()

    def updateSystem(self):
        self.loadDatabase()
        self.statusBar().showMessage("I am syncing myself, hope to finish soon ...")
        self.updateThread.start()

    def getSelectedPackages(self):
        def byRow(e):
            return e['row']

        pkg_categs = [{'row': pkg.row(), 'cat': pkg.data()} for pkg in self.databaseTable.selectionModel().selectedRows(0)]
        pkg_names = [{'row': pkg.row(), 'name': pkg.data()} for pkg in self.databaseTable.selectionModel().selectedRows(1)]
        pkg_categs = sorted(pkg_categs, key=byRow)
        pkg_names = sorted(pkg_names, key=byRow)
        selected_pkgs = [pkg_categs[i]['cat'] + '/' + pkg_names[i]['name'] for i in range(len(pkg_categs))]
        return(selected_pkgs)

    def packageInstall(self):
        if not self.databaseTable.selectionModel().hasSelection():
            self.statusBar().showMessage("No package selected, please pick at least one!")
        else:
            Sisyphus.pkgname = self.getSelectedPackages()
            self.statusBar().showMessage("I am installing requested package(s), please wait ...")
            self.installThread.start()

    def packageUninstall(self):
        if not self.databaseTable.selectionModel().hasSelection():
            self.statusBar().showMessage("No package selected, please pick at least one!")
        else:
            Sisyphus.pkgname = self.getSelectedPackages()
            self.statusBar().showMessage("I am removing requested package(s), please wait ...")
            self.uninstallThread.start()

    def systemUpgrade(self):
        self.statusBar().showMessage("I am upgrading the system, please be patient ...")
        self.upgradeThread.start()

    def orphansRemove(self):
        self.statusBar().showMessage("I am busy with some cleaning, please don't rush me ...")
        self.orphansThread.start()

    def jobDone(self):
        self.hideProgressBar()
        self.loadDatabase()

    def showProgressBar(self):
        self.hideButtons()
        self.progressBar.setRange(0, 0)
        self.progressBar.show()
        self.inputBox.setFocus()

    def hideProgressBar(self):
        self.progressBar.setRange(0, 1)
        self.progressBar.setValue(1)
        self.progressBar.hide()
        self.showButtons()
        self.inputBox.setFocus()

    def hideButtons(self):
        self.installButton.hide()
        self.uninstallButton.hide()
        self.orphansButton.hide()
        self.upgradeButton.hide()
        self.exitButton.hide()

    def showButtons(self):
        self.installButton.show()
        self.uninstallButton.show()
        self.orphansButton.show()
        self.upgradeButton.show()
        self.exitButton.show()

    def clearProgressBox(self):
        self.progressBox.clear()

    def updateStatusBox(self, workerMessage):
        self.progressBox.insertPlainText(workerMessage)
        self.progressBox.ensureCursorVisible()

    def showMirrorWindow(self):
        self.window = MirrorConfiguration()
        self.window.show()

    def showLicenseWindow(self):
        self.window = LicenseInformation()
        self.window.show()

    def sisyphusExit(self):
        self.close()

    def __del__(self):
        sys.stdout = sys.__stdout__ # restore stdout

# mirror configuration window
class MirrorConfiguration(QtWidgets.QMainWindow):
    def __init__(self):
        super(MirrorConfiguration, self).__init__()
        uic.loadUi('/usr/share/sisyphus/ui/mirrorcfg.ui', self)
        self.centerOnScreen()
        self.MIRRORLIST = sisyphus.mirror.getList()
        self.updateMirrorList()
        self.applyButton.pressed.connect(self.mirrorCfgApply)
        self.applyButton.released.connect(self.mirrorCfgExit)
        self.mirrorCombo.activated.connect(self.setMirrorList)

    def centerOnScreen(self):
        resolution = QtWidgets.QDesktopWidget().screenGeometry()
        self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
                  (resolution.height() / 2) - (self.frameSize().height() / 2))

    def updateMirrorList(self):
        model = QtGui.QStandardItemModel()
        for row in self.MIRRORLIST:
            indx = self.MIRRORLIST.index(row)
            item = QtGui.QStandardItem()
            item.setText(row['Url'])
            model.setItem(indx, item)
            if row['isActive']:
                self.ACTIVEMIRRORINDEX = indx
        self.mirrorCombo.setModel(model)
        self.mirrorCombo.setCurrentIndex(self.ACTIVEMIRRORINDEX)

    def setMirrorList(self):
        self.MIRRORLIST[self.ACTIVEMIRRORINDEX]['isActive'] = False
        self.ACTIVEMIRRORINDEX = self.mirrorCombo.currentIndex()
        self.MIRRORLIST[self.ACTIVEMIRRORINDEX]['isActive'] = True

    def mirrorCfgApply(self):
        sisyphus.mirror.writeList(self.MIRRORLIST)

    def mirrorCfgExit(self):
        self.close()


# license information window
class LicenseInformation(QtWidgets.QMainWindow):
    def __init__(self):
        super(LicenseInformation, self).__init__()
        uic.loadUi('/usr/share/sisyphus/ui/license.ui', self)
        self.centerOnScreen()

    def centerOnScreen(self):
        resolution = QtWidgets.QDesktopWidget().screenGeometry()
        self.move((resolution.width() / 2) - (self.frameSize().width() / 2),
                  (resolution.height() / 2) - (self.frameSize().height() / 2))


# worker/multithreading class
class MainWorker(QtCore.QObject):
    started = QtCore.pyqtSignal()
    finished = QtCore.pyqtSignal()
    workerOutput = QtCore.pyqtSignal(str)

    def write(self, text):
        self.workerOutput.emit(str(text))

    def flush(self):
        pass

    def fileno(self):
        return 0

    @QtCore.pyqtSlot()
    def startUpdate(self):
        self.started.emit()
        sisyphus.check.update()
        sisyphus.setjobs.start.__wrapped__() # undecorate
        sisyphus.update.start.__wrapped__() # undecorate
        self.finished.emit()

    @QtCore.pyqtSlot()
    def startInstall(self):
        self.started.emit()
        pkgname = Sisyphus.pkgname

        binhostURL = sisyphus.binhost.getURL()
        areBinaries,areSources,needsConfig = sisyphus.solvedeps.package.__wrapped__(pkgname) #undecorate

        os.chdir(sisyphus.filesystem.portageCacheDir)
        self.workerOutput.emit("\n" + "These are the binary packages that will be merged, in order:" + "\n\n" + str(areBinaries) + "\n\n" + "Total:" + " " + str(len(areBinaries)) + " " + "binary package(s)" + "\n\n")
        for index, binary in enumerate([package + '.tbz2' for package in areBinaries]):
            self.workerOutput.emit(">>> Fetching" + " " + binhostURL + binary)
            wget.download(binhostURL + binary)
            self.workerOutput.emit("\n")

            subprocess.call(['qtbz2', '-x'] + binary.rstrip().split("/")[1].split())
            CATEGORY = subprocess.check_output(['qxpak', '-x', '-O'] + binary.rstrip().split("/")[1].replace('tbz2', 'xpak').split() + ['CATEGORY'])

            if os.path.exists(binary.rstrip().split("/")[1].replace('tbz2', 'xpak')):
                os.remove(binary.rstrip().split("/")[1].replace('tbz2', 'xpak'))

            if os.path.isdir(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip())):
                shutil.move(binary.rstrip().split("/")[1], os.path.join(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()), os.path.basename(binary.rstrip().split("/")[1])))
            else:
                os.makedirs(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()))
                shutil.move(binary.rstrip().split("/")[1], os.path.join(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()), os.path.basename(binary.rstrip().split("/")[1])))

            if os.path.exists(binary.rstrip().split("/")[1]):
                os.remove(binary.rstrip().split("/")[1])

        portageExec = subprocess.Popen(['emerge', '--usepkg', '--usepkgonly', '--rebuilt-binaries', '--misspell-suggestion=n', '--fuzzy-search=n'] + pkgname, stdout=subprocess.PIPE)

        # kill portage if the program dies or it's terminated by the user
        atexit.register(sisyphus.killportage.start, portageExec)

        for portageOutput in io.TextIOWrapper(portageExec.stdout, encoding="utf-8"):
            if not "These are the packages that would be merged, in order:" in portageOutput.rstrip():
                if not "Calculating dependencies" in portageOutput.rstrip():
                    self.workerOutput.emit(portageOutput.rstrip() + "\n")

        portageExec.wait()
        sisyphus.database.syncLocal()
        self.finished.emit()

    @QtCore.pyqtSlot()
    def startUninstall(self):
        self.started.emit()
        pkgname = Sisyphus.pkgname
        portageExec = subprocess.Popen(['emerge', '--depclean'] + pkgname, stdout=subprocess.PIPE)

        # kill portage if the program dies or it's terminated by the user
        atexit.register(sisyphus.killportage.start, portageExec)

        for portageOutput in io.TextIOWrapper(portageExec.stdout, encoding="utf-8"):
            self.workerOutput.emit(portageOutput.rstrip() + "\n")

        portageExec.wait()
        sisyphus.database.syncLocal()
        self.finished.emit()

    @QtCore.pyqtSlot()
    def startUpgrade(self):
        self.started.emit()

        binhostURL = sisyphus.binhost.getURL()
        areBinaries,areSources,needsConfig = sisyphus.solvedeps.world.__wrapped__() #undecorate

        if not len(areSources) == 0:
            self.workerOutput.emit("\n" + "Source package upgrades detected; Use sisyphus CLI to perform the upgrade; Aborting." + "\n")
        else:
            if not len(areBinaries) == 0:
                self.workerOutput.emit("\n" + "These are the binary packages that will be merged, in order:" + "\n\n" + str(areBinaries) + "\n\n" + "Total:" + " " + str(len(areBinaries)) + " " + "binary package(s)" + "\n\n")
                os.chdir(sisyphus.filesystem.portageCacheDir)
                for index, binary in enumerate([package + '.tbz2' for package in areBinaries]):
                    self.workerOutput.emit(">>> Fetching" + " " + binhostURL + binary)
                    wget.download(binhostURL + binary)
                    self.workerOutput.emit("\n")

                    subprocess.call(['qtbz2', '-x'] + binary.rstrip().split("/")[1].split())
                    CATEGORY = subprocess.check_output(['qxpak', '-x', '-O'] + binary.rstrip().split("/")[1].replace('tbz2', 'xpak').split() + ['CATEGORY'])

                    if os.path.exists(binary.rstrip().split("/")[1].replace('tbz2', 'xpak')):
                        os.remove(binary.rstrip().split("/")[1].replace('tbz2', 'xpak'))

                    if os.path.isdir(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip())):
                        shutil.move(binary.rstrip().split("/")[1], os.path.join(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()), os.path.basename(binary.rstrip().split("/")[1])))
                    else:
                        os.makedirs(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()))
                        shutil.move(binary.rstrip().split("/")[1], os.path.join(os.path.join(sisyphus.filesystem.portageCacheDir, CATEGORY.decode().strip()), os.path.basename(binary.rstrip().split("/")[1])))

                    if os.path.exists(binary.rstrip().split("/")[1]):
                        os.remove(binary.rstrip().split("/")[1])

                portageExec = subprocess.Popen(['emerge', '--update', '--deep', '--newuse', '--usepkg', '--usepkgonly', '--rebuilt-binaries', '--backtrack=100', '--with-bdeps=y', '--misspell-suggestion=n', '--fuzzy-search=n', '@world'], stdout=subprocess.PIPE)

                # kill portage if the program dies or it's terminated by the user
                atexit.register(sisyphus.killportage.start, portageExec)

                for portageOutput in io.TextIOWrapper(portageExec.stdout, encoding="utf-8"):
                    if not "These are the packages that would be merged, in order:" in portageOutput.rstrip():
                        if not "Calculating dependencies" in portageOutput.rstrip():
                            self.workerOutput.emit(portageOutput.rstrip() + "\n")

                portageExec.wait()
                sisyphus.database.syncLocal()
            else:
                self.workerOutput.emit("\n" + "No package upgrades found; Quitting." + "\n")

        self.finished.emit()

    @QtCore.pyqtSlot()
    def cleanOrphans(self):
        self.started.emit()
        portageExec = subprocess.Popen(['emerge', '--depclean'], stdout=subprocess.PIPE)

        # kill portage if the program dies or it's terminated by the user
        atexit.register(sisyphus.killportage.start, portageExec)

        for portageOutput in io.TextIOWrapper(portageExec.stdout, encoding="utf-8"):
            self.workerOutput.emit(portageOutput.rstrip() + "\n")

        portageExec.wait()
        sisyphus.database.syncLocal()
        self.finished.emit()


# launch application
if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle('Breeze')
    window = Sisyphus()
    window.inputBox.setFocus()
    sys.exit(app.exec_())