#!/usr/bin/python3
# -*- coding:utf-8 -*-
#
# Copyright (C) 2015-2018 Luke Horwell <code@horwell.me>
# Copyright (C) 2015-2018 Martin Wimpress <code@flexion.org>
#
# Software Boutique 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.
#
# Software Boutique 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 Software Boutique. If not, see <http://www.gnu.org/licenses/>.
#

"""
    The curated software collection application for Parrot OS
"""

import gi
gi.require_version("Gdk", "3.0")
gi.require_version("Gtk", "3.0")
gi.require_version("Notify", "0.7")
gi.require_version("WebKit2", "4.0")
from gi.repository import GLib, Gio, GObject, Gdk, Gtk, Notify, WebKit2

import argparse
import gettext
import inspect
import json
import locale
import os
import platform
import random
import requests
import signal
import subprocess
import sys
import time
import traceback
import webbrowser
import gzip
import tarfile
from datetime import datetime
from threading import Thread
from shutil import rmtree

## FIXME! Temporary workaround to ensure the snap works when proctitle is not available on the host.
try:
    import setproctitle
    proctitle_available = True
except ImportError:
    proctitle_available = False

__VERSION__ = '18.04.0'

try:
    import pylib.boutique as boutique
    import pylib.preferences as Preferences
    import pylib.common as Common
except ImportError:
    import software_boutique.boutique as boutique
    import software_boutique.preferences as Preferences
    import software_boutique.common as Common

supported_arch = ["i386", "amd64", "armhf", "arm64"]
supported_codenames = ["parrot", "stable", "testing", "lts"]
default_codename = "stable"
support_url = "https://archive.parrotsec.org/store/"
index_url = "https://archive.parrotsec.org/store/"
show_holding_page = False

def get_data_source():
    """
    Retrieves the data source for assets used by the application.
    """
    current_folder = os.path.join(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe()))), 'data/')
    try:
        snap_folder = os.path.join(os.environ['SNAP'], 'usr', 'share', 'parrot-package-manager')
    except KeyError:
        snap_folder = ""
    system_folder = os.path.join('/', 'usr', 'share', 'parrot-package-manager')

    for folder in [current_folder, snap_folder, system_folder]:
        if os.path.exists(folder):
            dbg.stdout("Using " + folder + " for data source.", dbg.debug, 1)
            return folder

    dbg.stdout("Unable to source the data directory.", dbg.error, 1)
    sys.exit(1)

def spawn_thread(target, daemon=True, args=[]):
    """
    Creates another thread to run a function in the background.
    """
    dbg.stdout("Spawning thread: {0} (daemon {1}, args={2})".format(target.__name__, daemon, args), dbg.debug, 2)
    newthread = Thread(target=target, args=(args))
    if daemon:
        newthread.daemon = True
    newthread.start()

def delay_function(target, time_value):
    """
    Intentionally waits before running a function, e.g. for front-end.
    """
    time.sleep(time_value)
    target()

def make_html_safe(string):
    """
    Returns a string that is HTML safe that won't cause interference.
    For example, when used in JavaScript attributes.
    """
    return string.replace("'", "&#145;")


class WebView(WebKit2.WebView):
    """
    Setting up the program's web browser and processing WebKit operations
    """
    def __init__(self):
        self.webkit = WebKit2
        self.webkit.WebView.__init__(self)

        # Python <--> WebView communication
        self.connect("notify::title", self._on_title_change)
        self.connect("context-menu", self._on_context_menu)
        self.connect("load-changed", self.on_finish_load)

        # Enable keyboard navigation
        self.get_settings().set_enable_spatial_navigation(True)
        self.get_settings().set_enable_caret_browsing(True)

        # Show console messages in stdout if we're debugging.
        if dbg.verbose_level >= 2:
            self.get_settings().set_enable_write_console_messages_to_stdout(True)

        # Enable web inspector for debugging
        if dbg.verbose_level == 3:
            self.get_settings().set_property("enable-developer-extras", True)
            inspector = self.get_inspector()
            inspector.show()

        dbg.stdout("Finished webkit2 initalisation.", dbg.success, 1)

    def run_js(self, function):
        """
        Runs a JavaScript function on the page, regardless of which thread it is called from.
        GTK+ operations must be performed on the same thread to prevent crashes.
        """
        GLib.idle_add(self._run_js, function)

    def _run_js(self, function):
        """
        Runs a JavaScript function on the page when invoked from run_js()
        """
        self.run_javascript(function)
        return GLib.SOURCE_REMOVE

    def update_page(self, element, function, parm1=None, parm2=None):
        """
        Runs a JavaScript jQuery function on the page, ensuring correctly parsed quotes.
        """
        if parm1 and parm2:
            self.run_js('$("' + element + '").' + function + "('" + parm1.replace("'", '\\\'') + "', '" + parm2.replace("'", '\\\'') + "')")
        if parm1:
            self.run_js('$("' + element + '").' + function + "('" + parm1.replace("'", '\\\'') + "')")
        else:
            self.run_js('$("' + element + '").' + function + '()')

    def on_finish_load(self, view, frame):
        """
        Callback: On page change.
        """
        if not self.is_loading():
            dbg.stdout("Finished page initalisation.", dbg.success, 1)

    def _on_title_change(self, view, frame):
        """
        Callback: When page title is changed, used for communicating with Python.
        """
        title = self.get_title()
        if title != "null" and title != "" and title != None:
            dbg.stdout("Command: '{0}'".format(title), dbg.debug, 2)
            app.process_command(title)

    def _on_context_menu(self, webview, menu, event, htr, user_data=None):
        # Disable context menu.
        return True


class ApplicationWindow(object):
    """
    Main thread for building and interacting with the application.
    """
    def __init__(self):
        self.webview = None

    def build(self, webview_obj):
        title = _("Software Boutique")
        width = 900
        height = 600
        html_file = "boutique.html"

        # Nice process name
        if proctitle_available:
            setproctitle.setproctitle("parrot-package-manager")

        w = Gtk.Window()
        w.set_position(Gtk.WindowPosition.CENTER)
        w.set_wmclass("parrot-package-manager", "parrot-package-manager")
        w.set_title(title)
        w.set_icon_from_file(os.path.join(data_source, "img", "boutique-icon.svg"))

        # http://askubuntu.com/questions/153549/how-to-detect-a-computers-physical-screen-size-in-gtk
        s = Gdk.Screen.get_default()
        if s.get_height() <= 600:
            w.set_size_request(768, 528)
        else:
            w.set_size_request(width, height)

        self.webkit = webview_obj

        # Load the starting page
        html_path = "file://" + os.path.abspath(os.path.join(data_source, html_file))
        self.webkit.load_uri(html_path)

        # Build scrolled window widget and add our appview container
        sw = Gtk.ScrolledWindow()
        sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
        sw.add(self.webkit)

        # Build an autoexpanding box and add our scrolled window
        b = Gtk.VBox(homogeneous=False, spacing=0)
        b.pack_start(sw, expand=True, fill=True, padding=0)

        # Add the box to the parent window
        w.add(b)
        w.connect('delete-event', self._close)
        w.show_all()

    def run(self):
        signal.signal(signal.SIGINT, signal.SIG_DFL)
        Gtk.main()

    def _close(self, window, event):
        shutdown()


