#!/usr/bin/env python3
#
# SPDX-License-Identifier: MIT
#

## -- BEGIN PORTMASTER INFO --
PORTMASTER_VERSION = '2025.03.03-0141'
PORTMASTER_RELEASE_CHANNEL = 'stable'
## -- END PORTMASTER INFO --

PORTMASTER_MIN_VERSION = '2025-02-17-0000'
PORTMASTER_RELEASE_URL = 'https://github.com/PortsMaster/PortMaster-GUI/releases/latest/download/'
PORTMASTER_RELEASE_VALUES = ('stable', 'beta', 'alpha')

PORTMASTER_UPDATE_FREQUENCY = (60 * 60 * 1)
__builtins__.PORTMASTER_DEBUG = False  ## This adds a lot of extra info

import contextlib
import ctypes
import datetime
import errno
import functools
import gettext
import hashlib
import json
import math
import os
import re
import shutil
import subprocess
import sys
import tarfile
import textwrap
import time
import zipfile

from pathlib import Path

################################################################################
## Insert our extra modules.
PYLIB_PATH    = Path(__file__).parent / 'pylibs'
EXLIB_PATH    = Path(__file__).parent / 'exlibs'
PYLIB_ZIP     = Path(__file__).parent / 'pylibs.zip'
PYLIB_ZIP_MD5 = Path(__file__).parent / 'pylibs.zip.md5'

if not (Path(__file__).parent / '.git').is_dir() and not (Path(__file__).parent / '..' / '.git').is_dir():
    if PYLIB_ZIP.is_file():
        if PYLIB_PATH.is_dir():
            print("- removing old pylibs.")
            shutil.rmtree(PYLIB_PATH)

        if EXLIB_PATH.is_dir():
            print("- removing old exlibs.")
            shutil.rmtree(EXLIB_PATH)

        print("- extracting new pylibs.")
        with zipfile.ZipFile(PYLIB_ZIP, 'r') as zf:
            zf.extractall(Path(__file__).parent)

        md5_check = hashlib.md5()
        with PYLIB_ZIP.open('rb') as fh:
            while True:
                data = fh.read(1024 * 1024)
                if len(data) == 0:
                    break

                md5_check.update(data)

        with PYLIB_ZIP_MD5.open('wt') as fh:
            fh.write(md5_check.hexdigest())

        print("- recorded pylibs.zip.md5")

        del md5_check

        print("- removing pylibs.zip")
        PYLIB_ZIP.unlink()


if not (PYLIB_PATH / 'resources' / 'NotoSansTC-Regular.ttf').is_file():
    ## Extract Noto fonts.
    with tarfile.open(str(PYLIB_PATH / 'resources' / 'NotoSans.tar.xz'), 'r:xz') as tar:
        # Extract all contents into the specified directory
        tar.extractall(str(PYLIB_PATH / 'resources'))


## HACK D:
__builtins__.PYLIB_PATH = PYLIB_PATH

sys.path.insert(0, str(EXLIB_PATH))
sys.path.insert(0, str(PYLIB_PATH))

################################################################################
## Now load the stuff we include
import utility
import harbourmaster
import png
import requests

import sdl2
import sdl2.ext

import pySDL2gui
import pugtheme

from loguru import logger
from pugtheme import theme_load, ThemeEngine, ThemeDownloader
from pugscene import *

from harbourmaster import (
    HarbourMaster,
    make_temp_directory,
    )

_ = gettext.gettext

################################################################################
## Logging
LOG_FILE = harbourmaster.HM_TOOLS_DIR / "PortMaster" / "pugwash.txt"
LOG_FILE_HANDLE = None
if LOG_FILE.parent.is_dir():
    LOG_FILE_HANDLE = logger.add(LOG_FILE, level="DEBUG", backtrace=True, diagnose=True)


################################################################################
## Translations
LANG_DIR = PYLIB_PATH / "locales"
__builtins__.DEFAULT_LANG = None
__builtins__.CURRENT_LANG = None


ALL_LANGUAGES = {
    ## Keep it in order of importance, for example pt_PT before pt_BR.
    "da_DK": _("Danish"),
    "de_DE": _("German"),
    "en_US": _("English"),
    "es_ES": _("Spanish"),
    "fi_FI": _("Finnish"),
    "fr_FR": _("French"),
    "it_IT": _("Italian"),
    "ja_JP": _("Japanese"),
    "ko_KR": _("Korean"),
    "nl_NL": _("Dutch"),
    "pl_PL": _("Polish"),
    "pt_PT": _("Portuguese (Portugal)"),
    "pt_BR": _("Portuguese (Brazil)"),
    # "ro_RO": _("Romanian"),
    "ru_RU": _("Russian"),
    "sv_SE": _("Swedish"),
    "uk_UA": _("Ukrainian"),
    "zh_CN": _("Chinese Simplified"),
    }


def check_lang(lang):
    """
    Check if `LANG_DIR / lang / "LC_MESSAGES"` exists

    We simplify the language by removing any encoding eg: `en_AU.utf8` to `en_AU`
    We then simplify the language by removing any sub-language: `en_AU` to `en`

    We then check all languages on the list of languages.
    """
    if lang is None:
        return None

    temp = LANG_DIR / lang / "LC_MESSAGES"
    if temp.is_dir():
        return lang

    if '.' in lang:
        lang = lang.rsplit('.', 1)[0]

        temp = LANG_DIR / lang / "LC_MESSAGES"
        if temp.is_dir():
            return lang

    if '_' in lang:
        lang = lang.split('_', 1)[0]

    for lang_opt in ALL_LANGUAGES:
        temp = LANG_DIR / lang_opt / "LC_MESSAGES"
        if temp.is_dir() and lang_opt.startswith(lang):
            return lang_opt

    return None


def load_lang():
    """

    Load the desired language, we check for an override in our config.json, otherwise use the environment variables.

    """

    config = harbourmaster.HM_TOOLS_DIR / "PortMaster" / "config" / "config.json"
    if config.is_file():
        with open(config, 'r') as fh:
            config = harbourmaster.json_safe_load(fh)

        if config is not None:
            config_lang = config.get("language", None)

        else:
            config_lang = None

    else:
        config_lang = None

    environ_key = None
    environ_lang = None
    for key in ('LANG', 'LANGUAGE', 'LC_ALL', 'LC_MESSAGES'):
        if key in os.environ:
            if environ_key is not None:
                del os.environ[key]

            else:
                environ_key = key
                environ_lang = os.environ[key]

    if environ_key is None:
        environ_key = 'LANG'

    __builtins__.DEFAULT_LANG = check_lang(environ_lang) or 'en_US'

    if config_lang and check_lang(config_lang):
        os.environ[environ_key] = check_lang(config_lang)

    elif environ_lang and check_lang(environ_lang):
        os.environ[environ_key] = check_lang(environ_lang)

    else:
        os.environ[environ_key] = 'en_US'

    __builtins__.CURRENT_LANG = os.environ[environ_key]


def lang_list():
    languages = {
        lang_id: lang_name
        for lang_id, lang_name in sorted(ALL_LANGUAGES.items(), key=lambda item: (item[1]))
        if (LANG_DIR / lang_id).is_dir()}

    return languages


load_lang()
gettext.bindtextdomain('messages', str(LANG_DIR))
gettext.bindtextdomain('themes', str(LANG_DIR))
gettext.textdomain('messages')

################################################################################
## Code starts here.
def is_process_running(process_name):
    """
    Check if a process with the given name is running using pgrep.
    """

    try:
        output = subprocess.check_output(['pgrep', '-f', process_name], stderr=subprocess.DEVNULL)
        return bool(output.strip())  # If output exists, the process is running
    except subprocess.CalledProcessError:
        return False  # pgrep returns non-zero exit code if no process matches


