#! /bin/python3

### =============== Licence: GPL v3 ========================================== #
#
# Authors:
#   Dan Johansen <strit@manjaro.org>
#   Marcus Britanicus <marcusbritanicus@gmail.com>
#

#
# 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., 51 Franklin Street, Fifth Floor, Boston,
# MA 02110-1301, USA.
#
### ========================================================================== #

import os, sys
import locale

import urllib.request as wget
from lzma import LZMAFile
import json

try:
    from PyQt6.QtCore import QProcess, QObject, QCoreApplication, pyqtSignal, QDir, Qt, QTimer
    from PyQt6.QtGui import QIcon
    from PyQt6.QtWidgets import QApplication, QProgressBar, QMainWindow, QComboBox, QPushButton, QStyleFactory, QFrame, QFileDialog
    from PyQt6.QtWidgets import QGridLayout, QLabel, QSpacerItem, QSizePolicy, QHBoxLayout, QWidget, QMessageBox, QScrollArea
    from PyQt6.QtWidgets import QVBoxLayout, QHBoxLayout, QDialog, QRadioButton, QCheckBox
except ImportError:
    from PyQt5.QtCore import QProcess, QObject, QCoreApplication, pyqtSignal, QDir, Qt, QTimer
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication, QProgressBar, QMainWindow, QComboBox, QPushButton, QStyleFactory, QFrame, QFileDialog
    from PyQt5.QtWidgets import QGridLayout, QLabel, QSpacerItem, QSizePolicy, QHBoxLayout, QWidget, QMessageBox, QScrollArea
    from PyQt5.QtWidgets import QVBoxLayout, QHBoxLayout, QDialog, QRadioButton, QCheckBox

import requests
from typing import List
from typing import Tuple

BLOCKSIZE = 4 * 1024 * 1024             # 4 MiB

proxies = {
    "http": os.getenv( "HTTP_PROXY" ),
    "https": os.getenv( "HTTPS_PROXY" ),
}

def formattedSize( size ):

    if size <= 1024:
        return f"{size} B"

    elif size <= 1024 * 1024:
        return "%0.2f kiB" % (size/1024.)

    elif size <= 1024 * 1024 * 1024:
        return "%0.2f MiB" % (size/1024./1024.)

    elif size <= 1024 * 1024 * 1024 * 1024:
        return "%0.2f GiB" % (size/1024./1024./1024.)

    else:
        return "%0.2f TiB" % (size/1024./1024./1024./1024.)