class SoftwareBoutique(object):
    """
    "app" object that stores variables and shared data/functions
    throughout the program.
    """
    def __init__(self):
        ## Placeholders (these are set in start_loading() so UI appears quicker)
        self.webkit = None
        self.update_page = None
        self.run_js = None
        self.queue = None                   # QueueOperationsThread()
        self.is_ubuntu_mate = False
        self.boutique_ppa_enabled = False
        self.current_open_app_uuid = "null"

        # If the app isn't even compiled, stop.
        # If the app mysterously lost its data, also stop.
        if not os.path.exists(os.path.join(data_source, "boutique.css")):
            if data_source.startswith("/usr"):
                dbg.stdout(_("Software Boutique is missing critical data at this path:"), dbg.error, 0)
                dbg.stdout("  => " + data_source, dbg.error, 0)
                dbg.stdout(_("The application cannot start. Please try re-installing Software Boutique."), dbg.error, 0)
            else:
                dbg.stdout("Silly developer! You forgot to compile me, please run 'parrot-package-manager-dev' instead.", dbg.error, 0)
            exit(1)

        # User Preferences
        if pref.read("advanced-mode", False) == True:
            self.advanced_mode = True
        else:
            self.advanced_mode = False

    def process_command(self, cmd):
        if cmd == "init-boutique":
            spawn_thread(self.start_loading)

        elif cmd == "force-boutique-restart":
            os.execv(__file__, sys.argv)

        elif cmd == "force-cache-reset":
            sys.argv.append("--clear-cache")
            os.execv(__file__, sys.argv)

        elif cmd == "dismiss-unsupported-msg":
            pref.write("already-warned-distro", True)
            self.close_message_page()

        elif cmd == "show-introduction-again":
            self.show_introduction_page()

        elif cmd == "finished-introduction":
            self.close_introduction_page()

        elif cmd == "enter-start-page":
            self.generate_start_page_apps()

        elif cmd == "exit":
            shutdown()

        elif cmd.startswith("details?"):
            cmd = cmd.split("?")
            categoryid = cmd[1]
            appid = cmd[2]
            self.show_app_details(categoryid, appid)

        elif cmd == "close-details":
            self.hide_app_details()

        elif cmd.startswith("launch?"):
            cmd = cmd.split("?")
            categoryid = cmd[1]
            appid = cmd[2]
            app_obj = boutique.get_application_details(self.index, categoryid, appid)
            launch_cmd = app_obj.launch_cmd
            if launch_cmd:
                subprocess.Popen(launch_cmd.split(" "))
            return

        elif cmd.startswith("web?"):
            url = cmd.split("?")[1]
            webbrowser.open(url)

        elif cmd.startswith("install?") or cmd.startswith("remove?"):
            cmd = cmd.split("?")
            operation = cmd[0]
            categoryid = cmd[1]
            appid = cmd[2]
            app_uuid = categoryid + "-" + appid

            if self.queue.is_already_queued(app_uuid):
                dbg.stdout(app_uuid + " is already queued. It must be unqueued first.", dbg.error, 0)
            else:
                self.queue.add_item(operation, self.queue._get_appdata(categoryid, appid))
                self.run_js("animateClass('#queue-button', 'pulse-added', 2000)")

        elif cmd.startswith("drop-queue?"):
            cmd = cmd.split("?")
            operation = cmd[0]
            categoryid = cmd[1]
            appid = cmd[2]
            app_uuid = categoryid + "-" + appid

            self.queue.remove_item(app_uuid)
            self.run_js("animateClass('#queue-button', 'pulse-added', 2000)")

        elif cmd == "clear-queue-pending":
            self.update_page(".queue-card", "addClass", "swiping");
            self.update_page(".queue-active", "removeClass", "swiping");

            in_progress_item = None
            # Remove all items, except the one that's in progress.
            for item in self.queue.queue:
                if self.queue.current_app_uuid == self.queue.queue[0][1].uuid:
                    in_progress_item = item

            if in_progress_item:
                self.queue.queue = [in_progress_item]
            else:
                self.queue.queue = []

            # Allow swipe animation to finish.
            spawn_thread(delay_function, False, [self.queue.update_queue_state, 0.5])
            spawn_thread(delay_function, False, [self.queue.update_count, 0.5])

        elif cmd == "clear-queue-completed":
            self.update_page(".queue-complete", "addClass", "swiping");
            self.queue.finished_queue = []

            # Allow swipe animation to finish before refreshing list.
            spawn_thread(delay_function, False, [self.queue.update_queue_state, 0.5])

        elif cmd.startswith("toggle-pref?"):
            key = cmd.split("?")[1]
            pref.toggle(key)
            self.build_settings_page()

        else:
            dbg.stdout("Unimplemented function!", dbg.error, 2)

    def override_for_debugging(self):
        if dbg.override_arch:
            self.current_arch = dbg.override_arch
        if dbg.override_codename:
            boutique.current_os_codename = dbg.override_codename

    def start_loading(self):
        # Fade in navigation bar, matching colour where possible.
        self._set_navigation_theme()
        self.update_page("#header", "show")
        self.update_page("#footer", "show")
        self.update_page("#loading-page", "show")
        self.update_page("#message-page", "fadeOut", "fast")

        # Temporary holding page for snaps
        if show_holding_page:
            self.show_message_page("serious-error.png", _("In Development"),
                [
                    _("This new version of Software Boutique is currently in development."),
                    _("The existing version is part of Ubuntu MATE Welcome, which can be started as follows:"),
                    '<code>ubuntu-mate-welcome --boutique</code>'
                ],
                [
                    [_("Close"), "exit", "dialog-theme"]
                ]
            )
            return

        # Check Boutique is running on a supported version / architecture
        if boutique.system_arch not in supported_arch:
            self.show_message_page("other-distro.png", _("Unsupported Architecture"),
                                    [
                                        _("Sorry, Software Boutique cannot continue as it is not compatible with the {0} architecture.").replace(
                                            "{0}", boutique.system_arch)
                                    ],
                                    [
                                        [_("Visit Community"), "web?" + support_url, "dialog-theme green"],
                                        [_("Quit"), "exit", "dialog-theme inverted"]
                                    ]
                                  )
            return

        if boutique.current_os_codename not in supported_codenames and not pref.read("already-warned-distro", False):
            self.show_message_page("other-distro.png", _("Unsupported Version"),
                                    [
                                        _("Software Boutique has detected your system is running {version}, " \
                                        "but the Ubuntu MATE team have not tested our application picks for this version.").replace(
                                            "{version}", "<b>" + boutique.current_os_codename.title() + "</b>"),
                                        _("The software you see will be based on {version}'s listings, however, we cannot guarantee stability or compatibility.").replace(
                                            "{version}", default_codename.title())
                                    ],
                                    [
                                        [_("Continue"), "dismiss-unsupported-msg", "dialog-theme green"],
                                        [_("Quit"), "exit", "dialog-theme inverted"]
                                    ]
                                  )

        # Load application
        try:
            # Determine Environment
            dbg.stdout("Now starting application.", dbg.action, 1)
            self.is_ubuntu_mate = boutique.SoftwareInstallation.PackageKit._is_running_ubuntu_mate()
            self.boutique_ppa_enabled = boutique.SoftwareInstallation.PackageKit._is_boutique_subscribed()

            # Assume we're using Ubuntu MATE unless the metapackages are not present.
            if not self.is_ubuntu_mate and not pref.read("already-warned-distro", False):
                self.show_message_page("other-distro.png", _("Unsupported Distribution"),
                                        [
                                            _("Software Boutique is designed for software tested on Ubuntu MATE, " \
                                              "however a different distribution was detected."),

                                            _("While a large selection of software will work on other Ubuntu-based " \
                                              "distributions, we cannot guarantee our featured picks will work as intended " \
                                              "on your system."),

                                            _("Thank you for choosing Software Boutique.")
                                        ],
                                        [
                                            ["Continue", "dismiss-unsupported-msg", "dialog-theme inverted"]
                                        ]
                                      )

            # Adapt header/footer to current theme.
            self._set_navigation_theme()

            # Introduce user on first run.
            if not pref.read("introduced", False):
                self.show_introduction_page()

            # Prepare user cache folder
            pref.init_cache()

            ############################################
            # Index Cache
            ############################################
            def _update_loading_status(string):
                self.update_page("#loading-text", "html", string)
                dbg.stdout(string, dbg.action, 1)

            def _download_latest_index():
                _update_loading_status(_("Downloading Index..."))

                # Local Paths
                path_index = pref.folder_cache + "/index.json"
                path_index_tmp = pref.folder_cache + "/index.json.gz"
                path_metadata = pref.folder_cache + "/metadata/"
                path_metadata_tmp = pref.folder_cache + "/metadata.tar.xz"

                # Remote URLs
                index_locale_url = index_url + "/applications-{0}.json.gz"
                index_fallback_url = index_url + "/applications-en.json.gz"
                metadata_url = index_url + "/application-metadata.tar.xz"

                # (1) Download index (JSON)
                # -- Tries localised first, then fallbacks to English.
                try:
                    dbg.stdout("Trying localised index URL: " + index_locale_url, dbg.debug, 1)
                    r = requests.get(index_locale_url)
                    if r.status_code != 200:
                        dbg.stdout("Trying fallback URL:        " + index_fallback_url, dbg.debug, 1)
                        r = requests.get(index_fallback_url)
                        if r.status_code != 200:
                            _show_connection_error(r.status_code)
                            return

                    # (2) Save compressed data
                    dbg.stdout("Successfully got {0} bytes in {1} seconds.".format(r.headers["Content-Length"], r.elapsed.total_seconds()), dbg.success, 1)
                    with open(path_index_tmp, 'wb') as f:
                        for chunk in r:
                            f.write(chunk)

                except Exception as e:
                    dbg.stdout("Failed to download index. Details: " + str(e), dbg.error)
                    _show_connection_error(str(e))
                    return

                # (3) Get application metadata
                try:
                    dbg.stdout("Getting metadata from URL: " + metadata_url, dbg.debug, 1)
                    r = requests.get(metadata_url)
                    if r.status_code != 200:
                        _show_connection_error(r.status_code)
                        return

                    # (4) Save compressed data
                    dbg.stdout("Successfully got {0} bytes in {1} seconds.".format(r.headers["Content-Length"], r.elapsed.total_seconds()), dbg.success, 1)
                    with open(path_metadata_tmp, 'wb') as f:
                        for chunk in r:
                            f.write(chunk)

                except Exception as e:
                    dbg.stdout("Failed to download metadata. Details: " + str(e), dbg.error)
                    _show_connection_error(str(e))
                    return

                # (5) Verify data integrity
                #_update_loading_status(_("Verifying Index..."))
                # TODO: Check signature
                # TODO: Check checksums

                # (6) Decompress index and metadata
                _update_loading_status(_("Extracting Index..."))
                try:
                    # --> Index
                    with gzip.open(path_index_tmp, 'rb') as f:
                        with open(path_index, 'wb') as o:
                            o.write(f.read())

                    if os.path.exists(path_index_tmp):
                        os.remove(path_index_tmp)
                        dbg.stdout("Successfully extracted index.", dbg.success, 1)
                    else:
                        _failed_to_process_index()

                    # --> Metadata
                    with tarfile.open(path_metadata_tmp) as f:
                        f.extractall(path_metadata)

                    if os.path.exists(path_metadata_tmp):
                        os.remove(path_metadata_tmp)
                        dbg.stdout("Successfully extracted metadata.", dbg.success, 1)
                    else:
                        _failed_to_process_index()

                except Exception as e:
                    dbg.stdout("Failed to extract files! Exception: " + str(e), dbg.error)
                    _failed_to_process_index()

                # Update complete!
                dbg.stdout("Successfully cached latest index.", dbg.success, 1)

            def _show_connection_error(status_code):
                dbg.stdout("Failed to establish a connection!", dbg.error)
                if status_code:
                    if type(status_code) is str:
                        dbg.stdout("Exception: " + status_code, dbg.error)
                    elif type(status_code) is int:
                        dbg.stdout("Status Code: " + str(status_code), dbg.error)

                self.show_message_page("connection-error.png", _("Could not connect to server"),
                    [
                        _("Software Boutique could not connect to the index server to retrieve the list of applications."),
                        _("Please check your connection, that the index server is up, or try again later.")],
                    [
                        [_("Retry"), "init-boutique", "dialog-theme green"],
                        [_("Close"), "exit", "dialog-theme"]
                    ])
                exit(0)

            def _failed_to_process_index():
                self.show_message_page("index-error.png", _("Failed to process index"),
                    [
                        _("Software Boutique encountered an internal issue extracting or reading the index."),
                        _("It is possible the index is corrupted, please try again.")],
                    [
                        [_("Retry"), "force-cache-reset", "dialog-theme green"],
                        [_("Close"), "exit", "dialog-theme"]
                    ])
                exit(0)

            # Do we have the latest revision?
            _update_loading_status(_("Initalizing Index..."))
            local_revision = pref.get_index_revision()
            dbg.stdout("Local Revision: " + str(local_revision), dbg.debug, 1)
            dbg.stdout("Checking for latest version from: " + index_url, dbg.debug, 1)
            r = requests.get(index_url + "/latest_revision")
            if r.status_code == 200:
                dbg.stdout("Successfully got {0} bytes in {1} seconds.".format(r.headers["Content-Length"], r.elapsed.total_seconds()), dbg.success, 1)
                latest_revision = int(r.text)
                dbg.stdout("Remote Revision: " + str(latest_revision), dbg.debug, 1)
                if local_revision < latest_revision:
                    # Local index outdated or non-existant.
                    _download_latest_index()
                    pref.set_index_revision(r.text)
                else:
                    dbg.stdout("Local index up-to-date", dbg.success, 1)
            else:
                _show_connection_error(None)
                return

            ############################################
            # Load the Index
            ############################################
            _update_loading_status(_("Loading Index..."))
            boutique.screenshot_file_listing = os.listdir(os.path.join(pref.folder_cache, "metadata", "screenshots"))
            index_path = os.path.join(pref.folder_cache, "index.json")

            # Load index (raw data) into memory
            try:
                self.index = boutique.read_index(index_path)
                dbg.stdout("Successfully loaded software index.", dbg.success, 1)
            except Exception as e:
                dbg.stdout("Software Index corrupt or missing, Boutique cannot continue.", dbg.error, 0)
                _failed_to_process_index()

            ############################################
            # Stock the Boutique Categories & Apps
            ############################################
            self.queue = QueueOperationsThread(self.update_page)
            installed_apps = []

            dbg.stdout("Populating categories...", dbg.action, 2)
            self.all_categories = [
                ["start-page", _("Start Page")],
                ["accessories", _("Accessories")],
                ["education", _("Education")],
                ["games", _("Games")],
                ["graphics", _("Graphics")],
                ["internet", _("Internet")],
                ["office", _("Office")],
                ["development", _("Programming")],
                ["multimedia", _("Sound & Video")],
                ["system", _("System Tools")],
                ["accessibility", _("Universal Access")],
                ["server", _("Servers")],
                ["more-software", _("More Software")],
                ["fixes", _("Fixes")]
            ]

            for category in self.all_categories:
                self._append_category(category[0], category[1])

            dbg.stdout("Finished populating categories.", dbg.success, 2)

            # Set default starting category
            self.update_page("#current-category-icon", "attr", "src", data_source + "/categories/accessories.svg")

            ############################################
            # Push translated strings
            ############################################
            # Header Buttons
            self.update_page("#browse-button", "append", _("Browse"))
            self.update_page("#installed-button", "append", _("Installed"))
            self.update_page("#queue-button", "append", _("Queue") + "&nbsp;(<span id='queued-items'>0</span>)")
            self.update_page("#news-button", "append", _("What's New?"))

            # Header Tooltips
            self.update_page("#change-category-button", "attr", "title", _("Choose another category"))
            self.update_page("#browse-button", "attr", "title", _("Explore a variety of hand-picked software"))
            self.update_page("#installed-button", "attr", "title", _("Lists software installed via Software Boutique"))
            self.update_page("#queue-button", "attr", "title", _("Lists software with pending changes"))
            self.update_page("#news-button", "attr", "title", _("See what changes have been made to the software selection"))
            self.update_page("#search-button", "attr", "title", _("Search Boutique"))
            self.update_page("#settings-button", "attr", "title", _("Settings"))
            self.update_page("#scroll-top", "attr", "title", _("Top of Page"))

            # Page Titles
            self.update_page("#browse-button", "attr", "data-title", _("Browse"))
            self.update_page("#installed-button", "attr", "data-title", _("Installed Software"))
            self.update_page("#queue-button", "attr", "data-title", _("Queued Changes"))
            self.update_page("#news-button", "attr", "data-title", _("What's New?"))
            self.update_page("#search-button", "attr", "data-title", _("Search"))
            self.update_page("#settings-button", "attr", "data-title", _("Settings"))

            ############################################
            # Populate Boutique News
            ############################################
            # fixme: news page

            ############################################
            # Populate Installed Page
            ############################################
            # fixme: use global function

            ############################################
            # Build Settings Page
            ############################################
            self.build_settings_page()

            ############################################
            # Start Page
            ############################################
            self.generate_start_page_apps()
            self.update_page("#welcome-text", "html", _("Welcome to the Software Boutique"))
            self.update_page("#welcome-description", "html", _("There is an abundance of software available for Ubuntu MATE" \
                " and some people find that choice overwhelming. The Boutique is a"
                " carefully curated selection of the best-in-class applications chosen because they"
                " integrate well, complement Ubuntu MATE and enable you to self style your computing experience.")
            )

            html = ""
            for category in self.all_categories:
                if category[0] in ["start-page", "fixes", "more-software"]:
                    continue
                else:
                    html += self._generate_category_html(category[0], category[1])

            self.update_page("#start-page-categories", "append", html)

            self.update_page("#start-page-fixes", "prepend", self._generate_category_html("fixes", _("Fixes")))
            self.update_page("#start-page-fixes h4", "html", _("Something not working?"))
            self.update_page("#start-page-fixes span", "html",
                _("Just in case something goes wrong when updating or installing new software, we've put together" \
                " some one-click fixes that usually solve most common problems.")
            )

            self.update_page("#start-page-more-software", "prepend", self._generate_category_html("more-software", _("More Software")))
            self.update_page("#start-page-more-software h4", "html", _("Can't find what you're looking for?"))
            self.update_page("#start-page-more-software span", "html",
                _("As the Boutique is just a curvated collection of software, not everything is listed here. Install" \
                " a full-fledged software center to explore the entire Ubuntu catalogue.")
            )

            ############################################
            # Queue Monitor and Installation
            ############################################
            spawn_thread(self.queue.watch_queue)
            self.queue.update_queue_state()
            self.update_page("#queue-status-text", "html", _("Ready"))

            ############################################
            # Load Finished
            ############################################
            self.run_js("smoothFade('#loading-page', '#browse-page')")
            self.run_js("changeCategory('start-page', '" + _("Start Page") + "')")
            self.run_js("changeNavTitleType('browse')")
            self.update_page("#header .left", "fadeIn")
            self.update_page("#header .right", "fadeIn")
            self.update_page("#footer .left", "fadeIn")
            #~ self.update_page("#footer .right", "fadeIn")

        except Exception:
            details = traceback.format_exc()
            dbg.stdout("----------------------------------------", dbg.error, 0)
            dbg.stdout(_("Software Boutique failed to start!"), dbg.error, 0)
            dbg.stdout(_("Please file a bug report with this stacktrace to help resolve the problem."), dbg.error, 0)
            dbg.stdout("----------------------------------------", dbg.error, 0)
            print(details)
            dbg.stdout("----------------------------------------", dbg.error, 0)
            self.show_message_page("serious-error.png", _("Something went wrong."),
                                    [
                                        _("Sorry! Software Boutique could not be started due to a serious problem."),
                                        _("Please report this to the Ubuntu MATE Team:"),
                                        "<pre>" + details.replace('\n', '<br>') + "</pre>"
                                    ],
                                    [
                                        ["Retry", "force-boutique-restart", "dialog-theme"],
                                        ["Quit", "exit", "dialog-theme"]
                                    ]
                                  )

    def _generate_html_app_card(self, app_obj):
        """
        Generates the HTML for an "app card", seen when browsing lists.

        app_obj = boutique.get_application_details object
        """
        installed = app_obj.installation.is_installed()

        html = "<div class='app-card {installed_class} {app_uuid}'>" \
                        "<i class='fa fa-check-circle fa-2x install-checkmark' {show_if_installed} ></i>" \
                        "<img class='icon' src='{icon}' />" \
                        "<div class='title'>{name}</div>" \
                        "<div class='summary'>{summary}</div>" \
                        "<div class='actions'>{details_button} {install_buttons}</div>" \
                    "</div>".format(
                        app_uuid = app_obj.categoryid + "-" + app_obj.appid,
                        icon = app_obj.icon_path,
                        name = app_obj.name,
                        summary = app_obj.summary,
                        show_if_installed = "style='display:none'" if not installed else "",
                        hide_if_installed = "style='display:none'" if installed else "",
                        installed_class = "installed" if installed else "",
                        details_button = boutique.print_more_details_button(app_obj),
                        install_buttons = boutique.print_app_installation_buttons(app_obj, self.queue.queue)
                    )

        return(html)

    def _generate_category_html(self, internal_name, human_name):
        """
        Generates the HTML for category "card" buttons.

        internal_name   =   Category name in index. e.g. accessories
        human_name      =   Nice name for the user. e.g. Accessories
        """
        safe_human_name = make_html_safe(human_name)
        html = "<div id=\"category-{1}\" class=\"category\">" + \
                 "<a onclick=\"changeCategory('{1}', '{2}')\">" + \
                   "<div class=\"option\"><img src=\"{0}/categories/{1}.svg\"/> <label>{2}</label></div>" + \
                 "</a>" + \
               "</div>"
        html = html.replace('{0}', data_source).replace('{1}', internal_name).replace('{2}', safe_human_name)
        return html

    def _append_category(self, internal_name, human_name):
        """
        Generates and appends the HTML for populating category listings.

        internal_name   =   Category name in index. e.g. accessories
        human_name      =   Nice name for the user. e.g. Accessories
        """
        # Adds to the categories selection page
        html = self._generate_category_html(internal_name, human_name)
        safe_human_name = make_html_safe(human_name)
        self.update_page("#categories-container", "append", html)

        # Skip special categories (no apps are listed here)
        if internal_name in ["start-page", "fixes", "unlisted"]:
            return

        # Begin building category page
        browse_html = "<div id='category-page-{0}' class='category-contents' hidden>".format(internal_name)
        browse_html += "<h1>{0}</h1>".format(safe_human_name)

        # Generate application "cards"
        appids = self.index.get(internal_name).keys()
        ordered_appids = []
        for appid in appids:
            ordered_appids.append(appid)

        ordered_appids.sort()
        for appid in ordered_appids:
            app = boutique.get_application_details(self.index, internal_name, appid)
            browse_html += self._generate_html_app_card(app)

        # Append to actual category
        browse_html += "</div>"
        self.update_page("#browse-page", "append", browse_html)

    def show_message_page(self, icon_name, title, body_lines, buttons):
        """
        Shows an overlay message for providing important details.

        icon_name   = (str)     Filename in "img" folder.
        title       = (str)     Page title.
        body_lines  = (list)    Each new line of text.
        buttons     = (group)   {"button1-label": "function1", "button2-label": "function2"}
        """
        self.update_page("#message-page .left img", "attr", "src", os.path.join(data_source, "img", icon_name))
        self.update_page("#message-page .right", "html", " ")
        self.update_page("#message-page .right", "append", "<h2>" + title + "</h2>")
        for line in body_lines:
            self.update_page("#message-page .right", "append", "<p>" + line + "</p>")
        for button in buttons:
            label = button[0]
            onclick = button[1]
            classname = button[2]
            self.update_page("#message-page .right", "append", "<button class='{0}' onclick='cmd(\"{1}\")'>{2}</button>".format(classname, onclick, label))
        self.update_page("#message-page", "fadeIn")
        self.update_page(".navigation", "addClass", "disabled")
        self.update_page("#change-category-button", "addClass", "disabled")

    def close_message_page(self):
        self.update_page("#message-page", "fadeOut")
        self.update_page(".navigation", "removeClass", "disabled")
        self.update_page("#change-category-button", "removeClass", "disabled")

    def show_introduction_page(self):
        self.update_page("#firstrun-page", "fadeIn", "fast")
        self.update_page(".navigation", "addClass", "disabled")
        self.update_page("#change-category-button", "addClass", "disabled")
        self.update_page("#firstrun-page1 .text", "html", _("There is an abundance of software avaliable for your computer. That's an overwhelming choice for many users."))
        self.update_page("#firstrun-page2 .text", "html", _("Software Boutique aims to collate the best-in-class applications that complement your Ubuntu MATE experience that have been tested and integrate well."))
        self.update_page("#firstrun-page3 .text", "html", _("Not everything is listed here. If you can't find what you're looking for, install a software center to explore the complete Ubuntu catalogue."))
        self.update_page("#firstrun-prev", "html", _("Previous"))
        self.update_page("#firstrun-skip", "html", _("Skip"))
        self.update_page("#firstrun-next", "html", _("Next"))
        self.update_page("#firstrun-start", "html", _("Begin"))

    def close_introduction_page(self):
        pref.write("introduced", True)
        self.update_page("#firstrun-page", "fadeOut", "fast")
        self.update_page(".navigation", "removeClass", "disabled")
        self.update_page("#change-category-button", "removeClass", "disabled")

    def _set_navigation_theme(self):
        """
        Adjusts UI colours based on the current theme.
        """
        window = Gtk.Window()
        style_context = window.get_style_context()

        def _rgba_to_hex(color):
           """
           Return hexadecimal string for :class:`Gdk.RGBA` `color`.
           """
           return "#{0:02x}{1:02x}{2:02x}".format(
                                            int(color.red   * 255),
                                            int(color.green * 255),
                                            int(color.blue  * 255))

        def _get_color(style_context, preferred_color, fallback_color):
            color = _rgba_to_hex(style_context.lookup_color(preferred_color)[1])
            if color == '#000000':
                color = _rgba_to_hex(style_context.lookup_color(fallback_color)[1])
            return color

        bg_color = _get_color(style_context, 'dark_bg_color', 'theme_bg_color')
        fg_color = _get_color(style_context, 'dark_fg_color', 'theme_fg_color')
        selected_bg_color = _get_color(style_context, 'selected_bg_color', '#667E40')

        self.update_page('#header', 'css', 'background-color', bg_color)
        self.update_page('#footer', 'css', 'background-color', bg_color)
        self.update_page('#header', 'css', 'color', fg_color)
        self.update_page('#footer', 'css', 'color', fg_color)
        self.update_page('.navigation', 'css', 'color', fg_color)
        self.update_page('.dropdown', 'css', 'color', fg_color)

    def show_app_details(self, categoryid, appid, update_only=False):
        """
        Builds (or replaces) the app details page when an app is clicked.
        """
        app_obj = boutique.get_application_details(self.index, categoryid, appid)
        self.current_open_app_uuid = app_obj.uuid

        if not update_only:
            self.run_js("animate('#app-details-page', 'enter-more-info', 'in')")
            self.run_js("changeNavTitleType('back')")
            self.update_page(".header-title", "html", app_obj.name)

        installed = app_obj.is_installed()

        # Start top section
        html = "<div id='app-overview'>"
        html += "<div class='left'>"
        if installed:
            html += "<i class='fa fa-check-circle fa-2x install-checkmark'></i>"
        html += "<img id='app-icon' src='{0}'/>".format(app_obj.icon_path)
        html += "</div><div class='right'>"

        ### Screenshots
        html += "<div id='app-screenshots'>"
        if len(app_obj.screenshot_filenames) == 0:
            html += "<div id='screenshot-empty'><var>{0}</var></div>".format(_("No screenshot available"))

        else:
            path = os.path.join(boutique.cache_path, "metadata", "screenshots", app_obj.screenshot_filenames[0])
            html += "<button href='{0}' data-fancybox='gallery' id='screenshot-preview'><img src='{0}'/></button>".format(path)

        if len(app_obj.screenshot_filenames) >= 2:
            html += "<hr/>"
            for filename in app_obj.screenshot_filenames:
                path = os.path.join(boutique.cache_path, "metadata", "screenshots", filename)
                html += "<button href='{0}' data-fancybox='gallery' class='screenshot-chooser'><img src='{0}'/></button>".format(path)
        html += "</div>"

        ### App Details
        html += "<h2 id='app-name'>{name}</h2>" \
                "<button id='app-developer' class='link' onclick='cmd(\"web?{dev_url}\")'>{dev_name}</button>" \
                "<p id='app-description'>{description}</p>".format(
                    name = app_obj.name,
                    dev_name = app_obj.developer_name,
                    dev_url = app_obj.developer_url,
                    website_url = app_obj.urls.get("info"),
                    description = app_obj.description)

        ### Alternate to
        if app_obj.alternate_to:
            html += "<p id='app-alternate'>" + app_obj.alternate_to + "</p>"

        ### Show a checkmark / date if installed.
        def _update_install_info():
            """
            Prints a small piece of text showing when the app was installed.
            """
            current_time = int(time.time())
            installed_time = install_time.read(app_obj.uuid, 0)

            if pref.read("prefer-precise-time", False):
                # TODO: Improve date format
                time_string = (datetime.fromtimestamp(installed_time).ctime())

            else:
                if installed_time == 0:
                    time_string = _("Installed.")

                elif installed_time > current_time:
                    time_string = _("A wicked time traveller claims this will be installed in the future.")

                elif current_time - 300 < installed_time:       # Past 5 minutes
                    time_string = _("Installed just now.")

                elif current_time - 3600 < installed_time:      # Past hour
                    text = _("Installed [0] minutes ago.")
                    text = text.replace("[0]", str(int((current_time - installed_time) / 60)))
                    time_string = text

                elif current_time - 86400 < installed_time:     # Past week
                    text = _("Installed [0] hours ago.")
                    text = text.replace("[0]", str(int((current_time - installed_time) / 60 / 60)))
                    time_string = text

                elif current_time - 604800 < installed_time:    # Past month
                    text = _("Installed [0] days ago.")
                    text = text.replace("[0]", str(int((current_time - installed_time) / 60 / 60 / 24)))
                    time_string = text

                else:
                    # TODO: Improve date format
                    time_string = datetime.fromtimestamp(installed_time).ctime()

            return "<div id='app-install-info'><span class='fa fa-check-circle'></span> " + time_string + "</div>"

        if installed:
            html += _update_install_info()

        ### Action Buttons
        html += boutique.print_app_installation_buttons(app_obj, self.queue.queue)

        ### End top section
        html += "</div><hr/>"

        ### Start table section
        html += "<table>"

        ### License
        label = _("License")
        if app_obj.proprietary:
            data = _("Proprietary")
        else:
            data = _("Open Source")

        html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ### Supported Platforms
        def _print_arch_label(arch, use_img_file, icon_name, label, tooltip):
            if tooltip:
                tooltip = "title='{0}'".format(tooltip)
            else:
                tooltip = ""
            if use_img_file:
                img_path = os.path.join(data_source, "img", icon_name)
                this = "<div class='platform' {2}><img class='like-fa' src='{0}'/> {1}</div>".format(img_path, label, tooltip)
            else:
                this = "<div class='platform' {2}><i class='fa fa-{0}'/> {1}</div>".format(icon_name, label, tooltip)

            if arch == boutique.system_arch:
                return("<div class='current-arch'>" + this + "</div>")
            else:
                return(this)

        label = _("Platform")
        data = ""
        for arch in app_obj.arch:
            if arch == "i386":
                data += _print_arch_label(arch, False, "laptop", _("32-bit"), _("i386 / x86"))

            elif arch == "amd64":
                data += _print_arch_label(arch, False, "laptop", _("64-bit"), _("amd64 / x86_64"))

            elif arch == "armhf":
                data += _print_arch_label(arch, True, "rpi.png", _("Raspberry Pi and ARM devices"), ("armhf"))

            elif arch == "arm64":
                data += _print_arch_label(arch, False, "laptop", _("ARM64"), None)

            elif arch == "powerpc":
                data += _print_arch_label(arch, False, "desktop", _("PowerPC"), None)

            elif arch == "ppc64el":
                data += _print_arch_label(arch, False, "desktop", _("PowerPC 64-bit"), None)

            else:
                data += _print_arch_label(arch, False, "microchip", arch, None)

        try:
            url = app_obj.urls["android-app"]
            if url:
                data += "<button class='link' onclick='cmd(\"web?{2}\")'><div class='platform'><i class='fa fa-{0}'/> {1}</div></button>".format("android", "Android", url)
        except Exception:
            # Not required.
            pass

        try:
            url = app_obj.urls["ios-app"]
            if url:
                data += "<button class='link' onclick='cmd(\"web?{2}\")'><div class='platform'><i class='fa fa-{0}'/> {1}</div></button>".format("apple", "iOS", url)
        except Exception:
            # Not required.
            pass

        html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ## Tags
        label = _("Tags")
        data = ""
        for tag in app_obj.tags:
            data += "<div class='tag'>" + tag + "</div>"
        html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ## Website Info
        label = _("Website")
        data = "<button class='link' onclick='cmd(\"web?{0}\")'>{0}</button>".format(app_obj.urls["info"])
        html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ## Application Type (Advanced only)
        if self.advanced_mode:
            label = _("Type")
            if app_obj.method == "dummy":
                data = "<span class='fa fa-{0}'></span> {1}".format("cogs", _("Dummy Application"))
            elif app_obj.method == "apt":
                data = "<span class='fa fa-{0}'></span> {1}".format("cube", _("Debian Packaged Application"))
            elif app_obj.method == "snap":
                data = "<span class='fa fa-{0}'></span> {1}".format("puzzle-piece", _("Snap Application"))
            elif app_obj.method == "web":
                data = "<span class='fa fa-{0}'></span> {1}".format("globe", _("Web Application"))
            else:
                data = "<span class='fa fa-{0}'></span> {1}".format("question-circle", _("Unknown"))
            html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ### Sources
        if app_obj.method == "apt" and not boutique.force_dummy:
            label = _("Source")
            grp_data = app_obj.installation._get_instructions_for_this_codename()
            source = grp_data["source"]

            ubuntu_logo = os.path.join(data_source, "img", "ubuntu.png")
            canonical_logo = os.path.join(data_source, "img", "ubuntu.png")

            if source == "main":
                data = "<img class='like-fa' src='{0}'> {1}".format(ubuntu_logo, _("Ubuntu 'Main' Repository"))

            elif source == "universe":
                data = "<img class='like-fa' src='{0}'> {1}".format(ubuntu_logo, _("Ubuntu 'Universe' Repository"))

            elif source == "restricted":
                data = "<img class='like-fa' src='{0}'> {1}".format(ubuntu_logo, _("Ubuntu 'Restricted' Repository"))

            elif source == "multiverse":
                data = "<img class='like-fa' src='{0}'> {1}".format(ubuntu_logo, _("Ubuntu 'Multiverse' Repository"))

            elif source == "partner":
                data = "<img class='like-fa' src='{0}'> {1}".format(canonical_logo, _("Canonical Partner Repository"))

            elif source.startswith("ppa"):
                repo_author = source.split("ppa:")[1].split("/")[0]
                repo_name = source.split("ppa:")[1].split("/")[1]
                url = "https://launchpad.net/~{0}/+archive/ubuntu/{1}".format(repo_author, repo_name)
                data = "<span class='fa fa-{0}'></span> <button class='link' onclick='cmd(\"web?{2}\")'>{1}</button>".format("cube", source, url)

            elif source == "manual":
                try:
                    url = grp_data["list-key-url"]
                except Exception:
                    url = grp_data["list-key-server"][0]

                data = "<span class='fa fa-{0}'></span> {1}".format("globe", url)

            else:
                # It would be ironic if this happened.
                data = ""

            html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ## Package Listings (Advanced and Apt only)
        if self.advanced_mode:
            if app_obj.method == "apt" and not boutique.force_dummy:
                label = _("To be installed")
                data = ""
                for package in app_obj.installation._get_package_list("install"):
                    data += "<div class='tag'>" + package + "</div>"
                html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

                label = _("To be removed")
                data = ""
                for package in app_obj.installation._get_package_list("remove"):
                    data += "<div class='tag'>" + package + "</div>"
                html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ### Launch Command (Advanced only)
        if self.advanced_mode and app_obj.launch_cmd:
            label = _("Launch Command")
            data = "<code>" + app_obj.launch_cmd + "</code>"
            html += "<tr><th>{0}</th><td>{1}</td></tr>".format(label, data)

        ### End table section
        html += "</table>"

        self.update_page("#app-details-page", "html", html)

    def hide_app_details(self):
        self.run_js("animate('#app-details-page', 'exit-more-info', 'out')")
        self.run_js("changeNavTitleType('browse')")

    def generate_start_page_apps(self):
        def _add_app(random_app, position, direction):
            categoryid = random_app[0]
            appid = random_app[1]
            app_obj = boutique.get_application_details(self.index, categoryid, appid)
            cmd = "cmd(\"{0}?{1}?{2}\")".format("details", app_obj.categoryid, app_obj.appid)
            return "<button id='random-{4}-{3}' onclick='{2}' title='{0}'><img style='opacity:0.{3}' src='{1}'/></button>".format(app_obj.name, app_obj.icon_path, cmd, position, direction)

        html = ""
        random_apps = []
        data_source = get_data_source()

        for category in ["accessories", "server", "games", "system", "multimedia", "internet",
                         "development", "graphics", "office", "education", "more-software", "accessibility"]:
            apps = list(self.index[category].keys())
            random_apps.append([category, random.choice(apps)])
        random.shuffle(random_apps)

        html += _add_app(random_apps[0], 1, "left")
        html += _add_app(random_apps[1], 2, "left")
        html += _add_app(random_apps[2], 3, "left")
        html += _add_app(random_apps[3], 4, "left")
        html += _add_app(random_apps[4], 5, "left")
        html += _add_app(random_apps[5], 6, "left")
        html +=  "<img id='welcome-logo' src='{0}'>".format(data_source + "img/ubuntu-mate-logo.svg")
        html += _add_app(random_apps[6], 6, "right")
        html += _add_app(random_apps[7], 5, "right")
        html += _add_app(random_apps[8], 4, "right")
        html += _add_app(random_apps[9], 3, "right")
        html += _add_app(random_apps[10], 2, "right")
        html += _add_app(random_apps[11], 1, "right")
        self.update_page("#random-apps", "html", html)

    def build_settings_page(self):
        """
        Generates and builds the settings screen.
        """
        html = "<h2>{0}</h2><p><{1}</p>".format(
            _("Preferences"),
            _("Tune the Software Boutique to suit your preferred behaviors.")
        )

        def _get_onclick_toggle(prop):
            return 'cmd("toggle-pref?{0}")'.format(prop)

        def _get_initial_check_state(prop):
            if pref.read(prop, False):
                return "checked"
            else:
                return ""

        def add_category(name):
            return "<div class='categpry'><hr/>" \
                    "<div class='left'>" \
                    "<h4>" + name + "</h4>" \
                    "</div>" \
                    "<div class='right'>"

        def add_checkbox(label, prop):
            return "<label><input type='checkbox' onclick='{1}' {2}/> {0}</label>".format(
            label, _get_onclick_toggle(prop), _get_initial_check_state(prop))

        def add_button(label, command, fa_icon=None):
            if fa_icon:
                icon = "<span class='fa {0}'></span>".format(fa_icon)
            else:
                icon = ""
            return "<button class='dialog-theme' onclick='cmd(\"{0}\")'>{1} {2}</button>".format(
                command, icon, label)

        def add_label(label):
            return "<label>" + label + "</label>"

        def add_hint(label):
            return "<label class='hint'>" + label + "</label>"

        def end_category():
            return "</div></div>"

        # Version Information
        revision = 0
        html += add_category(_("Version"))
        html += add_hint(__VERSION__)
        html += add_checkbox(_("Keep Software Boutique up-to-date."), "keep-updated")
        html += end_category()

        html += add_category(_("Index Revision"))
        html += add_hint(str(pref.get_index_revision()))
        html += add_button(_("Force Update"), "force-cache-reset", "fa-refresh")
        html += end_category()

        html += add_category(_("Interface"))
        html += add_checkbox(_("Hide proprietary applications"), "always-hide-non-free")
        html += add_checkbox(_("Enable Advanced Views"), "advanced-mode")
        html += add_hint(_("Reveal more technical details about applications and advanced filters."))
        html += add_checkbox(_("Prefer precise times"), "prefer-precise-time")
        html += add_hint(_("Always shows the exact time it was installed, as opposed to relative, e.g. '2 days ago'."))
        html += end_category()

        html += add_category(_("Miscellaneous"))
        html += add_button(_("Show Introduction Screen"), "show-introduction-again", "fa-info-circle")
        html += end_category()

        self.update_page("#settings-page", "html", html)