__IP_ADDRESS=None
def get_ip_address(force_update=False):
    global __IP_ADDRESS

    if not force_update and __IP_ADDRESS is not None:
        return __IP_ADDRESS

    import socket

    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    s.settimeout(0)
    try:
        # doesn't even have to be reachable
        s.connect(('1.1.1.1', 1))
        __IP_ADDRESS = s.getsockname()[0]

    except Exception:
        __IP_ADDRESS = None

    finally:
        s.close()

    return __IP_ADDRESS


def format_progress(amount, total, fmt=None):
    if fmt == 'data':
        if total is None:
            return f"{harbourmaster.nice_size(amount)}"

        else:
            return f"{harbourmaster.nice_size(amount)} / {harbourmaster.nice_size(total)}"

    elif fmt == '%':
        if total is None:
            return f"{min(amount, 100):.0f} %"

        else:
            return f"{min(amount / total * 100, 100):.0f} %"

    else:
        if total is None:
            return f"{amount}"

        else:
            return f"{amount} / {total}"


def fifo_line_reader(io):
    buffer = ""

    while True:
        try:
            data = os.read(io, 4096)

            buffer += data.decode("utf-8")

        except OSError as err:
            if not (err.errno == errno.EAGAIN or err.errno == errno.EWOULDBLOCK):
                raise  # something else has happened -- better reraise

        while "\n" in buffer:
            result, buffer = buffer.split("\n", 1)

            yield result

        yield ""


class DirectoryScanner:
    def __init__(self):
        self.scans = {}
        self.results = {}
        self.callback = None

    def _check(self, scan_dir, scan_info):
        try:
            result = next(scan_info[1])
            scan_info[0] = result
            return result, False

        except StopIteration:
            del self.scans[scan_dir]
            self.results[scan_dir] = scan_info[0]
            return scan_info[0], True

    def _get_directory_size(self, path):
        stack = [path]

        while len(stack) > 0:
            total_size = 0
            path = stack.pop(0)

            if path.is_file():
                total_size += entry.stat().st_size

            elif path.is_dir():
                for entry in os.scandir(path):
                    if entry.name in ('.', '..'):
                        continue

                    elif entry.is_file():
                        total_size += entry.stat().st_size

                    elif entry.is_dir():
                        stack.append(Path(entry.path))

                yield total_size
                total_size = 0

            yield total_size

    def _calculate_total_size(self, path):
        total_size = 0

        for size in self._get_directory_size(path):
            total_size += size
            yield total_size

        yield total_size

    def iterate(self, max_scans=60):
        scanned_items = 0
        scanned = {}

        while scanned_items < max_scans and len(self.scans) > 0:
            for scan_dir in list(self.scans.keys()):
                scanned[scan_dir] = self._check(scan_dir, self.scans[scan_dir])

                scanned_items += 1
                if scanned_items >= max_scans:
                    break

        if self.callback:
            for scanned_dir in scanned:
                self.callback(scanned_dir, *scanned[scanned_dir])

    def check_directory(self, directory, nice_size=True):
        # Check if results are available for a directory scan, otherwise start it scanning
        if directory in self.results:
            if nice_size:
                return harbourmaster.nice_size(self.results[directory])

            else:
                return self.results[directory]

        elif directory in self.scans:
            if nice_size:
                return f"~ {harbourmaster.nice_size(self.scans[directory][0])}"

            else:
                return None

        else:
            self.scans[directory] = [0, self._calculate_total_size(directory)]
            if nice_size:
                return f"~ {harbourmaster.nice_size(self.scans[directory][0])}"

            return None

    def clear_directory(self, directory):
        # Clear data about a directory, cancel any scans in progress
        if directory in self.scans:
            del self.scans[directory]

        if directory in self.results:
            del self.results[directory]

    def clear_all(self):
        # Clear all scans in progress
        self.scans.clear()


class FileVerifier:
    BLOCK_SIZE = (1024 * 1024 * 1)

    def __init__(self):
        self.scans = {}
        self.results = {}
        self.callback = None

    def _verify(self, file_name, verify_info):
        try:
            result = next(verify_info[1])
            verify_info[0] = result
            return result, False

        except StopIteration:
            del self.scans[file_name]
            self.results[file_name] = verify_info[0]
            return verify_info[0], True

    def _get_md5sum(self, file_name):
        md5_obj = hashlib.md5()
        with open(file_name, 'rb') as fh:
            while True:
                data = fh.read(BLOCK_SIZE)
                if data == b"":
                    break

                md5_obj.update(data)
                yield None

        yield md5_obj.hexdigest()

    def iterate(self, max_scans=30):
        scanned_items = 0
        scanned = {}

        while scanned_items < max_scans and len(self.scans) > 0:
            for file_name in list(self.scans.keys()):
                scanned[file_name] = self._check(file_name, self.scans[file_name])

                scanned_items += 1
                if scanned_items >= max_scans:
                    break

        if self.callback:
            for file_name in scanned:
                self.callback(file_name, *scanned[file_name])

    def verify_file(self, file_name):
        # Check if results are available for a directory scan, otherwise start it scanning
        if file_name in self.results:
            return self.results[file_name]

        elif file_name not in self.scans:
            self.scans[file_name] = [0, self._verify(directory)]

        return "Verifying"

    def clear_file(self, file_name):
        # Clear data about a directory, cancel any scans in progress
        if file_name in self.scans:
            self.scans[file_name][1].close()
            del self.scans[file_name]

        if file_name in self.results:
            del self.results[file_name]

    def clear_all(self):
        # Clear all scans in progress
        for file_name in self.scans:
            self.clear_file(file_name)

        self.results.clear()
        self.scans.clear()


