#!/usr/bin/python3.5

################# NOTE #################
# This is quick and dirty written GUI! #
############### END NOTE ###############

import sys, os, atexit, subprocess, imp
sys.path.append("/usr/share/customizer")

import gui_ui # This will tell us which pyuic version was used

QT_VER = None
if sys.modules.keys().__contains__('PyQt4'):
    from PyQt4 import QtCore
    from PyQt4.QtCore import pyqtSignal, QObject, QThread
    from PyQt4.QtCore import QTranslator, QLibraryInfo, QLocale
    from PyQt4.QtGui import QApplication, QMainWindow, QFileDialog, QIcon
    from PyQt4.QtGui import QMessageBox, QWidget
    QT_VER = 4
elif sys.modules.keys().__contains__('PyQt5'):
    from PyQt5 import QtCore # We derive our Thread class from this.
    from PyQt5.QtCore import pyqtSignal, QObject, QThread
    from PyQt5.QtCore import QTranslator, QLibraryInfo, QLocale
    from PyQt5.QtGui import QIcon
    from PyQt5.QtWidgets import QApplication, QMainWindow, QFileDialog
    from PyQt5.QtWidgets import QMessageBox, QWidget
    QT_VER = 5

import lib.config as config
import lib.misc as misc
import lib.message as message
import actions.common as common
import actions.extract as extract
import actions.deb as deb
import actions.hook as hook
import actions.xnest as xnest
import actions.rebuild as rebuild
import actions.clean as clean
import actions.qemu as qemu
misc.CATCH = True
common.misc.CATCH = True
extract.misc.CATCH = True
deb.misc.CATCH = True
hook.misc.CATCH = True
qemu.misc.CATCH = True

# prepare for lift-off
app = QApplication(sys.argv)
translator = QTranslator()
tr_path = QLibraryInfo.location(QLibraryInfo.TranslationsPath)
if translator.load('customizer_' +  QLocale.system().name(), tr_path):
    app.installTranslator(translator)
MainWindow = QMainWindow()
ui = gui_ui.Ui_MainWindow()
ui.setupUi(MainWindow)

def msg_info(msg):
    QMessageBox.information(MainWindow, app.tr('Information'), u'{0}'.format(msg))

def msg_warning(msg):
    QMessageBox.warning(MainWindow, app.tr('Warning'), u'{0}'.format(msg))

def msg_critical(msg):
    QMessageBox.critical(MainWindow, app.tr('Critical'), u'{0}'.format(msg))

def msg_question(msg):
    return QMessageBox.question(MainWindow, app.tr('Question'), u'{0}'.format(msg))

# limit instances to one
lock = '/run/lock/customizer'
running = False
def remove_lock():
    if not running:
        return
    if os.path.isfile(lock):
        os.remove(lock)
atexit.register(remove_lock)

if os.path.isfile(lock):
    msg_critical(app.tr('An instance of Customizer is already running.'))
    sys.exit()
else:
    misc.write_file(lock, str(app.applicationPid()))
    running = True

if int(sys.version_info[0]) >= 3:
    msg_critical(app.tr('You are attempting to run Customizer with Python 3.'))

# Create a quick class to communicate PyQT Signals to the main window
class ThreadMailbox(QObject):
    # Note that because we use a string to specify the type of
    # the QString argument then this code will run under Python v2 and v3.
    question_messagebox = pyqtSignal(['QString'])
    critical_messagebox = pyqtSignal(['QString'])
    warning_messagebox = pyqtSignal(['QString'])
    info_messagebox = pyqtSignal(['QString'])
    exit_customizer = pyqtSignal()
    
# Threads use this mailbox to signal the main window on the main thread.
# Add more pyqtSignal in the class above to 'connect' or 'emit'.
main_thread_mailbox = ThreadMailbox() 

# Create a quick class for worker threads
class Thread(QThread):
    ''' Worker thread '''
    def __init__(self, func):
        super(Thread, self).__init__()
        self.func = func

    def run(self):
        try:
            self.func()
        except SystemExit:
            pass
        except RuntimeError as detail:  # catch misc.system_command
            errmsg = u"An error occured while a thread was running: \n \
            Type: {0}\nDetails: \n{1}\n\n".format(type(detail), detail)
            print(errmsg)
            myval = main_thread_mailbox.warning_messagebox.emit(errmsg)
        except Exception as detail:  # Something else went wrong.
            errmsg = u"An Exception occured while a thread was running: \n \
            Type: {0}\nDetails: \n{1}\n\n".format(type(detail), detail)
            print(errmsg)
            myval = main_thread_mailbox.critical_messagebox.emit(errmsg)
            self.quit()
        finally:
            self.finished.emit()