class QueueOperationsThread(object):
    """
    Thread that monitors the queue and performs Boutique operations.

    The queue is a list, each item is a list describing the desired operation, e.g:

    ** For queued items:
    [<operation>, <object>]

    0   operation   str     Describes what to do with this object.
                              - "install"
                              - "remove"

    1   object      obj     ApplicationData() object.

    ** For finished items:
    [<operation>, <object>, <successful>]

    This list has an extra boolean to indicate if it was successful or not.

    """
    def __init__(self, update_page_obj):
        self.update_page = update_page_obj
        self.queue = []
        self.finished_queue = []
        self.status_text = _("Ready")

    def watch_queue(self):
        """
        Looped function that monitors the queue and performs operations.
        """
        while True:
            time.sleep(1)
            if len(self.queue) > 0:
                queue_item = self.queue[0]
                operation = queue_item[0]
                app_obj = queue_item[1]

                boutique.ui_callback.current_app_uuid = app_obj.uuid
                boutique.ui_callback.operation = operation

                self.update_page(".queue-ui-element", "hide")
                dbg.stdout("Processing queue item: " + app_obj.name, dbg.action, 2)
                self.update_page("#queue-status-text", "hide")
                self.update_page("#queue-inprogress", "fadeIn", "fast")
                self.update_page("#queue-status-text", "fadeIn", "fast")

                if operation == "install":
                    self.update_page("#queue-status-text", "html", _("Installing {0}").replace('{0}', app_obj.name))
                    result = app_obj.installation.do_install()

                elif operation == "remove":
                    self.update_page("#queue-status-text", "html", _("Removing {0}").replace('{0}', app_obj.name))
                    result = app_obj.installation.do_remove()

                dbg.stdout("Finished queue item: {0} (Result: {1})".format(app_obj.name, str(result)), dbg.success, 2)
                self.send_desktop_notification(app_obj, operation, result)
                self.finished_queue.append([operation, app_obj, result])

                self.update_page(".queue-ui-element", "hide")
                self.update_page("#queue-progress", "hide")
                self.update_page("#queue-status-text", "hide")
                self.update_page("#queue-progress-inner", "css", "width", "0%")
                if result == True:
                    self.update_page("#queue-success", "fadeIn", "fast")
                    self.update_page("#queue-status-text", "fadeIn", "fast")
                    if operation == "install":
                        self.update_page("#queue-status-text", "html", _("Successfully installed {0}").replace('{0}', app_obj.name))
                    elif operation == "remove":
                        self.update_page("#queue-status-text", "html", _("Successfully removed {0}").replace('{0}', app_obj.name))
                else:
                    self.update_page("#queue-failure", "fadeIn", "fast")
                    self.update_page("#queue-status-text", "fadeIn", "fast")
                    if operation == "install":
                        self.update_page("#queue-status-text", "html", _("Failed to install {0}").replace('{0}', app_obj.name))
                    elif operation == "remove":
                        self.update_page("#queue-status-text", "html", _("Failed to remove {0}").replace('{0}', app_obj.name))

                self.queue.pop(0)
                self.update_queue_state()
                if app_obj.uuid == app.current_open_app_uuid:
                    app.show_app_details(app_obj.categoryid, app_obj.appid, True)

                self.update_page(".buttons-" + boutique.ui_callback.current_app_uuid, "html", boutique.print_app_installation_buttons(app_obj, self.queue))

                dbg.stdout("{1} items completed. {0} items remaining.".format(str(len(self.queue)), str(len(self.finished_queue))), dbg.debug, 2)

    def get_count(self):
        """
        Returns the number of items in the queue.
        """
        return len(self.queue)

    def update_count(self):
        """
        Updates the queue counter in UI.
        """
        self.update_page("#queued-items", "html", str(self.get_count()))

    def _get_appdata(self, categoryid, appid):
        """
        Returns the ApplicationData() based on the "category-appname"
        """
        return boutique.get_application_details(app.index, categoryid, appid)

    def add_item(self, operation, app_object):
        """
        Adds an application to the queue.

        operation   str     Describes what to do.
        object      obj     ApplicationData() object.
        """
        self.queue.append([operation, app_object])
        self.update_count()
        self.ui_update_buttons(app_object)
        self.update_queue_state()
        dbg.stdout("Added item to queue: " + app_object.uuid, dbg.success, 1)

    def remove_item(self, app_uuid):
        """
        Removes a specific application from the queue.

        app_uuid    str     Value of appid provided in ApplicationData()
        """
        for index, item in enumerate(self.queue):
            try:
                if item[1].uuid == app_uuid:
                    self.queue.pop(index)
                    self.update_count()
                    self.ui_update_buttons(item[1])
                    dbg.stdout("Removed item from queue: " + app_uuid, dbg.action, 1)
                    self.update_queue_state()
                    return
            except:
                # This is not an ApplicationData() object.
                pass

    def is_already_queued(self, app_uuid):
        """
        Returns a boolean to indicate if the item is already in the queue.

        app_uuid    str     Value of appid provided in ApplicationData()
        """
        for item in self.queue:
            if item[1].uuid == app_uuid:
                dbg.stdout("Already in queue: " + app_uuid, dbg.error, 1)
                return True
        return False

    def ui_update_buttons(self, app_object):
        """
        Updates the buttons when changes are being made.

        app_object  obj     ApplicationData() object
        """
        self.update_page(".buttons-" + app_object.uuid, "html", boutique.print_app_installation_buttons(app_object, self.queue))

    def update_queue_state(self):
        """
        Updates the queue UI and re-generates the queue page.
        """
        self.update_count()
        self.update_page("#queue-page", "html", " ")
        html = ""

        # Nothing in the queue
        if len(self.queue) == 0 and len(self.finished_queue) == 0:
            strings = [
                _("There's no items queued."),
                _("Nothing queued, nothing to do!"),
                _("Pick some software that interests you, and it'll start installing in here!"),
                _("Nothing to see here!")
            ]
            html = "<div id='queue-empty'>{0}<br/><br/>{1}</div>".format(
                random.choice(strings),
                "<button class='dialog-theme' onclick='changeTab(\"browse\")'><img class='inverted' src='img/boutique-icon-mono.svg'/> " + _("Find Software") + "</button>"
            )
            self.update_page("#queue-page", "append", html)
            return

        # Pending Queue
        html += "<button class='clear dialog-theme' onclick='cmd(\"{cmd}\")'>{span} {var}</button>" \
                "<h3><span class='fa fa-clock-o'></span> {title}</h3>".format(
                    title = _("In Progress"),
                    cmd = "clear-queue-pending",
                    span = "<span class='fa fa-times'></span>",
                    var = _("Cancel All")
                )

        for item in self.queue:
            operation = item[0]
            app_obj = item[1]
            html += "<div id='{uuid}' class='queue-card'>" \
                        "<button onclick='cmd(\"{cmd_details}\")'>" \
                            "<img class='icon' src='{icon}' />" \
                            "<div class='title'>{name}</div>" \
                        "</button>" \
                        "<div class='status'>{status}</div>" \
                        "<button class='close' onclick='cmd(\"{cmd_cancel}\")' title='{close_tooltip}'><span class='fa fa-close'></span></div>" \
                    "</div>".format(
                        uuid = "queue-" + app_obj.uuid,
                        icon = app_obj.icon_path,
                        name = app_obj.name,
                        status = "Pending",
                        close_tooltip = _("Cancel"),
                        cmd_details = "details?{0}?{1}".format(app_obj.categoryid, app_obj.appid),
                        cmd_cancel = "drop-queue?{0}?{1}".format(app_obj.categoryid, app_obj.appid)
                    )
        html += "<br/>"

        # Finished Queue
        if len(self.finished_queue) > 0:
            html += "<button class='clear dialog-theme' onclick='cmd(\"{cmd}\")'>{span} {var}</button>" \
                    "<h3><span class='fa fa-check'></span> {title}</h3>".format(
                        title = _("Finished"),
                        cmd = "clear-queue-completed",
                        span = "<span class='fa fa-reorder'></span>",
                        var = _("Clear")
                    )

        for item in self.finished_queue:
            operation = item[0]
            app_obj = item[1]
            successful = item[2]

            if successful:
                result_css = "success"
                status = _("Successfully installed")
            else:
                result_css = "failed"
                status = _("Successfully removed")

            html += "<div id='{uuid}' class='queue-card queue-complete {operation} {result_css}'>" \
                        "<button onclick='cmd(\"{cmd_details}\")'>" \
                            "<img class='icon' src='{icon}' />" \
                            "<div class='title '>{name}</div>" \
                        "</button>" \
                        "<div class='status'>{status}</div>".format(
                        uuid = "queue-" + app_obj.uuid,
                        operation = operation,
                        icon = app_obj.icon_path,
                        name = app_obj.name,
                        status = status,
                        result_css = result_css,
                        cmd_details = "details?{0}?{1}".format(app_obj.categoryid, app_obj.appid),
                    )

            # Only show "Launch" button if installed.
            if operation == "install" or operation == "remove":
                html += "<button class='dialog-theme' onclick='cmd(\"{cmd_launch}\")'>{launch_text}</div>".format(
                            launch_text = _("Launch"),
                            cmd_launch = "launch?{0}?{1}".format(app_obj.categoryid, app_obj.appid)
                        )

            html += "</div>"


        # Finish queue list
        self.update_page("#queue-page", "append", html)

    def send_desktop_notification(self, app_obj, operation, successful):
        """
        Shows a notification on the user's desktop.

        app_obj     obj     ApplicationData() object
        operation   str     Operation name
                              - "install"
                              - "remove"
        successful  bln     True/False
        """
        def notify_send(title, subtitle):
            title = title.replace("{0}", app_obj.name)
            subtitle = subtitle.replace("{0}", app_obj.name)
            icon_path = os.path.join(data_source, app_obj.icon_path)
            try:
                Notify.init(title)
                notification = Notify.Notification.new(title, subtitle, icon_path)
                notification.show()
            except Exception as e:
                dbg.stdout("Could not send notification: " + str(e), dbg.error, 1)

        if operation == "install":
            if successful:
                notify_send(_("{0} successfully installed"), _("This application is ready to use."))
            else:
                notify_send(_("{0} failed to install"), _("There was a problem installing this application."))

        elif operation == "remove":
            if successful:
                notify_send(_("{0} removed"), _("This application has been uninstalled."))
            else:
                notify_send(_("{0} failed to remove"), _("There was a problem removing this application."))