class PortMasterGUI(pySDL2gui.GUI, harbourmaster.Callback):
    TICK_INTERVAL = 1000 // 5
    TEXT_DATA_FREQ = 5000
    MIN_THEME_VERSION = 1

    def __init__(self, *, first_scene=None, force_theme=None):
        # Initialize SDL
        sdl2.ext.init(
            controller=True)

        def nice_version(v):
            return f"{v.major}.{v.minor}.{v.patch}"

        version = sdl2.SDL_version()
        sdl2.SDL_GetVersion(version)

        logger.info(f"PM: {PORTMASTER_VERSION}")
        logger.info(f"HM: {harbourmaster.HARBOURMASTER_VERSION}")
        logger.info(f"SDL DLL: {sdl2.dll.get_dll_file()},  {nice_version(version)}")
        logger.info(f"TTF DLL: {sdl2.sdlttf.get_dll_file()}, {nice_version(sdl2.sdlttf.TTF_Linked_Version()[0])}")
        logger.info(f"IMG DLL: {sdl2.sdlimage.get_dll_file()}, {nice_version(sdl2.sdlimage.IMG_Linked_Version()[0])}")
        logger.info(f"MIX DLL: {sdl2.sdlmixer.get_dll_file()}, {nice_version(sdl2.sdlmixer.Mix_Linked_Version()[0])}")
        logger.info(f"HM_TESTING: {harbourmaster.HM_TESTING}")

        # Load controller.
        count = sdl2.SDL_NumJoysticks()
        for index in range(count):
            is_game_controller = sdl2.SDL_IsGameController(index)
            print(f"{index}: {is_game_controller}")
            if is_game_controller == sdl2.SDL_TRUE:
                pad = sdl2.SDL_GameControllerOpen(index)
                if pad is not None:
                    logger.info(f"Opened GameController {index}: {sdl2.SDL_GameControllerName(pad)}")
                    logger.info(f" {sdl2.SDL_GameControllerMapping(pad)}")

        # Define window dimensions
        self.display_width = 640
        self.display_height = 480
        self.hm = None
        self.timers = pySDL2gui.Timer()

        # Get the current display mode
        display_mode = sdl2.video.SDL_DisplayMode()
        capabilities = harbourmaster.device_info()

        if sdl2.video.SDL_GetCurrentDisplayMode(0, display_mode) != 0:
            logger.error("Failed to get display mode:", sdl2.SDL_GetError())
            self.display_width, self.display_height = capabilities['resolution']

        else:
            self.display_width = display_mode.w
            self.display_height = display_mode.h
            # Print the display width and height
            logger.info(f"Display size: {self.display_width}x{self.display_height}")

        if harbourmaster.HM_TESTING:
            capabilities = harbourmaster.device_info()

            ## Uncomment one of these to pretend to be a different device. more devices in pylibs/harbourmaster/hardware.py
            pretend_device = (
                # 'rg351p'
                # 'rg552'
                # 'rg503'
                # 'rg351v'
                # 'rg353v'
                # 'rgb30'
                # 'ogs'
                # 'ogu'
                # 'x55'
                # 'ace'
                )

            if not pretend_device:
                pretend_device = 'pc'
                ## uncomment to make it *fullscreen
                # pretend_device = None

            pretend_resolution = None
            ## Uncomment to choose a particular resolution
            # pretend_resolution = (1024, 768)

            capabilities = harbourmaster.device_info(pretend_device, override_resolution=pretend_resolution)

        if capabilities['device'] == 'default':
            device = harbourmaster.find_device_by_resolution((self.display_width, self.display_height))
            logger.info(f"FOUND: {device}")
            if device != 'default':
                capabilities = harbourmaster.device_info(device)

        if capabilities['device'] == 'default':
            capabilities = harbourmaster.device_info(override_resolution=(self.display_width, self.display_height))

        capabilities['capabilities'].append(CURRENT_LANG)

        logger.info(capabilities)
        print(capabilities)

        # Create the window
        if harbourmaster.HM_TESTING:
            window_flags = None
        else:
            window_flags = sdl2.SDL_WINDOW_FULLSCREEN

            touch_file = harbourmaster.HM_TOOLS_DIR / "PortMaster" / "utils" / "pmsplash" / "stopsplash"

            if touch_file.parent.is_dir():
                touch_file.touch()

            for i in range(10):
                if not is_process_running(f"love.{capabilities['primary_arch']}"):
                    break

                sdl2.SDL_Delay(500)

            if is_process_running(f"love.{capabilities['primary_arch']}"):
                subprocess.check_output(["pkill", "-f", f"love.{capabilities['primary_arch']}"])

                if touch_file.is_file():
                    touch_file.unlink()


        self.window = sdl2.ext.Window("PortMaster", size=capabilities['resolution'], flags=window_flags)
        self.window.show()

        # Create a renderer for drawing on the window
        renderer = sdl2.ext.Renderer(self.window, flags=sdl2.SDL_RENDERER_ACCELERATED)

        sdl2.SDL_SetHint(sdl2.SDL_HINT_RENDER_SCALE_QUALITY, b"2")

        # Quickly present something, helps on ArkOS.
        renderer.clear()
        renderer.present()

        self.text_data = {}
        self.changed_keys = set()
        formatter = StringFormatter(self.text_data)

        super().__init__(renderer, formatter)

        cfg_data = self.get_config()
        default_sound = False

        if 'arkos' in capabilities['name'].lower() and 'rk3366' in capabilities['cpu']:
            default_sound = not Path('/etc/asound.conf').exists()

        self.sounds.sound_is_disabled = cfg_data.setdefault('sfx-disabled', default_sound)
        self.sounds.music_is_disabled = cfg_data.setdefault('music-disabled', default_sound)

        self.cancellable = True

        self.themes = ThemeEngine(self, force_theme=force_theme)
        self.dir_scanner = DirectoryScanner()
        self.dir_scanner.callback = self.dir_scanner_callback

        self.theme_data = self.themes.gui_init()
        self.resources.add_path(PYLIB_PATH / 'resources')
        self.theme_downloader = None

        self.spinner = 0

        if first_scene is not None:
            self.scenes = [
                ('root', [first_scene(self)]),
                ]
        else:
            self.scenes = [
                ('root', [TempMenuScene(self)]),
                ]

        self.callback_messages = []
        self.callback_progress = None
        self.callback_amount = 0
        self.message_box_disable = False
        self.message_box_depth = 0
        self.message_box_scene = None
        self.was_cancelled = False

        self.updated = True
        self.in_screenshot = False

        device_info = harbourmaster.device_info()
        self.set_data('system.portmaster_version', PORTMASTER_VERSION)
        self.set_data('system.harbourmaster_version', harbourmaster.HARBOURMASTER_VERSION)
        self.set_data('system.cfw_name', device_info['name'])
        self.set_data('system.cfw_version', device_info['version'])
        self.set_data('system.device_name', device_info['device'])
        self.set_data('system.ip_address', get_ip_address() or _("Unknown IP"))
        self.set_data('system.progress_text', "")
        self.set_data('system.progress_amount', "")
        self.set_data('system.progress_perc_5', "0")
        self.set_data('system.progress_perc_10', "0")
        self.set_data('system.progress_perc_25', "0")

        self.set_data('ports_list.filters', "")
        self.set_data('ports_list.total_ports', "")
        self.set_data('ports_list.filter_ports', "")

        self.set_port_info(None, {})

        self.lang_list = lang_list()
        self.update_counter = 0
        self.draw_counter = 0

        self.port_size_active_port = None
        self.port_size_files = {}
        self.port_size_file_lookup = {}

    # def init_theme(self):
    #     ## This has to run before harbourmaster is initialised, so we gotta work it out ourself.
    #     theme_name = self.get_current_theme()

    #     self.resources.add_path(PYLIB_PATH / 'resources')
    #     self.resources.add_path(self.get_theme_dir(theme_name))

    def get_config(self):
        cfg_dir = harbourmaster.HM_TOOLS_DIR / "PortMaster"
        cfg_file = cfg_dir / "config" / "config.json"
        cfg_data = {}

        if self.hm is None:
            if cfg_file.is_file():
                with open(cfg_file, 'r') as fh:
                    cfg_data = json.load(fh)
        else:
            cfg_data = self.hm.cfg_data

        return cfg_data

    def save_config(self, cfg_data):
        cfg_dir = harbourmaster.HM_TOOLS_DIR / "PortMaster"
        cfg_file = cfg_dir / "config" / "config.json"

        if self.hm is None:
            with open(cfg_file, 'w') as fh:
                json.dump(cfg_data, fh, indent=4)

        else:
            self.hm.save_config()

    ## Loop stuff.
    def run(self):
        if self.hm is not None:
            if self.hm.platform.WANT_XBOX_FIX:
                self.events.fix_xbox_mode()

            if self.hm.platform_name in ('retrodeck', ):
                self.events.fix_retrodeck_mode()

            self.SWAP_BUTTONS = self.hm.platform.WANT_SWAP_BUTTONS

        try:
            while True:
                self.do_loop()

        except harbourmaster.CancelEvent:
            pass

    def do_loop(self, *, no_delay=False):
        events = self.events
        events.handle_events()

        if events.buttons['START'] and self.events.buttons['BACK']:
            events.running = False

        if events.was_pressed('SCRN') or (events.buttons['BACK'] and events.was_pressed('Y')):
            self.create_screenshot()

        if not events.running:
            self.do_cancel()

        self.do_update()
        self.do_draw()

        ## TODO: fix it, 30 is approximately 30fps (1000 // 30)
        if not no_delay:
            sdl2.SDL_Delay(30)

        if self.timers.elapsed('updates_per_second', 1000, run_first=True):
            if PORTMASTER_DEBUG:
                print(f"UPS: {self.draw_counter} / {self.update_counter}")

            ## Unload extra images.
            self.images._clean()

            self.update_counter = 0
            self.draw_counter = 0
            self.updated = True

    def do_update(self):
        # Update tags
        if self.timers.elapsed('text_data_update', self.TEXT_DATA_FREQ, run_first=True):
            self.set_data("system.time_24hr", datetime.datetime.now().strftime("%H:%M"))
            self.set_data("system.time_12hr", datetime.datetime.now().strftime("%I:%M %p"))

            if harbourmaster.HM_PORTS_DIR.is_dir():
                disk_usage = shutil.disk_usage(str(harbourmaster.HM_PORTS_DIR))

                self.set_data("system.free_space", harbourmaster.nice_size(disk_usage.free))
                self.set_data("system.used_space", harbourmaster.nice_size(disk_usage.used))
                self.set_data("system.total_space", harbourmaster.nice_size(disk_usage.total))
            else:
                self.set_data("system.free_space", "000 B")
                self.set_data("system.used_space", "000 B")
                self.set_data("system.total_space", "000 B")

            battery_paths = [
                Path("/sys/class/power_supply/battery/capacity"),
                Path("/sys/class/power_supply/axp2202-battery/capacity"),
                Path("/sys/class/power_supply/BAT1/capacity"),
                Path("/sys/class/power_supply/qcom-battery/capacity"),
                ]

            for battery_file in battery_paths:
                if battery_file.exists():
                    self.set_data("system.battery_level", f"{int(battery_file.read_text().strip())}%")
                    break

            else:
                self.set_data("system.battery_level", _("N/A"))

        # Events get handled in reversed order.
        for scene in reversed(self.scenes[-1][1]):
            if scene.do_update(self.events):
                break

        # Update scanning
        if self.timers.elapsed('dir_scan_interval', 500, run_first=True):
            self.dir_scanner.iterate(30)

        ## Check for any keys changed in our template system.
        if len(self.changed_keys):
            for layer in self.scenes:
                for scene in layer[1]:
                    scene.update_data(self.changed_keys)

            self.changed_keys.clear()

        self.update_counter += 1

    def do_draw(self):
        if not self.in_screenshot:
            if not self.updated:
                return

            if not self.timers.elapsed('maximum_draw', 20, run_first=True):
                return

            if self.draw_counter > 30:
                return

        # Drawing happens in forwards order
        self.renderer.clear()

        for scene in self.scenes[-1][1]:
            scene.do_draw()

        if not self.in_screenshot:
            self.renderer.present()
            self.updated = False
            self.draw_counter += 1

        self.clean()

    def create_screenshot(self):
        """
        Creates a screenshot and saves it to screenshot.png
        """
        # Create a texture to render onto
        self.in_screenshot = True
        render_info = sdl2.SDL_RendererInfo()
        sdl2.SDL_GetRendererInfo(self.renderer.sdlrenderer, render_info)
        renderer_format = render_info.texture_formats[0]

        texture = sdl2.SDL_CreateTexture(
            self.renderer.sdlrenderer,
            renderer_format,
            sdl2.SDL_TEXTUREACCESS_TARGET,
            self.renderer.logical_size[0],
            self.renderer.logical_size[1])

        # Set the texture as the rendering target
        sdl2.SDL_SetRenderTarget(self.renderer.sdlrenderer, texture)

        # Draw the GUI
        self.do_draw()

        # Reset the rendering target to the default (the window)
        sdl2.SDL_SetRenderTarget(self.renderer.sdlrenderer, None)

        # Capture screenshot
        width, height = ctypes.c_int(0), ctypes.c_int(0)
        sdl2.SDL_QueryTexture(texture, None, None, ctypes.byref(width), ctypes.byref(height))
        pixels = (ctypes.c_uint8 * (width.value * height.value * 4))()
        sdl2.SDL_RenderReadPixels(
            self.renderer.sdlrenderer,
            None,
            sdl2.SDL_PIXELFORMAT_ABGR8888,
            pixels,
            width.value * 4)

        # Save the screenshot using pypng
        with open('screenshot.png', 'wb') as f:
            writer = png.Writer(width=width.value, height=height.value, greyscale=False, alpha=True)
            writer.write_array(f, pixels)

        # Clean up
        sdl2.SDL_DestroyTexture(texture)
        self.in_screenshot = False

    def get_port_image(self, port_name):
        image = None
        if self.hm is not None:
            image = self.hm.port_images(port_name)

            if image is not None:
                image = image.get('screenshot', None)

        if image is None:
            image = "NO_IMAGE"

        return image

    def set_port_info(self, port_name, port_info, want_install_size=False):
        ## TODO: make this better :D
        if port_name is None:
            self.set_data("port_info.name", "NOTHING")
            self.set_data("port_info.image", "NO_IMAGE")
            self.set_data("port_info.title", _("** NO PORT **"))
            self.set_data("port_info.description", "")
            self.set_data("port_info.instructions", "")
            self.set_data("port_info.genres", "")
            self.set_data("port_info.porter", "")
            self.set_data("port_info.ready_to_run", "")
            self.set_data("port_info.runtime", "")
            self.set_data("port_info.download_size", "")
            self.set_data("port_info.install_size", "")
            # self.set_data("port_info.image", "no-image")
            return

        self.set_data("port_info.name", port_name)
        self.set_data("port_info.image", str(self.get_port_image(port_name)))
        self.set_data("port_info.title", port_info['attr']['title'])
        self.set_data("port_info.description", port_info['attr']['desc'])
        self.set_data("port_info.instructions", port_info['attr']['inst'])
        self.set_data("port_info.genres", ', '.join(port_info['attr']['genres']))
        self.set_data("port_info.porter", harbourmaster.oc_join(port_info['attr']['porter']))
        self.set_data("port_info.ready_to_run", port_info['attr']['rtr'] and _("Ready to Run") or _("Requires Files"))
        # self.set_data("port_info.image", port_image)

        runtimes = port_info['attr']['runtime']
        if len(runtimes) > 0:
            self.set_data("port_info.runtime", harbourmaster.runtime_nicename(runtimes))
            installed_runtimes = 0
            for runtime in runtimes:
                if (self.hm.libs_dir / runtime).is_file():
                    installed_runtimes += 1

            if len(runtimes) == installed_runtimes:
                self.set_data("port_info.runtime_status", _("Installed"))
            else:
                self.set_data("port_info.runtime_status", _("Missing"))

        else:
            self.set_data("port_info.runtime", "")
            self.set_data("port_info.runtime_status", _("N/A"))

        self.set_data("port_info.download_size", harbourmaster.nice_size(self.hm.port_download_size(port_name)))
        if want_install_size and 'files' in port_info and port_info['files'] is not None:
            self.get_port_size(port_name, port_info)
        else:
            self.set_data("port_info.install_size", "")

        # print(f"INFO: {port_info}")

    def set_theme_info(self, theme_name, theme_info):
        ## TODO: make this better :D
        if theme_name is None:
            self.set_data("theme_info.image", "NO_IMAGE")
            self.set_data("theme_info.name", "")
            self.set_data("theme_info.description", "")
            self.set_data("theme_info.creator", "")
            self.set_data("theme_info.status", "")
            return

        status_to_lang = {
            "Installed": _("Installed"),
            "Update Available": _("Update Available"),
            "Not Installed": _("Not Installed"),
            }

        self.set_data("theme_info.image", str(theme_info['image'] or "NO_IMAGE"))
        self.set_data("theme_info.name", theme_info['name'])
        self.set_data("theme_info.description", theme_info['description'])
        self.set_data("theme_info.creator", theme_info['creator'])
        self.set_data("theme_info.status", status_to_lang.get(theme_info['status'], theme_info['status']))

    def get_port_size(self, port_name, port_info):
        self.port_size_active_port = port_name

        if port_name not in self.port_size_files:
            if port_info is None:
                # HRMMMMMmmmmmm
                return

            self.port_size_files[port_name] = {}

            ports_dir = harbourmaster.HM_PORTS_DIR
            for file_name in port_info['files']:
                if file_name == 'port.json':
                    continue

                full_file_name = ports_dir / file_name
                if not full_file_name.is_absolute():
                    full_file_name = full_file_name.resolve()

                lookup = self.port_size_file_lookup.setdefault(full_file_name, [])
                if port_name not in lookup:
                    lookup.append(port_name)

                if full_file_name.is_file():
                    self.port_size_files[port_name][full_file_name] = [os.stat(full_file_name).st_size, True]

                else:
                    result = self.dir_scanner.check_directory(full_file_name, False)
                    if result is None:
                        self.port_size_files[port_name][full_file_name] = [0, False]
                    else:
                        self.port_size_files[port_name][full_file_name] = [result, True]

        port_size = 0
        all_found = True
        for port_size_info in self.port_size_files[port_name].values():
            port_size += port_size_info[0]
            if not port_size_info[1]:
                all_found = False

        # print(f"PN: {port_name}")
        if not all_found:
            self.set_data("port_info.install_size", f"~ {harbourmaster.nice_size(port_size)}")

        else:
            self.set_data("port_info.install_size", harbourmaster.nice_size(port_size))

    def delete_port_size(self, port_name):
        # Delete info about a port
        if port_name in self.port_size_files:
            for port_file, port_info in self.port_size_files[port_name].items():
                if port_name in self.port_size_file_lookup[port_file]:
                    self.port_size_file_lookup[port_file].remove(port_name)

            del self.port_size_files[port_name]

    def clear_port_sizes(self):
        self.port_size_file_lookup.clear()
        self.port_size_files.clear()
        self.port_size_active_port = None

    def dir_scanner_callback(self, scan_dir, dir_size, is_final=False):
        # print(f"SCAN: {scan_dir}: {dir_size}, {is_final}")
        if scan_dir in self.port_size_file_lookup:
            for port_name in self.port_size_file_lookup[scan_dir]:
                self.port_size_files[port_name][scan_dir][0] = dir_size
                self.port_size_files[port_name][scan_dir][1] = is_final

                if port_name == self.port_size_active_port:
                    self.get_port_size(port_name, None)

    def set_data(self, key, value):
        if self.text_data.get(key, None) == value:
            return

        self.text_data[key] = value
        self.changed_keys.add(key)
        # logger.debug(f"{key}: {value}")

    def get_data(self, key):
        return self.text_data.get(key, None)

    def format_data(self, input_string, used_keys=None):
        return self.formatter.format_string(input_string, used_keys)

    def quit(self):
        # Clean up
        sdl2.ext.quit()

    ## Messagebox / Callback stuff
    def callback_update(self):
        self.updated = True
        if self.message_box_scene:
            page_size = max(self.message_box_scene.tags['message_text'].page_size, 12) + 1
            self.message_box_scene.tags['message_text'].text = '\n'.join(self.callback_messages[-int(page_size):])

            self.do_loop(no_delay=True)

    def progress(self, message, amount, total=None, fmt=None):
        if message is None:
            self.callback_progress = None
            self.callback_amount = 0
            self.spinner = 0
            self.set_data("system.progress_text", "")
            self.set_data("system.progress_amount", "")

            self.set_data('system.progress_perc_5', "0")
            self.set_data('system.progress_perc_10', "0")
            self.set_data('system.progress_perc_20', "0")
            self.set_data('system.progress_perc_25', "0")
            self.set_data('system.progress_spinner_5',  "0")
            self.set_data('system.progress_spinner_10', "0")
            self.set_data('system.progress_spinner_20', "0")
            self.set_data('system.progress_spinner_25', "0")
            self.set_data('system.progress_perc_5_or_spinner',  "0")
            self.set_data('system.progress_perc_10_or_spinner', "0")
            self.set_data('system.progress_perc_20_or_spinner', "0")
            self.set_data('system.progress_perc_25_or_spinner', "0")

        else:
            self.spinner += 1
            self.set_data('system.progress_perc', "0")
            self.set_data('system.progress_spinner_5',  f"{(((self.spinner % 20) + 1) *  5)}")
            self.set_data('system.progress_spinner_10', f"{(((self.spinner % 10) + 1) * 10)}")
            self.set_data('system.progress_spinner_20', f"{(((self.spinner %  5) + 1) * 20)}")
            self.set_data('system.progress_spinner_25', f"{(((self.spinner %  4) + 1) * 25)}")

            if total is not None:
                percent = amount / total * 100
                self.set_data('system.progress_perc',    f"{int(percent)}")
                self.set_data('system.progress_perc_5',  f"{int(percent //  5 *  5)}")
                self.set_data('system.progress_perc_10', f"{int(percent // 10 * 10)}")
                self.set_data('system.progress_perc_20', f"{int(percent // 20 * 20)}")
                self.set_data('system.progress_perc_25', f"{int(percent // 25 * 25)}")
                self.set_data('system.progress_perc_5_or_spinner', f"{int(percent //  5 *  5)}")
                self.set_data('system.progress_perc_10_or_spinner', f"{int(percent // 10 * 10)}")
                self.set_data('system.progress_perc_20_or_spinner', f"{int(percent // 20 * 20)}")
                self.set_data('system.progress_perc_25_or_spinner', f"{int(percent // 25 * 25)}")
                self.callback_amount = int(percent)

            else:
                self.set_data('system.progress_perc', "0")
                self.set_data('system.progress_perc_5', "0")
                self.set_data('system.progress_perc_10', "0")
                self.set_data('system.progress_perc_20', "0")
                self.set_data('system.progress_perc_25', "0")
                self.set_data('system.progress_perc_5_or_spinner',  f"{(((self.spinner % 20) + 1) *  5)}")
                self.set_data('system.progress_perc_10_or_spinner', f"{(((self.spinner % 10) + 1) * 10)}")
                self.set_data('system.progress_perc_20_or_spinner', f"{(((self.spinner %  5) + 1) * 20)}")
                self.set_data('system.progress_perc_25_or_spinner', f"{(((self.spinner %  4) + 1) * 25)}")
                self.callback_amount = 0

            self.set_data("system.progress_text", message)
            self.set_data("system.progress_amount", format_progress(amount, total, fmt))

        self.callback_update()

    def message(self, message):
        self.callback_messages.append(message)
        self.callback_update()

    def message_box(self, message, want_cancel=False, ok_text=None, cancel_text=None):
        """
        Display a message box
        """

        if ok_text is None:
            ok_text = _("Okay")

        if cancel_text is None:
            cancel_text = _("Cancel")

        if self.message_box_disable:
            if want_cancel:
                return False

            return True

        ## This fixes a bug :D
        self.events.handle_events()

        with self.enable_cancellable(True):
            self.push_scene('message_box', MessageBoxScene(
                self, message, want_cancel=want_cancel, ok_text=ok_text, cancel_text=cancel_text))

            self.updated = True
            try:
                while True:
                    if self.events.was_pressed('A'):
                        return True

                    if want_cancel and self.events.was_pressed('B'):
                        if want_cancel:
                            return False

                        return True

                    self.do_loop()

            finally:
                self.pop_scene()

    def messages_begin(self, *, internal=False):
        """
        Show messages window.

        Deprecated, use `with gui.enable_messages():` instead
        """

        if not internal:
            logger.error("Using old messages begin/end api is deprecated.")

        if self.message_box_depth < 0:
            self.message_box_depth = 0
            self.callback_messages.clear()

        if self.message_box_depth == 0:
            self.message_box_scene = MessageWindowScene(self)
            self.push_scene('messages', self.message_box_scene)

        self.message_box_depth += 1

    def messages_end(self, *, internal=False):
        """
        Hide messages window.

        Deprecated, use `with gui.enable_messages():` instead
        """
        if not internal:
            logger.error("Using old messages begin/end api is deprecated.")

        self.message_box_depth -= 1
        if self.message_box_depth <= 0 and self.message_box_scene:
            self.message_box_depth = 0
            self.callback_messages.clear()
            self.message_box_scene = None
            self.callback_progress = None
            self.pop_scene()

    @contextlib.contextmanager
    def enable_messages(self):
        """
        Shows and hides the messages window.
        """
        try:
            self.messages_begin(internal=True)

            yield

            ## Fix a bug
            self.progress(None, None, None)

        finally:
            self.messages_end(internal=True)

    @contextlib.contextmanager
    def disable_messagebox(self):
        """
        Disables displaying the messagebox, should be used in conjunction with enable_cancellable(False)
        """
        old_message_state = self.message_box_disable
        try:
            self.message_box_disable = True

            yield

        finally:
            self.message_box_disable = old_message_state

    ## Scene code.
    def scene_list(self):
        return [
            scene[0]
            for scene in self.scenes]

    def all_scenes(self):
        for layer, scenes in self.scenes:
            yield from scenes

    def push_scene(self, name, scene):
        """
        Add a scene, if the name is the same as the current layer it is added to it.
        """
        if name is None:
            name = self.scenes[-1][0]

        for old_scene in self.all_scenes():
            if old_scene.active:
                # print(f"DEACTIVATE {scene}")
                old_scene.scene_deactivate()

        if name == self.scenes[-1][0]:
            logger.debug(f"PUSH SCENE ADD {name}")
            self.scenes[-1][1].append(scene)
            logger.debug(f"SCENE LIST: {self.scene_list()}")

        else:
            logger.debug(f"PUSH SCENE LAYER {name}")
            self.scenes.append((name, [scene]))

            logger.debug(f"SCENE LIST: {self.scene_list()}")

        # print(f"ACTIVATE {scene}")
        scene.scene_activate()
        self.updated = True

    def pop_scene(self, name=None):
        """
        Remove a single scene, or remove until we get back to scene named "name".
        """
        if name is None:
            # If name is none, just pop the most top scene.
            if len(self.scenes[-1][1]) > 1:
                logger.debug(f"POP SCENE REM {self.scenes[-1][0]}")

                self.scenes[-1][1].pop(-1)

                logger.debug(f"SCENE LIST: {self.scene_list()}")

                self.updated = True

            elif len(self.scenes) > 1:
                logger.debug(f"POP SCENE LAYER {self.scenes[-1][0]}")
                self.scenes.pop(-1)

                logger.debug(f"SCENE LIST: {self.scene_list()}")

                self.updated = True

        elif name == self.scenes[-1][0]:
            # If name is the active, scene, just remove a single scene layer from it.
            logger.debug(f"POP SCENE {name} REM {self.scenes[-1][0]}")
            if len(self.scenes[-1][1]) > 1:
                self.scenes[-1][1].pop(-1)
                self.updated = True

            logger.debug(f"SCENE LIST: {self.scene_list()}")

        else:

            while len(self.scenes) > 1:
                if self.scenes[-1][0] == name:
                    break

                logger.debug(f"POP SCENE {name} LAYER {self.scenes[-1][0]}")

                self.scenes.pop(-1)
                self.updated = True

            logger.debug(f"SCENE LIST: {self.scene_list()}")

        if not self.scenes[-1][1][-1].active:
            # print(f"ACTIVATE {self.scenes[-1][1][-1]}")
            self.scenes[-1][1][-1].scene_activate()
            self.updated = True

    ## Cancelling code.
    def do_cancel(self):
        """
        Cancel if it is possible
        """
        if self.cancellable is True:
            raise harbourmaster.CancelEvent()

    @contextlib.contextmanager
    def enable_cancellable(self, cancellable=False):
        """
        Controls whether you
        """
        old_cancellable = self.cancellable
        self.cancellable = cancellable
        self.was_cancelled = False

        try:
            yield

        except requests.exceptions.ConnectionError as err:
            # self.do_popup_message(f"Connection Error: {err}")
            logger.error(f"Connection Error: {err}")
            self.was_cancelled = True

        except harbourmaster.CancelEvent:
            self.was_cancelled = True

        finally:
            self.cancellable = old_cancellable

    ## Gamelist xml updater
    def update_gamelist_xml(self):
        with self.enable_cancellable(False), \
                self.enable_messages(), \
                self.hm.platform.gamelist_backup() as gamelist_xml:

            self.message(_("Fetching latest gameinfo.xml files and cover images."))

            port_updates = []

            for source_name, source in self.hm.sources.items():
                if 'gameinfo.zip' not in source.utils:
                    continue

                gameinfo_zip = (self.hm.cfg_dir / f'gameinfo_{source_name}.zip')
                do_download = False

                if gameinfo_zip.is_file():
                    md5sum = harbourmaster.hash_file(gameinfo_zip)
                    logger.debug(f"{gameinfo_zip}: {md5sum}")
                    if md5sum != source._data['gameinfo.zip']['md5']:
                        do_download = True

                else:
                    do_download = True

                if do_download:
                    self.message(_("Downloading gameinfo.xml for {source_name}").format(
                        source_name=source.name))

                    gameinfo_temp = source.download('gameinfo.zip')

                    shutil.copy(gameinfo_temp, gameinfo_zip)

                with zipfile.ZipFile(gameinfo_zip, 'r') as zf:
                    for file_info in zf.infolist():
                        file_name = self.hm.ports_dir / file_info.filename

                        if not file_name.parent.is_dir():
                            logger.debug(f"- Skipping {file_name.parent.name}/{file_name.name}")
                            continue

                        if file_name.name == 'gameinfo.xml':
                            logger.debug(f"- Updating {file_name.parent.name}/{file_name.name}")
                            port_updates.append(file_name)
                        else:
                            logger.debug(f"- Adding {file_name.parent.name}/{file_name.name}")


                        zf.extract(file_info, path=self.hm.ports_dir)

            for port_update in port_updates:
                self.hm.platform.gamelist_add(port_update)

    ## HarbourMaster Commands.
    def do_install(self, port_name, port_url=None, allow_cancel=True, md5_source=None):
        if port_url is None:
            port_url = port_name

        with self.enable_messages():
            self.message(_("Installing {port_name}").format(port_name=port_name))
            self.do_loop(no_delay=True)

            with self.enable_cancellable(allow_cancel):
                self.hm.install_port(port_url)
                self.hm.load_ports()

    def do_uninstall(self, port_name):
        with self.enable_messages():
            self.message(_("Uninstalling {port_name}").format(port_name=port_name))
            self.do_loop(no_delay=True)

            with self.enable_cancellable(False):
                self.hm.uninstall_port(port_name)
                self.delete_port_size(port_name)
                self.hm.load_ports()

    def do_update_ports(self):
        with self.enable_messages():
            with self.enable_cancellable(False):
                self.message(_('Updating all port sources:'))
                self.do_loop(no_delay=True)
                self.hm.load_info(force_load=True)
                for source in self.hm.sources:
                    self.hm.sources[source].update()

                self.hm.load_ports()

    def do_runtime_check(self, runtime_name, in_install=False):
        with self.enable_messages():
            self.message(_("Checking {runtime_name}").format(
                runtime_name=harbourmaster.runtime_nicename(runtime_name)))
            self.do_loop(no_delay=True)

            with self.enable_cancellable(True):
                self.hm.check_runtime(runtime_name, in_install=in_install)

    ## Fifo Control
    def fifo_reg_set_info(self, fifo_config, args):
        if len(args) < 3:
            fifo_config['done-file'].write_text("FAIL")
            return

        reg_name, key_name, *info_data = args
        key_info = fifo_config["register"].setdefault(reg_name, {}).setdefault(key_name, {})
        for info_datum in info_data:
            if ':' not in info_datum:
                continue

            info_key, info_value = info_datum.split(':', 1)
            key_info[info_key] = info_value

        fifo_config['done-file'].write_text("DONE")

    def fifo_reg_clear_info(self, fifo_config, args):
        if len(args) < 2:
            fifo_config['done-file'].write_text("FAIL")
            return

        reg_name, key_name, value = args[:2]
        register = fifo_config["register"].setdefault(reg_name, {})
        if key_name in register:
            del register[key_name]

        fifo_config['done-file'].write_text("DONE")

    def fifo_reg_clear(self, fifo_config, args):
        if len(args) < 1:
            fifo_config['done-file'].write_text("FAIL")
            return

        reg_name = args[0]
        if reg_name in fifo_config["register"]:
            del fifo_config["register"][reg_name]

        fifo_config['done-file'].write_text("DONE")

    def fifo_reg_dump(self, fifo_config, args):
        if len(args) < 1:
            fifo_config['done-file'].write_text("FAIL")
            return

        reg_name = args[0]
        if reg_name in fifo_config["register"]:
            fifo_config['done-file'].write_text(json.dumps(fifo_config["register"][reg_name]))

        else:
            fifo_config['done-file'].write_text("FAIL")

    def fifo_selection_list(self, fifo_config, args):
        config = {
            'want_cancel': False,
            'want_description': False,
            'want_images': False,
            'ok_text': _("Okay"),
            'cancel_text': _("Cancel"),
            }

        register = None

        i = 0
        while i < len(args):
            if not args[i].startswith('--'):
                i += 1
                continue

            if args[i] == '--':
                del args[i]
                break

            if args[i].startswith('--cancel-text='):
                config['ok_text'] = _(args[i].split('=', 1)[-1])
                del args[i]

            elif args[i].startswith('--ok-text='):
                config['ok_text'] = _(args[i].split('=', 1)[-1])
                del args[i]

            elif args[i].startswith('--register='):
                reg_name = args[i].split('=', 1)[-1]
                register = fifo_config['register'].get(reg_name, {})
                del args[i]

            elif args[i].startswith('--want-description'):
                config['want_description'] = True
                del args[i]

            elif args[i].startswith('--want-images'):
                config['want_images'] = True
                del args[i]

            else:
                logger.warning(f"unknown option {args[i]}")
                del args[i]

        ## This fixes a bug :D
        self.events.handle_events()

        with self.enable_cancellable(False):
            selection_list = DialogSelectionList(self, config, register)
            self.push_scene('selection_list', selection_list)

            try:
                while True:
                    if self.events.was_pressed('A'):
                        temp = selection_list.selected_option()
                        logger.debug(temp)
                        fifo_config['done-file'].write_text(str(temp))
                        return

                    if config['want_cancel'] and self.events.was_pressed('B'):
                        fifo_config['done-file'].write_text("CANCEL")
                        return

                    self.do_loop()

            finally:
                self.pop_scene()

        fifo_config['done-file'].write_text("FAIL")

    def fifo_messages_begin(self, fifo_config, args):
        if self.message_box_depth > 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        self.messages_begin(internal=True)
        fifo_config['done-file'].write_text("DONE")

    def fifo_messages_end(self, fifo_config, args):
        if self.message_box_depth == 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        self.messages_end(internal=True)
        fifo_config['done-file'].write_text("DONE")

    def fifo_message(self, fifo_config, args):
        if len(args) == 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        self.message(args[0])
        fifo_config['done-file'].write_text("DONE")

    def fifo_progress(self, fifo_config, args):
        if len(args) == 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        amount = 0
        total = 100
        fmt = None

        if len(args) > 3 and args[3] in ('data', '%'):
            total = args[3]

        if len(args) > 2 and args[2].isnumeric():
            total = int(args[2])

        if len(args) > 1 and args[1].isnumeric():
            amount = int(args[1])

        if amount > total:
            total, amount = amount, total

        if total == 0:
            total = 100

        self.progress(args[0], amount, total, fmt)

        fifo_config['done-file'].write_text("DONE")

    def fifo_progress_clear(self, fifo_config, args):
        self.progress(None, None, None)
        fifo_config['done-file'].write_text("DONE")

    def fifo_message_box(self, fifo_config, args):
        """
        message_box
        """
        mb_config = {
            "want_cancel": False,
            }

        if len(args) == 0:
            fifo_config['done-file'].write_text("TRUE")
            return

        i = 0
        while i < len(args):
            if not args[i].startswith('--'):
                i += 1
                continue

            if args[i] == '--':
                del args[i]
                break

            if args[i] == '--want-cancel':
                mb_config['want_cancel'] = True
                del args[i]
                continue

            if len(args) > 1:
                if args[i] == '--cancel-text':
                    mb_config['cancel_text'] = _(args[i + 1])
                    del args[i]
                    continue

                elif args[i] == '--ok-text':
                    mb_config['ok_text'] = _(args[i + 1])
                    del args[i]
                    continue

            logger.warning(f"unknown option {args[i]}")
            del args[i]

        if len(args) == 0:
            fifo_config['done-file'].write_text("TRUE")
            return

        fifo_config['done-file'].write_text("WAIT")
        result = self.message_box(args[0], **mb_config)
        fifo_config['done-file'].write_text(result and "TRUE" or "FALSE")

    def fifo_check_runtime(self, fifo_config, args):
        if self.hm is None:
            logger.debug("runtime install requested with no harbourmaster.")
            fifo_config['done-file'].write_text("FAIL")
            return

        if len(args) == 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        with self.enable_messages():
            with self.enable_cancellable(False):
                result = self.hm.check_runtime(args[0])
                fifo_config['done-file'].write_text(result and "FAIL" or "OKAY")

    def fifo_install(self, fifo_config, args):
        if self.hm is None:
            logger.debug("port install requested with no harbourmaster.")
            fifo_config['done-file'].write_text("FAIL")
            return

        if len(args) == 0:
            fifo_config['done-file'].write_text("FAIL")
            return

        with self.enable_messages():
            with self.enable_cancellable(False):
                with self.disable_messagebox():
                    result = self.hm.install_port(args[0])
                    logger.debug(f"result: {result}")
                    fifo_config['done-file'].write_text(result and "FAIL" or "OKAY")

    fifo_commands = {
        'register_set_info': fifo_reg_set_info,
        'register_clear_info': fifo_reg_clear_info,
        'register_clear': fifo_reg_clear,
        'register_dump': fifo_reg_dump,

        'selection_list': fifo_selection_list,

        'messages_begin': fifo_messages_begin,
        'messages_end': fifo_messages_end,
        'message': fifo_message,

        'progress': fifo_progress,
        'progress_clear': fifo_progress_clear,

        'message_box': fifo_message_box,
        'check_runtime': fifo_check_runtime,
        'install': fifo_install,
        }

    def do_fifo_control(self, config, argv):
        """
        {command} fifo_control /dev/shm/portmaster/pg_input /dev/shm/portmaster/pg_done > /dev/null &

        printf "begin_messages" | sudo tee /dev/shm/portmaster/pg_input > /dev/null
        printf "message\1Words go here mate." | sudo tee /dev/shm/portmaster/pg_input > /dev/null
        printf "end_messages" | sudo tee /dev/shm/portmaster/pg_input > /dev/null
        printf "message_box\1with_false\1This is a message you might want to display." | sudo tee /dev/shm/portmaster/hm_input > /dev/null

        """
        if len(argv[1]) < 2:
            return 0

        logger.info("-- Beginning Fifo Control --")

        fifo_file = Path(argv[0])
        done_file = Path(argv[1])

        if fifo_file.exists():
            fifo_file.unlink()

        if done_file.exists():
            done_file.unlink()

        fifo_config = {
            'fifo-file': fifo_file,
            'done-file': done_file,
            'register': {},
            }

        self.cancellable = False
        pipe = None

        try:
            os.mkfifo(fifo_file, mode=0o777)

            pipe = os.open(fifo_file, os.O_RDONLY | os.O_NONBLOCK)

            reader = fifo_line_reader(pipe)
            done_file.write_text("DONE")

            while True:
                args = next(reader)

                if args == "":
                    self.do_loop()
                    continue

                args = args.strip("\1").split("\1")

                done_file.write_text("WAIT")

                if args[0] == 'exit':
                    done_file.write_text("DONE")
                    return 0

                if args[0].lower() in self.fifo_commands:
                    self.fifo_commands[args[0].lower()](self, fifo_config, args[1:])

                else:
                    logger.warning(f"fifo: unknown command {args[0]}")
                    done_file.write_text("DONE")

                self.do_loop(no_delay=True)

        finally:
            if pipe is not None:
                os.close(pipe)

            if fifo_file.exists():
                fifo_file.unlink()

            logger.info("-- Endo Fifo Control --")


