#!/usr/bin/python3

# GUI for install third party application by epm play
# (c) 2021 Andrey Cherepanov <cas@altlinux.org>

# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.                                 
                                                                    
# 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 General Public License for more
# details.                        
                                                                    
# You should have received a copy of the GNU 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 PyQt6 import QtCore, QtGui, QtWidgets, uic
from PyQt6.QtGui import QMovie, QGuiApplication
from PyQt6.QtCore import QTimer, QObject, QThread, pyqtSignal
import locale
import os
import sys
import subprocess
import re
import time

data_dir = "/usr/share/appinstall"
allowed_list_dir = "/etc/appinstall/allow.d"
at0 = time.perf_counter()

# epm check tty and not working if tty is not functional
#prg_env = {}
prg_env = os.environ.copy()
prg_env["USETTY"] = "0"


class LoadingSplash(QtWidgets.QWidget):
    """Class for loading picture"""
    def __init__(self, parent):
        super().__init__(parent)
        self.parent = parent

        self.moveToCenter()

        self.load_animation = QtWidgets.QLabel(self)
        self.movie = QMovie("loading.gif")
        self.load_animation.setMovie(self.movie)

        self.startAnimation()

    def moveToCenter(self):
        self.setFixedSize(32, 32)
        w = self.parent.frameGeometry().width()
        h = self.parent.frameGeometry().height()
        self.move(int(w/2) - 16, int(h/2) - 16)

    def startAnimation(self):
        self.movie.start()
        self.show()

    def stopAnimation(self):
        self.movie.stop()
        self.hide()

class Worker(QObject):
    """Class for long-time loading process"""
    finished = pyqtSignal()
    progress = pyqtSignal(int)

    def __init__(self, window):
        super().__init__()
        self.w = window

    def run(self):
        """Long-running task."""
        self.w.getApplications()
        self.finished.emit()

    def complete(self):
        """Restore cursor"""
        self.w.loading_splash.stopAnimation()
        QtWidgets.QApplication.restoreOverrideCursor()