def setup_gui():
    locales_file = misc.join_paths(config.FILESYSTEM_DIR, \
        'usr/share/i18n/SUPPORTED')
    if os.path.isfile(locales_file):
        for line in misc.readlines_file(locales_file):
            if not line or not line.startswith('#'):
                ui.localesBox.addItem(line.split(' ')[0])
    ui.qemuButton.setEnabled(False)
    ui.workDirEdit.setText(config.WORK_DIR)
    index = ui.localesBox.findText(config.LOCALES)
    ui.localesBox.setCurrentIndex(index)
    index = ui.resolutionBox.findText(config.RESOLUTION)
    ui.resolutionBox.setCurrentIndex(index)
    index = ui.vramBox.findText(config.VRAM)
    ui.vramBox.setCurrentIndex(index)
    index = ui.compressionBox.findText(config.COMPRESSION)
    ui.compressionBox.setCurrentIndex(index)
    casper = misc.join_paths(config.FILESYSTEM_DIR, 'etc/casper.conf')
    if os.path.isdir(config.FILESYSTEM_DIR) \
        and os.path.exists(casper):
        ui.changeWorkDirButton.setEnabled(False)
        ui.configurationBox.setEnabled(True)
        ui.customizationBox.setEnabled(True)
        ui.rebuildButton.setEnabled(True)
        ui.cleanButton.setEnabled(True)
        ui.extraCustomizationBox.setEnabled(True)

        liveuser = common.get_value(casper, 'export USERNAME=')
        if not liveuser.strip():
            liveuser = 'ubuntu'
        ui.userEdit.setText(liveuser)
        ui.hostnameEdit.setText(common.get_value(casper, 'export HOST='))
        common.substitute(casper, '# export FLAVOUR=.*', \
            'export FLAVOUR="Custom"')

        ui.pkgmButton.setEnabled(False)
        for sfile in ('aptitude', 'aptitude-curses', 'synaptic'):
            for sdir in ('bin', 'sbin', 'usr/bin', 'usr/sbin'):
                full_file = misc.join_paths(config.FILESYSTEM_DIR, sdir, sfile)
                if os.path.exists(full_file) and os.access(full_file, os.X_OK):
                    ui.pkgmButton.setEnabled(True)

        ui.xnestButton.setEnabled(False)
        for sfile in misc.list_files(misc.join_paths(config.FILESYSTEM_DIR, \
            'usr/share/xsessions')):
            if sfile.endswith('.desktop'):
                ui.xnestButton.setEnabled(True)

        arch = misc.chroot_exec(('dpkg', '--print-architecture'), \
            prepare=False, mount=False, output=True)
        distrib = common.get_value(config.FILESYSTEM_DIR + '/etc/lsb-release', \
            'DISTRIB_ID=')
        release = common.get_value(config.FILESYSTEM_DIR + '/etc/lsb-release', \
            'DISTRIB_RELEASE=')
        if os.path.exists('%s/%s-%s-%s.iso' % \
            (config.WORK_DIR, distrib, arch, release)):
            ui.qemuButton.setEnabled(True)
    elif os.path.isdir(config.FILESYSTEM_DIR):
        msg_warning(app.tr('The filesystem is not valid or corrupted. Clean is recommended.'))
        ui.cleanButton.setEnabled(True)
    else:
        ui.changeWorkDirButton.setEnabled(True)
        ui.configurationBox.setEnabled(False)
        ui.customizationBox.setEnabled(False)
        ui.rebuildButton.setEnabled(False)
        ui.cleanButton.setEnabled(False)
        ui.extraCustomizationBox.setEnabled(False)
    main_thread_mailbox.question_messagebox.connect(msg_question)
    main_thread_mailbox.critical_messagebox.connect(msg_critical)
    main_thread_mailbox.warning_messagebox.connect(msg_warning)
    main_thread_mailbox.info_messagebox.connect(msg_info)
    main_thread_mailbox.exit_customizer.connect(quit)

def run_core(args, terminal=True):
    message.sub_debug('GUI Running', 'customizer ' + args)
    if terminal:
        terminal = None
        for term in ('xterm', 'lxterminal', 'xfce4-terminal', 'terminal',
            'konsole', 'mate-terminal', 'gnome-terminal', 'terminator', \
            'sakura', 'urxterm', 'aterm', 'Eterm', 'x-terminal-emulator'):
            spath = misc.whereis(term)
            if spath:
                terminal = spath
                message.sub_debug('Found terminal', terminal)

        if not terminal:
            msg_critical(app.tr('No supported terminal emulator detected.'))
            return
    message.sub_debug('Selected last found terminal', terminal)

    try:
        if terminal and terminal.endswith('konsole'):
            # KDE apps tend to fork, this is not desired here since we want to
            # detect if anything changed in the filesystem from setup_gui() at
            # the end of the process but if forked the finished() signal gets
            # emited immidiatly
            subprocess.check_call((terminal, '--nofork', '-e', 'customizer -D ' + args))
        elif terminal:
            subprocess.check_call((terminal, '-e', 'customizer -D ' + args))
        else:
            subprocess.check_call((misc.whereis('customizer'), '-D', args))
    except Exception as detail:
        msg_critical(detail)
    finally:
        setup_gui()