def portmaster_check_update(pm, config, temp_dir):
    cfg_dir = harbourmaster.HM_TOOLS_DIR / "PortMaster"

    cfg_file = cfg_dir / "config" / "config.json"
    cfg_data = {}

    if cfg_file.is_file():
        with open(cfg_file, 'r') as fh:
            cfg_data = json.load(fh)

    update_checked = cfg_data.get('update_checked', None)

    release_channel = cfg_data.setdefault('release_channel', PORTMASTER_RELEASE_CHANNEL)
    change_channel = cfg_data.setdefault('change_channel', False)

    if release_channel not in PORTMASTER_RELEASE_VALUES:
        release_channel = PORTMASTER_RELEASE_VALUES[-1]
        cfg_data['release_channel'] = release_channel

        with open(cfg_file, 'w') as fh:
            json.dump(cfg_data, fh, indent=4)

    if update_checked is None or harbourmaster.datetime_compare(update_checked) > PORTMASTER_UPDATE_FREQUENCY:
        update_checked_was_none = update_checked is None
        try:
            release_info = harbourmaster.fetch_json(PORTMASTER_RELEASE_URL + 'version.json')

        except Exception as err:
            logger.error(f'Unable to download {PORTMASTER_RELEASE_URL}version.json: {err}')
            return None

        if release_info is None:
            # Whelp, we tried.
            logger.error(f'Unable to download {PORTMASTER_RELEASE_URL}version.json')
            return False

        if release_channel not in release_info:
            logger.error(f'Unable to find {release_channel} in {release_info!r}')
            # Whelp, we tried.
            return False

        latest_version = release_info[release_channel]['version']
        if harbourmaster.version_parse(latest_version) < harbourmaster.version_parse(PORTMASTER_MIN_VERSION):
            logger.info(f"PortMaster cannot downgrade to {latest_version}, minimum version is {PORTMASTER_MIN_VERSION}")
            return False

        logger.info(f"Checking for updates: {latest_version} vs {PORTMASTER_VERSION}")

        cfg_data['update_checked'] = datetime.datetime.now().isoformat()
        if not cfg_file.parent.is_dir():
            cfg_file.parent.mkdir(0o777, parents=True)

        with open(cfg_file, 'w') as fh:
            json.dump(cfg_data, fh, indent=4)

        update_ask = False
        update_reason = ""
        update_release = False

        VERSION_NEWER = harbourmaster.version_parse(latest_version) > harbourmaster.version_parse(PORTMASTER_VERSION)
        VERSION_OLDER = harbourmaster.version_parse(latest_version) < harbourmaster.version_parse(PORTMASTER_VERSION)

        if change_channel and VERSION_OLDER:
            update_release = True
            update_ask = True
            update_reason = _("You are switching from the {old_release_channel} to the {new_release_channel} version.\n\nDo you want to downgrade?").format(
                old_release_channel=PORTMASTER_RELEASE_CHANNEL,
                new_release_channel=release_channel)

        # elif VERSION_NEWER:
            # update_ask = True
            # update_reason = _("There is a new version of PortMaster ({portmaster_version})\n\nDo you want to upgrade?").format(
                # portmaster_version=latest_version)

        elif update_checked_was_none and cfg_data.get('konami', False):
            update_ask = True
            update_reason = _("Do you want to reinstall PortMaster?\n\nThis will reinstall from the {release_channel} channel to {portmaster_version}.").format(
                portmaster_version=latest_version,
                release_channel=release_channel)

        if update_ask:
            old_check = config['no-check']
            config['no-check'] = True

            pm.hm = HarbourMaster(config, temp_dir=temp_dir, callback=pm)
            if pm.hm.platform.WANT_XBOX_FIX:
                pm.events.fix_xbox_mode()

            pm.SWAP_BUTTONS = pm.hm.platform.WANT_SWAP_BUTTONS

            if pm.message_box(update_reason, want_cancel=True):
                pm.do_install(
                    "PortMaster",
                    release_info[release_channel]['url'],
                    allow_cancel=False,
                    md5_source=release_info[release_channel]['md5'])

                if change_channel:
                    pm.hm.cfg_data['change_channel'] = False
                    pm.hm.save_config()

                reboot_file = (harbourmaster.HM_TOOLS_DIR / "PortMaster" / ".pugwash-reboot")
                if not reboot_file.is_file():
                    reboot_file.touch(0o644)

                return True

            config['no-check'] = old_check
            pm.hm = None

    return False