def parse_parameters():
    global _
    parser = argparse.ArgumentParser(add_help=False)
    parser._optionals.title = _("Optional arguments")
    parser.add_argument("-h", "--help", help=_("Show this help message and exit"), action="help")
    #~ parser.add_argument("--version", help=_("Print progran version and exit"), action="store_true")
    parser.add_argument("-v", "--verbose", help=_("Be verbose to stdout"), action="store_true")
    parser.add_argument("--arch", help=_("Show listings for a specific architecture, e.g. i386"))
    parser.add_argument("--codename", help=_("Show listings for a specific release, e.g. xenial"))
    parser.add_argument("--locale", help=_("Force locale for interface"))
    parser.add_argument("--clear-cache", help=_("Force index re-download"), action="store_true")

    # For general debugging
    parser.add_argument("-vv", "-d", "--debug", help=_("Be very verbose (for debugging)"), action="store_true")

    # For front-end debugging (developer tools)
    parser.add_argument("--inspect", help=argparse.SUPPRESS, action="store_true")

    # For front-end debugging (use dummy module)
    parser.add_argument("--simulate", help=argparse.SUPPRESS, action="store_true")

    # For index testing locally
    parser.add_argument("--use-local-index", help=argparse.SUPPRESS, action="store_true")

    # Temporary holding page for snaps
    parser.add_argument("--holding-page", help=argparse.SUPPRESS, action="store_true")

    args = parser.parse_args()

    #~ if args.version:
        #~ # Not hardcoded. Get this version from Apt.
        #~ exit(0)

    if args.verbose:
        dbg.verbose_level = 1
        dbg.stdout(_("Verbose enabled"), dbg.debug, 1)

    if args.debug:
        dbg.verbose_level = 2
        dbg.stdout(_("Debug verbose enabled"), dbg.debug, 2)

    if args.arch:
        dbg.override_arch = args.arch
        dbg.stdout("=> Showing listings for arch: " + args.arch, dbg.debug)

    if args.codename:
        dbg.override_codename = args.codename
        dbg.stdout("=> Showing listings for release: " + dbg.override_codename, dbg.debug)

    if args.locale:
        dbg.override_locale = args.locale
        _ = Common.setup_translations(__file__, "software-boutique", dbg.override_locale)
        dbg.stdout("=> Forcing locale: " + dbg.override_locale, dbg.debug)

    if args.simulate:
        boutique.force_dummy = True
        dbg.stdout("=> Simulating software changes.", dbg.debug)

    if args.inspect:
        dbg.verbose_level = 3

    if args.clear_cache:
        if os.path.exists(pref.folder_cache):
            dbg.stdout("Cache cleared.", dbg.debug)
            rmtree(pref.folder_cache)
            os.makedirs(pref.folder_cache)
        else:
            dbg.stdout("Cache is already empty.", dbg.debug)

    if args.use_local_index:
        global index_url
        index_url = "http://localhost:8000/"
        dbg.stdout("Using local index on port 8000", dbg.debug)

    if args.holding_page:
        global show_holding_page
        show_holding_page = True