def change_value(sec, var, val):
    conf = None
    try:
        conf = open('/etc/customizer.conf', 'w')
        if not config.conf.has_section(sec):
            config.conf.add_section(sec)
        config.conf.set(sec, var, u'{}'.format(val))
        message.debug('Writing Configuration file', '/etc/customizer.conf')
        config.conf.write(conf)
    except Exception as detail:
        msg_critical(detail)
    finally:
        if conf:
            conf.close()
        imp.reload(config)

def worker_started():
    ui.progressBar.setRange(0, 0)
    ui.progressBar.show()
    ui.changeWorkDirButton.setEnabled(False)
    ui.configurationBox.setEnabled(False)
    ui.customizationBox.setEnabled(False)
    ui.selectButton.setEnabled(False)
    ui.rebuildButton.setEnabled(False)
    ui.qemuButton.setEnabled(False)
    ui.cleanButton.setEnabled(False)
    ui.extraCustomizationBox.setEnabled(False)

def worker_stopped():
    ui.progressBar.setRange(0, 1)
    ui.progressBar.hide()
    ui.selectButton.setEnabled(True)
    setup_gui()

def worker(func):
    # prevent the thread being destroyed
    global thread
    thread = Thread(func)
    thread.finished.connect(worker_stopped)
    worker_started()
    thread.start()

def obtain_filename(dir, extfilter):
    sfile = QFileDialog.getOpenFileName(MainWindow, app.tr('Open'), \
        dir, extfilter)
    message.sub_debug("File Dialog returned: {0} {1}".format(sfile, type(sfile)))
    if QT_VER is not 5:
        return sfile
    else:  # QFileDialog's return is a tuple in 5.x+
        message.sub_debug("File Dialog was QT5 formatted: {0} {1}".format(sfile[0], type(sfile[0])))
        return sfile[0]

def run_extract():
    sfile = obtain_filename(config.ISO, \
        app.tr('ISO Files (*.iso);;All Files (*)'))
    if not sfile:
        return
    change_value('saved', 'iso', sfile)
    extract.config.ISO = sfile
    message.sub_debug('Extracting ISO', sfile)
    try:
        worker(extract.main)
    except Exception as detail:
        msg_critical(detail)

def run_rebuild():
    message.sub_debug('Starting Rebuild...')
    try:
        worker(rebuild.main)
    except Exception as detail:
        msg_critical(detail)

def run_clean():
    if msg_question(app.tr('Confirm cleaning?')) == QMessageBox.Yes:
        message.sub_debug('Starting Cleaning...')
        try:
            worker(clean.main)
        except Exception as detail:
            msg_critical(detail)

def edit_sources():
    message.sub_debug('Trying to edit sources...')
    editor = None
    for edit in ('mousepad', 'leafpad', 'pluma', 'gedit', 'kate', 'kwrite', \
        'medit', 'nedit', 'geany', 'bluefish', 'ultraedit'):
        spath = misc.whereis(edit, False)
        if spath:
            editor = spath

    if not editor:
        msg_critical(app.tr('No supported text editor detected.'))
        return

    try:
        subprocess.check_call((editor, misc.join_paths(config.FILESYSTEM_DIR, \
            'etc/apt/sources.list')))
    except Exception as detail:
        msg_critical(detail)
    finally:
        setup_gui()

def run_deb():
    try:
        sfile = obtain_filename(config.DEB, \
            app.tr('Deb Files (*.deb);;All Files (*)'))
        if not sfile:
            return
        change_value('saved', 'deb', sfile)
        deb.config.DEB = sfile
        message.sub_debug('Installing DEB package', sfile)
        worker(deb.main)
    except Exception as detail:
        msg_critical(detail)

def run_pkgm():
    message.sub_debug('Starting Graphical Package Manager...')
    run_core('-p')

def run_hook():
    try:
        sfile = obtain_filename(config.HOOK, \
            app.tr('Shell Scripts (*.sh);;All Files (*)'))
        if not sfile:
            return
        change_value('saved', 'hook', sfile)
        hook.config.HOOK = sfile
        message.sub_debug('Attempting to run hook', sfile)
        worker(hook.main)
    except Exception as detail:
        msg_critical(detail)