@logger.catch
def main(argv):
    global LOG_FILE_HANDLE
    global LOG_FILE

    with make_temp_directory() as temp_dir:
        argv = argv[:]

        config = {
            'quiet': False,
            'no-check': False,
            'debug': False,
            'no-colour': False,
            'force-colour': False,
            'no-log': False,
            'help': False,
            'offline': False,
            'no-harbour': False,
            }

        i = 1
        while i < len(argv):
            if argv[i] == '--':
                del argv[i]
                break

            if argv[i].startswith('--'):
                if argv[i][2:] in config:
                    config[argv[i][2:]] = True
                else:
                    if not config['quiet']:
                        logger.error(f"Unknown argument {argv}")

                del argv[i]
                continue

            i += 1

        if config['quiet']:
            logger.remove(0)  # For the default handler, it's actually '0'.
            logger.add(sys.stderr, level="ERROR")

        elif config['debug']:
            logger.remove(0)  # For the default handler, it's actually '0'.
            logger.add(sys.stderr, level="DEBUG")

        elif not PORTMASTER_DEBUG:
            logger.remove(0)  # For the default handler, it's actually '0'.
            logger.add(sys.stderr, level="SUCCESS")

            ## Once we reach here we can just reduce it to INFO level.
            logger.remove(LOG_FILE_HANDLE)
            LOG_FILE_HANDLE = logger.add(LOG_FILE, level="INFO", backtrace=True, diagnose=True)

        if config['no-log']:
            logger.remove(LOG_FILE_HANDLE)
            LOG_FILE_HANDLE = None

        if config['no-colour']:
            utility.do_color(False)

        elif config['force-colour']:
            utility.do_color(True)

        if not get_ip_address():
            config['offline'] = True
            logger.warning("No internet connection found, running in offline mode.")

        if len(argv) > 1 and argv[1] == "fifo_control":
            pm = PortMasterGUI(first_scene=BlankScene, force_theme="default_theme")
            pm.hm = None

            if not config['no-harbour']:
                with pm.enable_cancellable(False):
                    pm.hm = HarbourMaster(config, temp_dir=temp_dir, callback=pm)

            pm.do_fifo_control(config, argv[2:])
            pm.quit()
            return 0

        pm = PortMasterGUI()
        pm.hm = None

        if not harbourmaster.HM_TESTING and not config['no-check']:
            if portmaster_check_update(pm, config, temp_dir):
                pm.quit()
                return 0

        with pm.enable_cancellable(False):
            pm.hm = HarbourMaster(config, temp_dir=temp_dir, callback=pm)

        reboot_file = (harbourmaster.HM_TOOLS_DIR / "PortMaster" / ".pugwash-reboot")
        if not reboot_file.is_file():
            with pm.enable_cancellable(True):
                pm.run()

        if not harbourmaster.HM_TESTING:
            logger.debug(f"{pm.hm}: {pm.hm.platform.ES_NAME}")
            if pm.hm is not None and pm.hm.platform.ES_NAME is not None:
                logger.debug(f"{pm.hm.platform.ports_changed()}")
                if pm.hm.platform.ports_changed():
                    if pm.hm.platform.ES_NAME == 'show-refresh':
                        pm.message_box(_("Ports have been added or removed, update your games list to see the new games or remove the uninstalled ones." + pm.hm.platform.ROMS_REFRESH_TEXT))
                    else:
                        refresh_file = (pm.hm.tools_dir / "PortMaster" / f".{pm.hm.platform.ES_NAME}-refresh")
                        logger.debug(f"{refresh_file}")

                        refresh_file.touch(0o644, exist_ok=True)

        # if harbourmaster.HM_TESTING:
        #     for key, value in pm.text_data.items():
        #         print(f"- {key}: {value}")

        pm.quit()

        return 0


if __name__ == '__main__':
    exit(main(sys.argv))