def shutdown():
    """
    Quit the application gracefully.
    """
    dbg.stdout("Closing Software Boutique...", dbg.action)
    Gtk.main_quit()
    sys.exit(0)


class QueueUICallback(object):
    """
    A copy of the boutique.UICallback class tailored for Boutique.
    """
    def __init__(self, update_page):
        self.update_page = update_page
        self.current_app_uuid = "null"
        self.operation = "null"

    def update_current_progress(self, status_text, percent=-1):
        self.update_page(".queue-ui-element", "hide")
        self.update_page("#queue-" + self.current_app_uuid + " .status", "html", status_text)
        self.update_page("#queue-status-text", "html", "")
        self.update_page("#queue-inprogress", "show")

        if percent == -1:
            self.update_page("#queue-progress", "hide")
        else:
            self.update_page("#queue-progress", "show")
            self.update_page("#queue-progress-inner", "show")
            self.update_page("#queue-progress-inner", "css", "width", str(percent) + "%")

        app_status_text = {
            "install": [_("Installing..."), "fa-download"],
            "remove": [_("Removing..."), "fa-trash"]
        }

        self.update_page(".buttons-" + self.current_app_uuid, "html",
            "<div id='app-install-info'><span class='fa {0}'></span> {1}</div>".format(
                app_status_text[self.operation][1],
                app_status_text[self.operation][0]))

        print('\033[95m' + "Status Changed: {0} ({1}%)".format(status_text, str(percent)) + '\033[0m')