def run_chroot():
    message.sub_debug('Opening terminal...')
    run_core('-c')

def run_xnest():
    message.sub_debug('Opening Nested GUI Desktop...')
    try:
        worker(xnest.main)
    except Exception as detail:
        msg_critical(detail)

def run_qemu():
    message.sub_debug('Starting qemu...')
    try:
        worker(qemu.main)
    except Exception as detail:
        msg_critical(detail)

def change_user():
    message.sub_debug('Attempting to change username...')
    try:
        value = u'{}'.format(ui.userEdit.text())
        if not value.strip():
            msg_critical(app.tr('Live user can not be empty.'))
            ui.userEdit.setText('ubuntu')
            return
        common.set_value(misc.join_paths(config.FILESYSTEM_DIR, \
            'etc/casper.conf'), 'export USERNAME=', value)
    except Exception as detail:
        msg_critical(detail)

def change_hostname():
    message.sub_debug('Attempting to change hostname...')
    try:
        common.set_value(misc.join_paths(config.FILESYSTEM_DIR, \
            'etc/casper.conf'), 'export HOST=', u'{}'.format(ui.hostnameEdit.text()))
    except Exception as detail:
        msg_critical(detail)

def change_work_dir():
    message.sub_debug('Attempting to change hostname...')
    try:
        spath = QFileDialog.getExistingDirectory(MainWindow, \
            'Directory', config.WORK_DIR)
        if not spath:
            return
        # QFileDialog.getExistingDirectory's return is NOT a tuple in 5.x+
        spath = u'{}'.format(spath)
        change_value('preferences', 'work_dir', spath)
        ui.workDirEdit.setText(spath)
    except Exception as detail:
        msg_critical(detail)

def change_locales():
    message.sub_debug('Attempting to change Locales...')
    current = u'{}'.format(ui.localesBox.currentText())
    change_value('preferences', 'locales', current)

def change_resolution():
    message.sub_debug('Attempting to change qemu resolution...')
    current = u'{}'.format(ui.resolutionBox.currentText())
    change_value('preferences', 'resolution', current)

def change_vram():
    message.sub_debug('Attempting to change qemu VRAM size...')
    current = u'{}'.format(ui.vramBox.currentText())
    change_value('preferences', 'vram', current)

def change_compression():
    message.sub_debug('Attempting to change compression type...')
    current = u'{}'.format(ui.compressionBox.currentText())
    change_value('preferences', 'compression', current)

def browse_dir(sdir):
    message.sub_debug('Attempting to open GUI file browser for', sdir)
    try:
        manager = None
        for term in ('dolphin', 'thunar', 'pcmanfm', 'nautilus', 'emelfm', \
            'caja', 'xfe', 'worker', 'gentoo', 'filerunner', 'krusader', \
            'rox-filer'):
            spath = misc.whereis(term)
            if spath:
                manager = spath

        if not manager:
            msg_critical(app.tr('No supported file manager detected.'))
            return

        subprocess.check_call((manager, sdir))
    except Exception as detail:
        msg_critical(detail)
    finally:
        setup_gui()

def browse_filesystem():
    browse_dir(config.FILESYSTEM_DIR)

def browse_iso():
    browse_dir(config.ISO_DIR)

ui.progressBar.hide()
ui.aboutLabel.setText('<b>Customizer 4.2.0 (Zip Release)</b>')
ui.selectButton.clicked.connect(run_extract)
ui.rebuildButton.clicked.connect(run_rebuild)
ui.qemuButton.clicked.connect(run_qemu)
ui.cleanButton.clicked.connect(run_clean)
ui.sourcesButton.clicked.connect(edit_sources)
ui.debButton.clicked.connect(run_deb)
ui.pkgmButton.clicked.connect(run_pkgm)
ui.hookButton.clicked.connect(run_hook)
ui.chrootButton.clicked.connect(run_chroot)
ui.xnestButton.clicked.connect(run_xnest)
ui.userEdit.textChanged.connect(change_user)
ui.hostnameEdit.textEdited.connect(change_hostname)
ui.changeWorkDirButton.clicked.connect(change_work_dir)
ui.localesBox.currentIndexChanged.connect(change_locales)
ui.resolutionBox.currentIndexChanged.connect(change_resolution)
ui.vramBox.currentIndexChanged.connect(change_vram)
ui.compressionBox.currentIndexChanged.connect(change_compression)
# extra
ui.browseFileSystemButton.clicked.connect(browse_filesystem)
ui.browseISOButton.clicked.connect(browse_iso)
setup_gui()
MainWindow.setWindowIcon(QIcon("/usr/share/customizer/logo.png"))
MainWindow.show()
sys.exit(app.exec_())