class GHReleaseLister( QObject ):
	"""A remote directory lister.
	This is a class to read the all the Manjaro ARM releases
	from github using their REST API
	"""

	resultsReady = pyqtSignal()

	def __init__( self ):

		super( GHReleaseLister, self ).__init__()

		#
		# For each 'project', we will have a list of releases (versions)
		# Each 'release' will have a list of assets
		# DeviceEditionVersionMap = {
		#	'rpi4': {
		#		'kde-plasma': {
		#			'21.02':      ('<https://github.com/url/for/rpi4/kde-plasma/21.02.tar.xz', false),
		# 			'21.12':      ('<https://github.com/url/for/rpi4/kde-plasma/21.12.tar.xz', false),
		# 			'2021.12.24': ('<https://github.com/url/for/rpi4/kde-plasma/21.12.tar.xz',  true),
		# 			...
		# 			...
		#		}
		# 	}
		#

		self.DeviceEditionVersionMap = {}

	def fetch( self, detail = "device", device = "", edition = "" ):

		"""detail - The detail that needs to be fetched.
		     - device:  Get the devices list
		     - edition: Get the list of editions for a device
		     - version: Get the list of versions for an edition
			 - url    : Get the download url for the device-edition-version
		   device -  If we are fetching the editions, provide a device name
		   edition - If we are fetcing version list, provide a edition name
		   version - If we need the download url, provide the version name
		"""

		self.detail = detail[:]
		self.device = device[:]
		self.edition = edition[:]

		# Fetch the devices
		if self.detail == "device":
			# If there are devices in this map, then consider the result ready
			if len( self.DeviceEditionVersionMap ):
				self.resultsReady.emit()
				return

		# Fetch the editions for the given device
		elif self.detail == "edition":
			if len( self.DeviceEditionVersionMap[ self.device ] ):
				self.resultsReady.emit();
				return

		# Fetch the versions for the given device, and edition
		elif self.detail == "version":
			if len( self.DeviceEditionVersionMap[ self.device ][ self.edition ] ):
				self.resultsReady.emit();
				return

		self.running = True
		self.run()

	def run( self ):
		"""Perform the actual fetch
		"""

		# Get the device list
		if ( self.detail == "device" ):
			r = requests.get( "https://api.github.com/orgs/manjaro-arm/repos?per_page=50&sort=full_name", proxies = proxies )
			if ( r.status_code != 200 ):
				return

			for d in r.json():
				url = d[ "html_url" ]
				if url.endswith( "-images" ):
					device = url.replace( "https://github.com/manjaro-arm/", "" ).replace( "-images", "" )

					# Init the map to store the editions
					self.DeviceEditionVersionMap[ device ] = {}

			else:
				self.resultsReady.emit()

		# Get edition and version details
		elif ( self.detail == "edition" ):
			r = requests.get( f"https://api.github.com/repos/manjaro-arm/{self.device}-images/releases", proxies = proxies )
			if ( r.status_code != 200 ):
				return

			for d in r.json():
				for i in range( len( d[ "assets" ] ) ):
					if d[ "assets" ][ i ][ "browser_download_url" ].endswith( "img.xz" ) :
						url =  d[ "assets" ][ i ][ "browser_download_url" ]
						parts = url.split( "/" )
						version = parts[ 7 ]
						edition = os.path.basename( parts[ -1 ] ).replace( "Manjaro-ARM-", "" ).split( "-" + self.device )[ 0 ]
						# https://github.com/manjaro-arm/pbpro-images/releases/download/202103220259/Manjaro-ARM-kde-plasma-pbpro-20210322.img.xz

						# Fail-safe
						if ( not self.device in self.DeviceEditionVersionMap.keys() ):
							self.DeviceEditionVersionMap[ self.device ] = {}

						# Init the map to store the versions
						if ( not edition in self.DeviceEditionVersionMap[ self.device ].keys() ):
							self.DeviceEditionVersionMap[ self.device ][ edition ] = {}

						self.DeviceEditionVersionMap[ self.device ][ edition ][ version ] = ( url, d[ "prerelease" ] )

			else:
				self.resultsReady.emit()

		else:
			self.resultsReady.emit()

	def results( self ):
		"""Return the results of the 'fetch'
		"""

		if self.detail == "device":
			return self.DeviceEditionVersionMap.keys()

		elif self.detail == "edition":
			return self.DeviceEditionVersionMap[ self.device ].keys()

		elif self.detail == "version":
			return self.DeviceEditionVersionMap[ self.device ][ self.edition ]

		# Should no come here
		return []


def flashImage( imagePath, targetFile ):
    """A simple function to flash @imagePath to @targetFile

    Note: @targetFile can be a file, a partition or even a device.

    This function will require root privileges for flashing to devices living in /dev/
    """

    print( "Flashing %s to the %s." % ( imagePath, targetFile ) )
    print( "This process will take time.." )
    print( "Please wait patiently..." )

    imgFileSize = os.path.getsize( imagePath )
    fmtSize = imgFileSize / ( 1024 * 1024 * 1024 )

    print( "Compressed image size: %0.2f GiB (%d B)" % ( fmtSize, imgFileSize ) )

    xz = LZMAFile( imagePath, "rb" )    # Assume @imagePath is the full path of the image file
                                        # and it opens without problems [no check performed for now]

    dev = open( targetFile, 'wb' )      # Assume @targetFile is the full path of the target
                                        # and we have write permission for it [no check performed for now]

    buf = b"\x00";
    while len( buf ) > 0 :              # Keep looping till we have completely read the image in blocks of 4 MiB

        buf = xz.read( BLOCKSIZE )      # Read 1 BLOCKSIZE bytes of uncompressed data at once
        dev.write( buf )                # Write the just read data ro the file
        dev.flush()

        sys.stdout.write( "%d %d\n" % ( xz.tell(), len( buf ) ) )
        sys.stdout.flush()
        os.fsync( dev.fileno() )

    dev.flush()
    dev.close()