if __name__ == "__main__":
    # Prepare for verbose output and translation support.
    dbg = Common.Debugging()
    data_source = get_data_source()
    boutique.data_source = data_source
    _ = Common.setup_translations(__file__, "software-boutique")
    boutique.dbg = dbg

    # JSON in memory
    # - Stores preferences.
    # - Stores timestamps for applications installed via Boutique.
    pref = Preferences.Preferences(dbg, "preferences", "software-boutique")
    install_time = Preferences.Preferences(dbg, "install_time", "software-boutique")

    # String dictonary when passing to boutique.py module
    boutique.string_dict = {
        "details_text": _("Details"),
        "details_tooltip": _("Learn more about this application"),
        "install_text": _("Install"),
        "install_tooltip": _("Install this application on your computer"),
        "reinstall_text": "",
        "reinstall_tooltip": _("Reinstall this application"),
        "remove_text": "",
        "remove_tooltip": _("Remove this application"),
        "launch_text": "Launch",
        "launch_tooltip": _("Runs the application"),
        "remove_queue": _("Remove from queue"),
        "remove_queue_tooltip": _("Cancels any changes for this application"),
        "installing": _("Installing..."),
        "removing": _("Removing..."),
        "queued-install": _("Will be installed."),
        "queued-remove": _("Will be removed."),
        "starting-install": _("Starting to install"),
        "starting-remove": _("Starting to remove")
    }

    # On this thread
    parse_parameters()
    app = SoftwareBoutique()
    webview = WebView()
    main = ApplicationWindow()
    main.build(webview)
    app.webkit = webview
    app.run_js = webview.run_js
    app.update_page = webview.update_page
    boutique.ui_callback = QueueUICallback(app.update_page)
    main.run()