class Ui(QtWidgets.QWidget):
    list = {}
    installed = []
    allowed = []
    proc = None
    unprocessed_string = ''
    processing_app = ''

    def __init__(self):
        super(Ui, self).__init__() # Call the inherited classes __init__ method

        # Load UI from file
        uic.loadUi('appinstall.ui', self) # Load the .ui file

        # UI tuning
        self.apps.headerItem().setText(0, "")
        self.apps.setColumnWidth(0, 16)
        
        # Hide filterList (is not implemented) and details
        # self.filterList.hide()
        self.details.hide()
        
        # Set slots for signals
        self.apps.currentItemChanged.connect( self.onSelectionChange )
        self.actionButton.clicked.connect( self.onInstall )
        self.closeButton.clicked.connect( self.onClose )
        self.filterList.textChanged.connect( self.filterChanged )

        # Loading animation
        self.loading_splash = LoadingSplash(self)
        self.loadData()

        # Show window
        self.show()

    def resizeEvent(self, event):
        self.loading_splash.moveToCenter()
        return super().resizeEvent(event)

    def loadData(self):
        self.thread = QThread()
        self.worker = Worker(self)
        self.worker.moveToThread(self.thread)
        self.thread.started.connect(self.worker.run)
        self.worker.finished.connect(self.thread.quit)
        self.worker.finished.connect(self.worker.deleteLater)
        self.thread.finished.connect(self.thread.deleteLater)

        QtWidgets.QApplication.setOverrideCursor( QtCore.Qt.CursorShape.WaitCursor )
        self.thread.start()

        # Final resets
        self.thread.finished.connect(
            lambda: self.worker.complete()
        )

    def getApplicationsInstalled(self):
        """Get installed application from epm play --list"""
        out = subprocess.Popen( [ "/usr/bin/epm", "--inscript", "play", "--list", "--short" ], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, env=prg_env)
        self.installed = [ x.decode().rstrip() for x in out.stdout.readlines() ]
        #print("Installed:", self.installed )

    def getAllowedApplications(self):
        self.allowed = []
        for r, d, f in os.walk( allowed_list_dir ):
            for file in f:
                if file.endswith( ".list" ):
                    with open( os.path.join( allowed_list_dir, file ) ) as fc:
                        self.allowed.extend( [ x.rstrip() for x in fc.readlines() ] )
        #print("Allowed:", self.allowed )

    def getApplications(self):
        """Get application list from epm play"""
        t0 = time.perf_counter()
        self.getApplicationsInstalled()
        self.getAllowedApplications()
        t1 = time.perf_counter()
        out = subprocess.Popen( [ "/usr/bin/epm", "--inscript", "play", "--list-all" ], stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, env=prg_env)
        regexp = re.compile(r"  (\S+)\s+- (.*)", re.ASCII)

        for l in out.stdout.readlines():
            d = regexp.match( l.decode().rstrip() )
            if d:
                #print( d.group(1), d.group(2) )
                name = d.group(1)

                # Hack for wrong exit code of install glusterfs7 (see https://bugzilla.altlinux.org/show_bug.cgi?id=41429)
                # Exclude glusterfs7 from application list
                if name == 'glusterfs7':
                    continue

                # Filter out not allowed applications (if allowed application list is not empty)
                if len( self.allowed ) > 0 and name not in self.allowed:
                    continue

                self.list[ name ] = [ name in self.installed, self.tr( d.group( 2 ) ) ]


        self.filterChanged()
        self.filterList.setFocus()

    def onSelectionChange(self, current, previous):
        """Slot for change selection"""
        if current and current.data( 0, 256 ):
            # Already installed
            self.actionButton.setText( QtWidgets.QApplication.translate( "appinstall", "&Uninstall" ) )
        else:
            # Not installed
            self.actionButton.setText( QtWidgets.QApplication.translate( "MainWindow", "&Install" ) )

    def onInstall(self):
        """Slot for Install button press"""
        selected = self.apps.selectedItems()
        if selected and self.proc == None:
            app = selected[0].text( 1 )
            installed = selected[0].data( 0, 256 )
            #print("onInstall:", app, installed )
            
            # Perform action
            action = [ 'play', '--auto' ]
            if installed:
                action_caption = '%s is removing...'
                action.append( '--remove' )
            else:
                action_caption = '%s is installing...'
            if self.proc:
                self.proc.kill()
            action.append( app )
            #print(action)
            self.processing_app = app

            # Show action
            self.details.show()
            self.details.clear()
            self.details.appendPlainText( QtWidgets.QApplication.translate( "appinstall", action_caption ) % ( app ) )

            # Show buzy cursor
            QtWidgets.QApplication.setOverrideCursor( QtCore.Qt.CursorShape.WaitCursor )

            self.proc = QtCore.QProcess()
            self.proc.finished.connect( self.onProcessFinish )
            self.proc.readyReadStandardOutput.connect( self.onProcessStdout )
            self.proc.readyReadStandardError.connect( self.onProcessStderr )

            # Start subprocess
            self.proc.start( '/usr/bin/epm', action )
            
    def onProcessStdout(self):
        """Main process stdout handle"""
        data = self.proc.readAllStandardOutput()
        self.writeDetail( bytes(data).decode("utf8", errors='ignore') )
        
    def onProcessStderr(self):
        """Main process stderr handle"""
        data = self.proc.readAllStandardError()
        self.writeDetail( bytes(data).decode("utf8", errors='ignore') )
        
    def writeDetail( self, s ):
        """Write part of raw output to detail pane"""
        s = self.unprocessed_string + s
        a = s.split( '\n' )
        self.unprocessed_string = a.pop(-1)
        for line in a:
            self.details.appendPlainText( line )
               
    def onProcessFinish(self):
        """On main process finish"""
        QtWidgets.QApplication.restoreOverrideCursor()
        if self.unprocessed_string:
            self.details.appendPlainText( self.unprocessed_string )
        # Use stored self.processing_app
        it = QtWidgets.QTreeWidgetItemIterator(self.apps)
        selected = []
        while it.value() and self.processing_app:
            item = it.value()
            if item.text(1) == self.processing_app:
                selected = [item]
            it += 1
        if len(selected) > 0 and self.proc.exitCode() == 0:
            installed = selected[0].data( 0, 256 )
            # On successful finish update tick with application
            selected[0].setData( 0, 256, not installed )
            if installed:
                selected[0].setIcon( 0, QtGui.QIcon() )
            else:
                selected[0].setIcon( 0, self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_ArrowDown ))
            # Change button text only if processing app is selected
            if self.apps.selectedItems() and self.apps.selectedItems()[0].text(1) == self.processing_app:
                self.onSelectionChange( selected[0], None )
        self.processing_app = ''
        self.proc = None


    def filterChanged(self):
        """filter list changed"""

        list2 = {}

        filter = self.filterList.text().lower()

        names = list(self.list.keys())

        for key in names:
            item = self.list[key]
            desc = QtWidgets.QApplication.translate( "appinstall", item[1] )

            if filter != '':
                if filter not in key.lower() and filter not in desc.lower():
                    continue

            list2[key] = item


        self.apps.clear()

        names = list( list2.keys() )
        if len( names ) == 0:
            return
        names.sort()
        t2 = time.perf_counter()
        #print("getApplicationsInstalled(): %0.4f" % (t1-t0))
        #print("get all applications: %0.4f" % (t2-t1))

        for i in names:
            #print( i, self.list[ i ][0] )
            child = QtWidgets.QTreeWidgetItem( self.apps )
            if list2[ i ][0]:
                child.setIcon( 0, self.style().standardIcon( QtWidgets.QStyle.StandardPixmap.SP_ArrowDown ))
                child.setData( 0, 256, True )
            else:
                child.setData( 0, 256, False )
            child.setText( 1, i )
            child.setText( 2, QtWidgets.QApplication.translate( "appinstall", list2[i][1] ) )


    def onClose(self):
        """Stop installation process and close window"""
        if self.proc:
            self.proc.kill()
        self.close()

# Chdir to data_dir
os.chdir( data_dir )
        
# Run application
app = QtWidgets.QApplication(sys.argv) # Create an instance of QtWidgets.QApplication

# Bind desktop file to display icon in wayland
QGuiApplication.setDesktopFileName("appinstall")

# Load current locale translation
translator = QtCore.QTranslator(app)
tr_file = "appinstall_" + locale.getlocale()[0].split( '_' )[0]
#print( "Load translation from %s.qm" % ( tr_file ) )
translator.load( tr_file )
app.installTranslator(translator)

# Initialize UI
at1 = time.perf_counter()
window = Ui() # Create an instance of our class
at2 = time.perf_counter()
#print("translator: %0.4f" % (at1-at0))
#print("ui: %0.4f" % (at2-at1))

# Start the application
sys.exit( app.exec() )