class ManjaroArmFlasher( QMainWindow ):
    """
    Download a manjaro image suitable for a given device and flash it.
    """

    def __init__( self ) :
        """Class initializer
        """

        QMainWindow.__init__( self )

        self.abort = False
        self.downloading = False

        title = 'Manjaro ARM Flasher'
        self.setWindowTitle(title)
        self.haveLocalImage = False

        self.createUI()

        self.progressBar.setRange( 0, 0 )
        self.progressLabel.setText( "Checking network connectivity..." )

        QApplication.processEvents()

        QTimer.singleShot( 1000, self.checkNetwork )

    def checkNetwork( self ) :
        """checkNetwork() -> None

        Check the internet connectivity

        @return None
        """

        ping = QProcess()
        ## ping github.com 10s timeout, 5 pings
        ping.start( "ping", [ "-w", "10", "-c", "5", "github.com" ] )
        ping.waitForFinished( -1 )

        if ping.exitCode():
            self.progressLabel.setText( "ERROR!!!" )
            QApplication.processEvents()

            QMessageBox.information(
                self,
                "Manjaro ARM Flasher | Network Error",
                "There seems to be a problem connecting to the internet. "
                "You'll be able to flash local images only. "
                "If you want to download remote images, please check your connectivity "
                "and try again."
            )

            self.deviceCB.setDisabled( True )
            self.progressLabel.setText( "Network inaccessible. Please choose a local image." )

            self.browseBtn.setEnabled( True )

        else:
            self.progressLabel.setText( "Loading device list..." )
            QApplication.processEvents()

            devices = []
            self.dl = GHReleaseLister()
            self.dl.fetch( "device" )
            devices = self.dl.results()

            self.deviceCB.clear()
            self.deviceCB.addItems( devices )
            self.deviceCB.setCurrentIndex( -1 )
            self.deviceCB.currentIndexChanged[ int ].connect( self.loadEditions )

            self.downloadBtn.setDisabled( True )
            self.progressLabel.setText( "Choose device..." )
            QApplication.processEvents()


        self.setEnabled( True )
        self.progressBar.setRange( 0, 1 )

    def getImageFile( self ):
        title = "Manjaro ARM Flasher | Open Image File"
        self.fileName, self.ext = QFileDialog.getOpenFileName(
            self,
            title,
            QDir.homePath(),
            "Image Files (*.img.xz) (*.img.xz)"
        )

        if self.fileName :
            print( f"Ready to flash: {self.fileName}" )
            self.downloadBtn.setEnabled( True )
            self.haveLocalImage = True

    def createUI( self ) :
        """createUI() -> None

        Create the user interface

        @return None
        """

        self.browseBtn = QPushButton( "&Browse" )
        self.browseBtn.clicked.connect( self.getImageFile )

        self.deviceCB = QComboBox()

        self.editionCB = QComboBox()
        self.editionCB.setDisabled( True )
        self.editionCB.currentIndexChanged[ int ].connect( self.loadVersions )

        self.versionCB = QComboBox()
        self.versionCB.setDisabled( True )
        # self.versionCB.currentIndexChanged[ int ].connect( self.loadVersions )

        self.targetCB = QComboBox()

        self.listPreRelCB = QCheckBox( "List Pre-Releases" )
        self.listPreRelCB.toggled.connect( self.loadVersions )

        self.progressBar = QProgressBar();
        # Looks nice: A nice thick progressbar: Disable this for a native look
        self.progressBar.setStyle( QStyleFactory.create( "Fusion" ) )

        self.progressLabel = QLabel()
        self.progressLabel.setAlignment( Qt.AlignmentFlag.AlignCenter )

        self.downloadBtn = QPushButton( QIcon.fromTheme( ":/icons/icon.png" ), "&Start" )
        self.downloadBtn.clicked.connect( self.downloadAndFlash )
        self.downloadBtn.setDisabled( True )

        self.cancelBtn = QPushButton( QIcon.fromTheme( "dialog-cancel" ), "&Cancel" )
        self.cancelBtn.clicked.connect( self.cancelDownload )
        self.cancelBtn.setDisabled( True )

        self.quitBtn = QPushButton( QIcon.fromTheme( "application-exit" ), "&Quit" )
        self.quitBtn.clicked.connect( self.quitApp )

        # For auto-layout (vertical on mobile and horizontal on pc)
        size = QApplication.instance().primaryScreen().size()

        # Layout for comboboxes
        baseLyt = QGridLayout()

        # Landscape mode
        if ( size.width() > size.height() ):
            baseLyt.addWidget( QLabel( "Choose a local image:" ), 0, 0 )
            baseLyt.addWidget( self.browseBtn, 0, 1 )
            baseLyt.addWidget( QLabel( "Or select a remote image:" ), 1, 0 )

            baseLyt.addItem( QSpacerItem( 10, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.MinimumExpanding ), 2, 0, 1, 4 )

            baseLyt.addWidget( QLabel( "Device" ), 3, 0 )
            baseLyt.addWidget( self.deviceCB, 4, 0 )
            baseLyt.addWidget( QLabel( "Edition" ), 3, 1 )
            baseLyt.addWidget( self.editionCB, 4, 1 )
            baseLyt.addWidget( QLabel( "Version" ), 3, 2 )
            baseLyt.addWidget( self.versionCB, 4, 2 )
            baseLyt.addWidget( QLabel( "Target Drive" ), 3, 3 )
            baseLyt.addWidget( self.targetCB, 4, 3 )

            baseLyt.addWidget( self.listPreRelCB, 5, 0, 1, 4 )

            baseLyt.addWidget( self.progressBar, 6, 0, 1, 4 )
            baseLyt.addWidget( self.progressLabel, 7, 0, 1, 4 )

            btnLyt = QHBoxLayout()
            btnLyt.addWidget( self.quitBtn )
            btnLyt.addStretch()
            btnLyt.addWidget( self.cancelBtn )
            btnLyt.addWidget( self.downloadBtn )

            baseLyt.addLayout( btnLyt, 8, 0, 1, 4 )

            base = QWidget()
            base.setLayout( baseLyt )

            self.setCentralWidget( base )

        # Portait mode
        else:
            baseLyt.addWidget( QLabel( "Choose a local image:" ), 0, 0 )
            baseLyt.addWidget( self.browseBtn, 1, 0 )
            baseLyt.addWidget( QLabel( "Or select a remote image:" ), 2, 0 )

            baseLyt.addWidget( QLabel( "Device" ), 5, 0 )
            baseLyt.addWidget( self.deviceCB, 6, 0 )
            baseLyt.addWidget( QLabel( "Edition" ), 7, 0 )
            baseLyt.addWidget( self.editionCB, 8, 0 )
            baseLyt.addWidget( QLabel( "Version" ), 9, 0 )
            baseLyt.addWidget( self.versionCB, 10, 0 )
            baseLyt.addWidget( QLabel( "Target Drive" ), 11, 0 )
            baseLyt.addWidget( self.targetCB, 12, 0 )
            baseLyt.addWidget( self.listPreRelCB, 13, 0 )

            baseLyt.addItem( QSpacerItem( 10, 20, QSizePolicy.Fixed, QSizePolicy.MinimumExpanding ), 14, 0 )

            baseLyt.addWidget( self.progressBar, 15, 0 )
            baseLyt.addWidget( self.progressLabel, 16, 0 )

            sep = QFrame()
            sep.setFrameShape( QFrame.HLine )

            baseLyt.addWidget( self.downloadBtn, 17, 0 )
            baseLyt.addWidget( self.cancelBtn, 18, 0 )
            baseLyt.addWidget( sep, 19, 0 )
            baseLyt.addWidget( self.quitBtn, 20, 0  )

            base = QWidget()
            base.setLayout( baseLyt )

            scroll = QScrollArea()
            scroll.setWidgetResizable( True )
            scroll.setWidget( base )

            self.setCentralWidget( scroll )

        self.setDisabled( True )

        self.loadDisks()

    def loadEditions( self ) :

        device = self.deviceCB.currentText()
        if device:
            # Clear the list; we will store all values from all devices
            self.editionCB.clear()

            self.progressBar.setRange( 0, 0 )
            QApplication.instance().processEvents()

            editions = []
        self.dl.fetch( "edition", device )
        editions = self.dl.results()

        if ( self.editionCB.receivers( self.editionCB.currentIndexChanged ) ):
            self.editionCB.currentIndexChanged.disconnect()

            self.editionCB.setEnabled( True )
            self.editionCB.addItems( editions )
            self.editionCB.setCurrentIndex( -1 )

            self.editionCB.currentIndexChanged[ int ].connect( self.loadVersions )
            self.progressBar.setRange( 0, 1 )
            self.progressLabel.setText( "Choose edition..." )
            QApplication.instance().processEvents()

        self.downloadBtn.setDisabled( True )

    def loadVersions( self ) :

        device = self.deviceCB.currentText()
        edition = self.editionCB.currentText()
        if device and edition:
            self.versionCB.clear()

            self.progressBar.setRange( 0, 0 )
            QApplication.instance().processEvents()

            self.dl.fetch( "version", self.deviceCB.currentText(), self.editionCB.currentText() )
            print( "Versions retrieved." )

            self.versions = self.dl.results();
            print( self.versions )
            for version in self.versions.keys() :
                preRel = self.versions[ version ][ 1 ]
                if not preRel:
                    self.versionCB.addItem( version )

                else:
                    if ( self.listPreRelCB.isChecked() ):
                        self.versionCB.addItem( version )

            self.versionCB.setEnabled( True )
            self.versionCB.setCurrentIndex( -1 )

            self.progressBar.setRange( 0, 1 )
            self.progressLabel.setText( "Choose version..." )
            QApplication.instance().processEvents()

            def prepareDnF() :
                self.downloadBtn.setEnabled( True )
                self.progressLabel.setText( "Ready to download..." )
                QApplication.instance().processEvents()

            self.versionCB.currentIndexChanged.connect( prepareDnF )

        self.downloadBtn.setDisabled( True )

    def loadDisks( self ) :
        """loadDisks() -> None

        Run lsblk and load the disks which have no mounted partitions

        @return None
        """

        blkinfo = QProcess();
        blkinfo.start( "lsblk", [ "-o", "NAME,MOUNTPOINT", "-J" ] )
        blkinfo.waitForFinished()

        disks_data = blkinfo.readAll()
        disks = json.loads( disks_data.data() )

        for disk in disks[ 'blockdevices' ]:
            path = disk[ 'name' ]

            # If this drive has 'mountpoint' key, and is mounted, continue
            # I.e, don't process it further.
            if ( 'mountpoint' in disk ) and ( disk[ 'mountpoint' ] != None ):
                continue

            # By now, we can be sure that disk is not mounted.
            # If there are no children, add it to the combo and continue
            if 'children' not in disk:
                self.targetCB.addItem( path )
                continue

            # This disk has children.
            # We should now see if any of the children are mounted
            children = disk[ 'children' ]
            for child in children:
                if ( 'mountpoint' in child ) and ( child[ 'mountpoint' ] != None ):
                    break

            else:
                self.targetCB.addItem( path )

    def downloadAndFlash( self ) :
        """downloadAndFlash() -> None

        Begin the download of the file and optionally flash it to a device

        @return None
        """

        #  outputname = QFileDialog.getSaveFileName( self, "Save File", QDir.homePath(), "All files (*.*)" );
        self.downloadBtn.setDisabled( True )
        self.cancelBtn.setEnabled( True )

        deviceValue  = "" if self.haveLocalImage else self.deviceCB.currentText()
        editionValue = "" if self.haveLocalImage else self.editionCB.currentText()
        versionValue = "" if self.haveLocalImage else self.versionCB.currentText()

        imageLocation = "/var/tmp/"
        outputname = "Manjaro-ARM-" + editionValue + "-" + deviceValue + "-" + versionValue + ".img.xz"

        if not self.haveLocalImage:
            self.fileName = imageLocation + outputname
            print( f"Starting image download - storing it in {self.fileName}." )

        # I guess this is needed only for testing purposes
        # print("Device = " + deviceValue)
        # print("Edition = " + editionValue)
        # print("Version = " + versionValue)
        # print("Image file = " + self.fileName)

        self.progressBar.setRange( 0, 0 )
        self.abort = False
        self.downloading = True

        QApplication.instance().processEvents()

        ## Download and flash a remote image
        if not self.haveLocalImage:
            url = self.versions[ self.versionCB.currentText() ][ 0 ]
            try:
                wget.urlretrieve( url, self.fileName, self.showDownloadProgress )  # added reporthook callback

            except Exception as e:
                print( e )
                QMessageBox.warning(
                    self,
                    "Download aborted",
                    "The download was aborted before it could be completed. Following error was returned: <p><tt>%s</tt></p>" % e
                )

                self.progressBar.setRange( 0, 1 )
                self.progressBar.setValue( -1 )
                self.downloadBtn.setEnabled( True )
                self.cancelBtn.setDisabled( True )

                self.abort = False
                self.downloading = False

                return

            else:
                self.progressBar.setRange( 0, 1 )
                self.progressBar.setValue( 1 )
                self.progressLabel.setText( "Ready to flash..." )
                ret = QMessageBox.information(
                    self,
                    "Download complete",
                    "The download was completed successfully. Click Ok to flash the image to the device.",
                    QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Abort
                )

                if ( ret == QMessageBox.StandardButton.Abort ) :
                    self.close()
                    return

                self.flash()

        ## A local image is available, so flash it
        else:
            ret = QMessageBox.information(
                self,
                "Local file chosen",
                "You have chosen a local file to be flashed. Click Ok to flash the image to the device.",
                QMessageBox.StandardButton.Ok, QMessageBox.StandardButton.Abort
            )

            if ( ret == QMessageBox.StandardButton.Abort ) :
                self.close()
                return

            self.flash()

    def flash( self ):
        """flash() -> None

        Flash the given file to a device

        @return None
        """

        targetValue = self.targetCB.currentText()
        # print("SD/eMMC/USB = " + targetValue)

        self.browseBtn.setDisabled( True )
        self.deviceCB.setDisabled( True )
        self.editionCB.setDisabled( True )
        self.versionCB.setDisabled( True )
        self.downloadBtn.setDisabled( True )
        self.cancelBtn.setDisabled( True )
        self.quitBtn.setDisabled( True )

        xz = QProcess()
        xz.start( "xz", [ "--list", self.fileName ] )
        xz.waitForFinished( -1 )

        ## Some assumption:
        #  There will be one and only one stream.
        #  It will give us the uncompressed file size
        #  Typical output:
        #      Strms  Blocks   Compressed Uncompressed  Ratio  Check   Filename
        #          1     229  1,116.9 MiB  5,493.0 MiB  0.203  CRC64   Man.....

        res = xz.readAll().data().decode()
        res = res.strip().split( "\n" )
        sizeLine = res[ 1 ].strip().split()
        size = 0
        try:
            size = locale.atof( sizeLine[ 4 ] )
            size = int( size * 1024 )

        except ValueError:
            pass

        self.downloading = False
        exec_ = os.path.realpath( sys.argv[ 0 ] )
        args = ( exec_, "--flash", self.fileName, "/dev/" + targetValue )

        ## Begin the flashing: Use QProcess to initiate the flashing as root.
        ## All users have permissions to flash as root, since we've added that rule.

        self.flashProc = QProcess()
        self.flashProc.readyReadStandardOutput.connect( self.showFlashProgress )
        self.flashProc.finished.connect( self.showFlashComplete )

        ## We cannot still get the uncompressed image size
        ## So we'll not show 0 - 100% progress
        self.progressBar.setRange( 0, size )

        self.flashProc.start( "sudo", args )

    def cancelDownload( self ) :
        """cancelDownload() -> None

        Cancel the download of the file

        @return None
        """

        reply = QMessageBox.question(
            self,
            "Abort?",
            "If you abort now, you cannot continue from where you left off. You will have to start again. Continue?",
            QMessageBox.Yes, QMessageBox.No
        )

        if ( reply == QMessageBox.Yes ):
            self.abort = True
            QApplication.instance().processEvents()
            if not self.haveLocalImage:
                os.remove(self.fileName)

    def quitApp( self ) :
        """quitApp() -> None

        Cancel the download and quit the app

        @return None
        """

        if self.downloading:
            reply = QMessageBox.question(
                self,
                "Abort?",
                "If you quit now, the download will be aborted. You cannot continue from where you left off. "
                "You will have to start again. Continue?",
                QMessageBox.Yes, QMessageBox.No
            )

            if ( reply == QMessageBox.Yes ):
                self.abort = True
                QApplication.instance().processEvents()
                if not self.haveLocalImage:
                    os.remove(self.fileName)

        self.close()

    def showDownloadProgress( self, blocks, blocksize, filesize ) :
        """showDownloadProgress() -> None

        Show the progress of the download

        @return None
        """

        QApplication.instance().processEvents()

        if self.abort:
            raise Exception( "User aborted the download!" )

        self.progressBar.setRange( 0, filesize )
        self.progressBar.setValue( blocks * blocksize )

        self.progressLabel.setText( f"Downloaded {formattedSize( blocks * blocksize )} of {formattedSize( filesize )}" )

        QApplication.instance().processEvents()

    def showFlashProgress( self ):

        QApplication.instance().processEvents()

        # Our program is not mature to obtain the uncompressed image size yet
        data = self.flashProc.readAll().data().decode( "utf8" ).strip()
        try:
            xdata = data.split()
            bytes = int( xdata[ 0 ] )
            self.progressBar.setValue( bytes // 1024 )

            if ( bytes // 1024 == 100 ):
                self.progressLabel.setText( "Flushing device..." )

            else:
                self.progressLabel.setText( "Data written " + formattedSize( bytes ) )

        except:
            print( data )

        QApplication.instance().processEvents()

    def showFlashComplete( self, exitCode, exitStatus ):

        self.close()

        print( self.flashProc.readAll().data().decode( "utf8" ).strip() )

        print( "Exit code:  ", exitCode )
        print( "Exit status:", exitStatus )

        if exitCode or exitStatus == QProcess.ExitStatus.CrashExit:
            QMessageBox.information(
                self,
                "Flashing failed",
                "It appears there was some problem in flashing the image to your device. Please check the logs or try again later."
            )
            if not self.haveLocalImage:
                os.remove(self.fileName)

        else:
            QMessageBox.information(
                self,
                "Flashing successful",
                "The image was flashed successfully to your device."
            )
            if not self.haveLocalImage:
                os.remove(self.fileName)
                
        qApp.quit()

if __name__ == '__main__' :

    if ( "--flash" in sys.argv ) :
        flashImage( sys.argv[ 2 ], sys.argv[ 3 ] )
        sys.exit( 0 )

    else:
        app = QApplication( sys.argv )

        Gui = ManjaroArmFlasher();
        Gui.show()

        sys.exit( app.exec() )
