#!/usr/bin/env python3
#
# cdemu: command-line CDEmu client
# Copyright (C) 2006-2026 Rok Mandeljc
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.

import argparse
import configparser
import datetime
import getpass
import gettext
import os
import shlex
import subprocess
import sys
import time

import gi

# Put under if block to avoid triggering E402 warnings in subsequent imports...
if True:
    gi.require_version('GLib', '2.0')
    gi.require_version('Gio', '2.0')

from gi.repository import GLib, Gio


# *** Globals ***
app_name = "cdemu"
app_version = "3.3.0"
supported_daemon_interface_version = (7, 0)

# I18n
gettext.install(app_name)

# Set process name
if sys.platform == "linux":
    try:
        import ctypes
        libc = ctypes.CDLL("libc.so.6")
        libc.prctl(15, app_name.encode('utf-8'), 0, 0, 0)  # 15 = PR_SET_NAME
    except Exception:
        pass


# String to boolean conversion helper
def str2bool(value):
    return value.lower() in {"yes", "true", "t", "1"}


########################################################################
#                            Terminal colors                           #
########################################################################
# attrs is a comma separated list of integers, where valid values include
# foreground: 30-37, background: 40-47, reset: 0, bold: 1. (ECMA-48 / VT102)
def set_terminal_color(stream, *attrs):
    if stream.isatty():
        attrs_str = ";".join(str(attr) for attr in attrs)
        stream.write(f"\x1b[{attrs_str:s}m")


def print_header(message):
    set_terminal_color(sys.stdout, 1, 34)
    print(message, file=sys.stdout)
    set_terminal_color(sys.stdout, 0)


def print_error(message):
    set_terminal_color(sys.stderr, 1, 31)
    print(_("ERROR: %s") % (message), file=sys.stderr)
    set_terminal_color(sys.stderr, 0)


def print_warning(message):
    set_terminal_color(sys.stdout, 1, 33)
    print(_("WARNING: %s") % (message), file=sys.stdout)
    set_terminal_color(sys.stdout, 0)


########################################################################
#                                 Error                                #
########################################################################
class CDEmuError(Exception):
    pass


class CDEmuParserError(Exception):
    pass


########################################################################
#                              Load device                             #
########################################################################
class cmd_load:
    def __init__(self, subparsers):
        name = "load"
        description_msg = _("")
        help_msg = _("loads an image to the device")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "image_file",
            type=str,
            nargs="+",
            help=_("image file(s)"),
        )
        parser.add_argument(
            "--encoding",
            type=str,
            help=_("encoding for text-based images"),
        )
        parser.add_argument(
            "--unload",
            "--replace",
            action="store_true",
            dest="unload",
            default=False,
            help=_("unload image from a loaded device if necessary"),
        )
        parser.add_argument(
            "--create",
            action="store_true",
            dest="create",
            default=False,
            help=_("create a new device if necessary"),
        )

    def __call__(self, proxy, arguments):
        # We need to pass absolute filenames to daemon
        filenames = [os.path.abspath(filename) for filename in arguments.image_file]

        # Gather parser parameters into a dictionary
        parser_params = {}
        if hasattr(arguments, "encoding"):
            parser_params["encoding"] = GLib.Variant("s", arguments.encoding)

        # Particular device vs. any device
        if arguments.device == "any":
            try:
                num_devices = proxy.GetNumberOfDevices()
            except GLib.Error as e:
                raise CDEmuError(_("Failed to get number of devices: %s") % (e))

            for device in range(num_devices):
                # Device's status
                try:
                    status = proxy.DeviceGetStatus(device)
                except GLib.Error as e:
                    print_warning(_("Failed to get status of device %i: %s") % (device, e))
                    continue

                # If device is already loaded, skip it
                if status[0]:
                    continue

                # Load device
                return self.load_device(proxy, device, filenames, parser_params)

            # If we are here, it means we did not find an empty device...

            # We are allowed to unload a device; go over all devices again, and load the first device that we manage
            # to unload.
            if arguments.unload:
                for device in range(num_devices):
                    try:
                        if self.try_unload_device(proxy, device):
                            return self.load_device(proxy, device, filenames, parser_params)
                    except Exception:
                        print_warning(_("Error while trying to unload device %d: %s") % (device, e))
                        continue

            # We are allowed to create a new device.
            if arguments.create:
                # Create new device
                try:
                    proxy.AddDevice()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to add device: %s") % (e))

                # Query the number again, just in case
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                # Load the last (= newly added) device
                return self.load_device(proxy, num_devices - 1, filenames, parser_params)

            raise CDEmuError(_("No empty device found"))
        else:
            try:
                device = int(arguments.device, 0)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

            # If we are allowed to unload the device, ensure that it is unloaded before submitting load command.
            if arguments.unload:
                try:
                    self.try_unload_device(proxy, device)
                except Exception as e:
                    print_warning(_("Error while trying to unload device %d: %s") % (device, e))

            return self.load_device(proxy, device, filenames, parser_params)

    # Check if device is loaded, and try to unload it
    def try_unload_device(self, proxy, device):
        status = proxy.DeviceGetStatus(device)
        if not status[0]:
            return True  # Already unloaded

        # Request unload. Note that this does not necessarily unload the device, and even if it does, it usually happens
        # some time after the command returns. For example, when linux mounts the filesystem on the optical disc, it
        # issues a lock command to the optical drive. Therefore, the unload command only generates an event that is
        # equivalent to the press on the eject button on the physical drive, and returns immediately. The system then
        # picks up the generated event, unmounts the filesystem, unlocks the device, and ejects the disc from it.
        # Therefore, after we submit the unload command, we wait for a bit while polling the device status. We cannot be
        # too eager here, otherwise a "cdemu load --unload any <file>" might cause all devices to be unloaded and still
        # report failing to find an empty device. On the other hand, we also do not want to wait forever...
        TIMEOUT = 15  # seconds
        proxy.DeviceUnload(device)

        start_time = time.time()
        while True:
            elapsed = time.time() - start_time

            # Query status
            status = proxy.DeviceGetStatus(device)
            if not status[0]:
                return True  # Unloaded

            # Timeout
            if elapsed > TIMEOUT:
                break

            # Wait...
            time.sleep(0.25)

        return False

    # Device loading with password query support
    def load_device(self, proxy, device, filenames, params):
        # Try to load it
        try:
            proxy.DeviceLoad(device, filenames, params)
        except GLib.Error as e:
            if "net.sf.cdemu.CDEmuDaemon.errorMirage.EncryptedImage" in str(e):
                # We need password to load the image, so ask the user for it
                try:
                    password = self.ask_for_password()
                except Exception as e:
                    raise CDEmuError(_("Failed to obtain password for encrypted image: %s") % (e, ))

                # Append password to params
                params["password"] = GLib.Variant("s", password)
                return self.load_device(proxy, device, filenames, params)
            else:
                raise CDEmuError(_("Failed to load image: %s") % (e))

    def ask_for_password(self):
        def _get_command_output(command_args):
            output = subprocess.run(
                command_args,
                check=True,
                text=True,
                stdout=subprocess.PIPE,
            ).stdout

            # Remove the trailing newline, if preset. Some commands,
            # such as zenity, seem to include it in their output.
            if output.endswith('\n'):
                output = output[:-1]

            return output

        # If zenity integration is enabled via CDEMU_USE_ZENITY environment
        # variable, use zenity dialog to ask for password. While this could
        # also be achieved by setting CDEMU_ASKPASS to "zenity --password",
        # we can better customize the dialog in this dedicated codepath.
        if os.environ.get("CDEMU_USE_ZENITY", "0") != "0":
            try:
                command_args = [
                    "zenity",
                    "--entry",
                    "--title",
                    _("CDEmu client"),
                    "--text",
                    _("Please enter password for encrypted image"),
                    "--hide-text",
                ]
                return _get_command_output(command_args)
            except Exception as e:
                raise CDEmuError(_("Failed to obtain password via zenity: %s") % (e, ))

        # If CDEMU_ASKPASS is defined, use it.
        if "CDEMU_ASKPASS" in os.environ:
            try:
                command_args = shlex.split(os.environ["CDEMU_ASKPASS"])
                return _get_command_output(command_args)
            except Exception as e:
                raise CDEmuError(_("Failed to run password agent defined via CDEMU_ASKPASS environment variable: %s") % (e, ))

        # If we have console available, use `getpass`.
        if sys.stdin.isatty():
            print(_("The image you are trying to load is encrypted."))
            return getpass.getpass(_("Password: "))

        # Otherwise, use the command defined by SSH_ASKPASS, if available.
        if "SSH_ASKPASS" in os.environ:
            try:
                command_args = shlex.split(os.environ["SSH_ASKPASS"])
                return _get_command_output(command_args)
            except Exception as e:
                raise CDEmuError(_("Failed to run password agent defined via SSH_ASKPASS environment variable: %s") % (e, ))

        raise CDEmuError(_("No password agent was supplied."))


########################################################################
#                              Create blank                            #
########################################################################
class cmd_create_blank:
    def __init__(self, subparsers):
        name = "create-blank"
        description_msg = _("")
        help_msg = _("load blank/recordable image to the device")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "image_file",
            type=str,
            help=_("image filename/basename"),
        )
        parser.add_argument(
            "--writer-id",
            type=str,
            nargs="?",
            required=True,
            help=_("ID of image writer to use"),
        )
        parser.add_argument(
            "--medium-type",
            type=str,
            nargs="?",
            choices=['cdr74', 'cdr80', 'cdr90', 'cdr99', 'dvd+r', 'bdr'],
            help=_("medium type"),
        )
        parser.add_argument(
            "--param",
            type=str,
            nargs="?",
            action='append',
            help=_("additional writer parameter(s)"),
        )

    def __call__(self, proxy, arguments):
        # We need to pass absolute filename/basename to daemon
        filename = os.path.abspath(arguments.image_file)

        # Gather writer parameters into a dictionary
        parameters = {}
        if hasattr(arguments, "writer_id"):
            parameters["writer-id"] = GLib.Variant("s", arguments.writer_id)
        if hasattr(arguments, "medium_type"):
            parameters["medium-type"] = GLib.Variant("s", arguments.medium_type)
        if hasattr(arguments, "param"):
            # Process parameters
            writer_parameters = self.process_writer_parameters(arguments.param)
            # Validate parameters using writer's parameter sheet
            writer_parameters = self.validate_writer_parameters(
                proxy,
                arguments.writer_id,
                writer_parameters,
            )
            # Set to parameters dictionary that we will send to daemon
            parameters.update(writer_parameters)

        # Particular device vs. any device
        if arguments.device == "any":
            try:
                num_devices = proxy.GetNumberOfDevices()
            except GLib.Error as e:
                raise CDEmuError(_("Failed to get number of devices: %s") % (e))

            for device in range(num_devices):
                # Device's status
                try:
                    status = proxy.DeviceGetStatus(device)
                except GLib.Error as e:
                    print_warning(_("Failed to get status of device %i: %s") % (device, e))
                    continue

                # If device is already loaded, skip it
                if status[0]:
                    continue

                # Load device
                return self.load_device(proxy, device, filename, parameters)
            else:
                # If we're here, it means we didn't get an empty device
                raise CDEmuError(_("No empty device found"))
        else:
            try:
                device = int(arguments.device, 0)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

            return self.load_device(proxy, device, filename, parameters)

    # Device loading
    def load_device(self, proxy, device, filenames, params):
        # Try to load it
        try:
            proxy.DeviceCreateBlank(device, filenames, params)
        except GLib.Error as e:
            raise CDEmuError(_("Failed to load blank image: %s") % (e))

    # Image writer parameter processing
    def process_writer_parameters(self, parameter_strings):
        parameters = {}

        # In general, we can be given multiple strings, one for each
        # command-line argument...
        for parameter_string in parameter_strings:
            # ... and to make things interesting, each string can
            # contain several "key=value" pairs, separated by semi-colon
            parameter_tokens = parameter_string.split(";")

            for parameter_token in parameter_tokens:
                # Split into key and value
                try:
                    key, value = parameter_token.split("=")
                except ValueError:
                    raise CDEmuError(_("Parameter token '%s' is not a key=value pair!") % parameter_token)

                # Strip whitespaces and insert into dictionary
                parameters[key.strip()] = value.strip()

        return parameters

    # Writer parameter validation
    def validate_writer_parameters(self, proxy, writer_id, parameters):
        # Grab writer's parameter sheet
        try:
            parameter_sheet = proxy.EnumWriterParameters(writer_id)
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get parameter sheet for writer: %s") % (e))

        # Convert the XML tree into a dictionary for convenience
        valid_parameters = {}
        for parameter_entry in parameter_sheet:
            valid_parameters[parameter_entry[0]] = parameter_entry

        # Now, go over given parameters and validate them
        # NOTE: we need to use list(parameters.items()) because we may
        # change the dictionary (pop() call during validation), and that
        # raises error in python3
        for key, value in list(parameters.items()):
            try:
                parameter_entry = valid_parameters[key]
            except KeyError:
                print_warning(_("Parameter '%s' not found in writer's parameter sheet!") % (key))
                parameters.pop(key, '')  # Remove from dictionary
                continue

            # Determine type
            if len(parameter_entry[4]):
                # Enum; no conversion to be done, but check if value is
                # on the list of valid values, and if not, print a warning
                for enum_value in parameter_entry[4]:
                    if value == enum_value:
                        parameters[key] = GLib.Variant("s", value)
                        break
                else:
                    print_warning(_("Invalid value '%s' given for parameter '%s'!") % (value, key))
            elif isinstance(parameter_entry[3], str):
                # String
                parameters[key] = GLib.Variant("s", value)
            elif isinstance(parameter_entry[3], bool):
                # Boolean; convert string value to boolean
                parameters[key] = GLib.Variant("b", str2bool(value))
            elif isinstance(parameter_entry[3], int):
                # Convert string value to integer
                try:
                    parameters[key] = GLib.Variant("i", int(value, 0))
                except ValueError:
                    raise CDEmuError(
                        _("String '%s' given as value for parameter '%s' is not a number!") % (value, key))
            else:
                print_warning(
                    _("Unhandled parameter type '%s' declared for parameter '%s'!") %
                    (type(parameter_entry[3]), key),
                )
                continue

        return parameters


########################################################################
#                             Unload device                            #
########################################################################
class cmd_unload:
    def __init__(self, subparsers):
        name = "unload"
        description_msg = _("")
        help_msg = _("unloads the device")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )

    def __call__(self, proxy, arguments):
        # Particular device vs. all devices
        if arguments.device == "all":
            try:
                num_devices = proxy.GetNumberOfDevices()
            except GLib.Error as e:
                raise CDEmuError(_("Failed to get number of devices: %s") % (e))

            for device in range(num_devices):
                try:
                    proxy.DeviceUnload(device)
                except GLib.Error as e:
                    print_warning(_("Failed to unload device %i: %s") % (device, e))
                    continue
        else:
            try:
                device = int(arguments.device, 0)
                proxy.DeviceUnload(device)
            except GLib.Error as e:
                raise CDEmuError(_("Failed to unload device %i: %s") % (device, e))
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments.device))


########################################################################
#                            Display status                            #
########################################################################
class cmd_display_status:
    def __init__(self, subparsers):
        name = "status"
        description_msg = _("")
        help_msg = _("displays the devices' status")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Print status for all devices
        try:
            num_devices = proxy.GetNumberOfDevices()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get number of devices: %s") % (e))

        print_header(_("Devices' status:"))
        print("%-5s %-10s %s" % (_("DEV"), _("LOADED"), _("FILENAME")))
        for device in range(num_devices):
            try:
                loaded, filenames = proxy.DeviceGetStatus(device)
            except GLib.Error as e:
                print_warning(_("Failed to get status of device %i: %s") % (device, e))
                continue

            if not loaded:
                filenames = [""]

            # First line is for all device's data, the rest are for additional filenames
            print("%-5s %-10s %s" % (device, loaded, filenames[0]))
            for filename in filenames[1:]:
                print("%-5s %-10s %s" % ("", "", filename))


########################################################################
#                            Device mapping                            #
########################################################################
class cmd_device_mapping:
    def __init__(self, subparsers):
        name = "device-mapping"
        description_msg = _("")
        help_msg = _("displays the device mapping information")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Print device mapping for all devices
        try:
            num_devices = proxy.GetNumberOfDevices()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get number of devices: %s") % (e))

        print_header(_("Device mapping:"))
        print("%-5s %-15s %-15s" % (_("DEV"), _("SCSI CD-ROM"), _("SCSI generic")))
        for device in range(num_devices):
            try:
                dev_sr, dev_sg = proxy.DeviceGetMapping(device)
            except GLib.Error as e:
                print_warning(_("Failed to get device mapping of device %i: %s") % (device, e))
                continue

            print("%-5s %-15s %-15s" % (device, dev_sr, dev_sg))


########################################################################
#                           Daemon debug mask                          #
########################################################################
class cmd_daemon_debug_mask:
    def __init__(self, subparsers):
        name = "daemon-debug-mask"
        description_msg = _("")
        help_msg = _("displays/sets daemon debug mask")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Retrieve list of valid masks (for encoding/decoding)
        mask_mapping = self._get_debug_mask_mapping(proxy)

        # Set daemon debug mask
        if hasattr(arguments, "new_value"):
            # Convert the input value to mask
            mask = self._convert_input_to_mask(arguments.new_value, mask_mapping)

            # Set the mask
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting daemon debug mask of all devices to 0x%X.") % (mask))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "daemon-debug-mask", GLib.Variant("i", mask))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set daemon debug mask of device %i to 0x%X: %s") %
                            (device, mask, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting daemon debug mask of device %i to 0x%X.") % (device, mask))
                try:
                    proxy.DeviceSetOption(device, "daemon-debug-mask", GLib.Variant("i", mask))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set daemon debug mask of device %i to 0x%X: %s") %
                        (device, mask, e),
                    )

        # Get daemon debug mask
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' daemon debug masks:"))
                print("%-5s %-10s   %s" % (_("DEV"), _("DEBUG MASK"), _("MEANING")))

                for device in range(num_devices):
                    try:
                        mask = proxy.DeviceGetOption(device, "daemon-debug-mask")
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to get daemon debug mask of device %i: %s") %
                            (device, e),
                        )
                        continue

                    mask_meaning = self._convert_mask_to_string(mask, mask_mapping)

                    print("%-5s 0x%08X   %s" % (device, mask, mask_meaning))
            else:
                try:
                    device = int(arguments.device, 0)
                    mask = proxy.DeviceGetOption(device, "daemon-debug-mask")
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to get daemon debug mask of device %i: %s") %
                        (device, e),
                    )
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                mask_meaning = self._convert_mask_to_string(mask, mask_mapping)

                print(
                    _("Daemon debug mask of device %i: 0x%X (%s)") %
                    (device, mask, mask_meaning),
                )

    def _convert_input_to_mask(self, input_str, mapping):
        output_mask = 0

        # Split input string on pipe (|)
        tokens = input_str.split("|")
        for token in tokens:
            token = token.strip()

            try:
                if token in mapping:
                    val = mapping[token]
                else:
                    val = int(token, 0)
            except ValueError:
                raise CDEmuError(
                    _("String '%s' is not a number nor was it found in the list of valid debug mask identifiers") %
                    (token)
                )

            output_mask |= val

        return output_mask

    def _convert_mask_to_string(self, value, mapping):
        output_list = [
            mask_name for mask_name, mask_value in mapping.items()
            if value & mask_value != 0
        ]
        return "|".join(output_list)

    def _get_debug_mask_mapping(self, proxy):
        try:
            debug_masks = proxy.EnumDaemonDebugMasks()
            mapping = {
                mask_entry[0]: mask_entry[1]
                for mask_entry in debug_masks
            }
        except GLib.Error as e:
            print_warning(_("Failed to enumerate supported daemon debug masks: %s") % (e))
            mapping = {}

        return mapping


########################################################################
#                          Library debug mask                          #
########################################################################
class cmd_library_debug_mask:
    def __init__(self, subparsers):
        name = "library-debug-mask"
        description_msg = _("")
        help_msg = _("displays/sets library debug mask")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )

    def __call__(self, proxy, arguments):
        # Retrieve list of valid masks (for encoding/decoding)
        mask_mapping = self._get_debug_mask_mapping(proxy)

        # Set debug mask
        if hasattr(arguments, "new_value"):
            # Convert the input value to mask
            mask = self._convert_input_to_mask(arguments.new_value, mask_mapping)

            # Set the mask
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting library debug mask of all devices to 0x%X.") % (mask))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "library-debug-mask", GLib.Variant("i", mask))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set library debug mask of device %i to 0x%X: %s") %
                            (device, mask, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting library debug mask of device %i to 0x%X.") % (device, mask))
                try:
                    proxy.DeviceSetOption(device, "library-debug-mask", GLib.Variant("i", mask))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set library debug mask of device %i to 0x%X: %s") %
                        (device, mask, e),
                    )

        # Get debug mask
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' library debug masks:"))
                print("%-5s %-10s   %s" % (_("DEV"), _("DEBUG MASK"), _("MEANING")))

                for device in range(num_devices):
                    try:
                        mask = proxy.DeviceGetOption(device, "library-debug-mask")
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to get library debug mask of device %i: %s") %
                            (device, e),
                        )

                    mask_meaning = self._convert_mask_to_string(mask, mask_mapping)

                    print("%-5s 0x%08X   %s" % (device, mask, mask_meaning))
            else:
                try:
                    device = int(arguments.device, 0)
                    mask = proxy.DeviceGetOption(device, "library-debug-mask")
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to get library debug mask of device %i: %s") %
                        (device, e),
                    )
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (mask))

                mask_meaning = self._convert_mask_to_string(mask, mask_mapping)

                print(
                    _("Library debug mask of device %i: 0x%X (%s)") %
                    (device, mask, mask_meaning),
                )

    def _convert_input_to_mask(self, input_str, mapping):
        output_mask = 0

        # Split input string on pipe (|)
        tokens = input_str.split("|")
        for token in tokens:
            token = token.strip()

            try:
                if token in mapping:
                    val = mapping[token]
                else:
                    val = int(token, 0)
            except ValueError:
                raise CDEmuError(
                    _("String '%s' is not a number nor was it found in the list of valid debug mask identifiers") %
                    (token),
                )

            output_mask |= val

        return output_mask

    def _convert_mask_to_string(self, value, mapping):
        output_list = [
            mask_name for mask_name, mask_value in mapping.items()
            if value & mask_value != 0
        ]
        return "|".join(output_list)

    def _get_debug_mask_mapping(self, proxy):
        try:
            debug_masks = proxy.EnumLibraryDebugMasks()
            mapping = {
                mask_entry[0]: mask_entry[1]
                for mask_entry in debug_masks
            }
        except GLib.Error as e:
            print_warning(_("Failed to enumerate supported library debug masks: %s") % (e))
            mapping = {}

        return mapping


########################################################################
#                            DPM emulation                            #
########################################################################
class cmd_dpm_emulation:
    def __init__(self, subparsers):
        name = "dpm-emulation"
        description_msg = _("")
        help_msg = _("displays/sets DPM emulation flag")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )

    def __call__(self, proxy, arguments):
        # Set DPM emulation flag
        if hasattr(arguments, "new_value"):
            try:
                enabled = str2bool(arguments.new_value)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments.new_value))

            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting DPM emulation flag of all devices to %i.") % (enabled))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "dpm-emulation", GLib.Variant("b", enabled))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set DPM emulation flag of device %i to %i: %s") %
                            (device, enabled, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting DPM emulation flag of device %i to %i.") % (device, enabled))
                try:
                    proxy.DeviceSetOption(device, "dpm-emulation", GLib.Variant("b", enabled))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set DPM emulation flag of device %i to %i: %s") %
                        (device, enabled, e),
                    )

        # Get DPM emulation flag
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' DPM emulation flag:"))
                print("%-5s %-10s" % (_("DEV"), _("ENABLED")))

                for device in range(num_devices):
                    try:
                        enabled = proxy.DeviceGetOption(device, "dpm-emulation")
                    except GLib.Error as e:
                        print_warning(_("Failed to get DPM emulation flag of device %i: %s") % (device, e))
                        continue

                    print("%-5s %i" % (device, enabled))
            else:
                try:
                    device = int(arguments.device, 0)
                    enabled = proxy.DeviceGetOption(device, "dpm-emulation")
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get DPM emulation flag of device %i: %s") % (device, e))
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("DPM emulation flag of device %i: %i") % (device, enabled))


########################################################################
#                        Transfer rate emulation                       #
########################################################################
class cmd_tr_emulation:
    def __init__(self, subparsers):
        name = "tr-emulation"
        description_msg = _("")
        help_msg = _("displays/sets transfer rate emulation flag")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )

    def __call__(self, proxy, arguments):
        # Set TR emulation flag
        if hasattr(arguments, "new_value"):
            try:
                enabled = str2bool(arguments.new_value)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments[1]))

            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting transfer rate emulation flag of all devices to %i.") % (enabled))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "tr-emulation", GLib.Variant("b", enabled))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set transfer rate emulation flag of device %i to %i: %s") %
                            (device, enabled, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting transfer rate emulation flag of device %i to %i.") % (device, enabled))
                try:
                    proxy.DeviceSetOption(device, "tr-emulation", GLib.Variant("b", enabled))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set transfer rate emulation flag of device %i to %i: %s") %
                        (device, enabled, e),
                    )

        # Get TR emulation flag
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Devices' transfer rate emulation flag:"))
                print("%-5s %-10s" % (_("DEV"), _("ENABLED")))

                for device in range(num_devices):
                    try:
                        enabled = proxy.DeviceGetOption(device, "tr-emulation")
                    except GLib.Error as e:
                        print_warning(_("Failed to get transfer rate emulation flag of device %i: %s") % (device, e))
                        continue

                    print("%-5s %i" % (device, enabled))
            else:
                try:
                    device = int(arguments.device, 0)
                    enabled = proxy.DeviceGetOption(device, "tr-emulation")
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get transfer rate emulation flag of device %i: %s") % (device, e))
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Transfer rate emulation flag of device %i: %i") % (device, enabled))


########################################################################
#                         Bad sector emulation                         #
########################################################################
class cmd_bad_sector_emulation:
    def __init__(self, subparsers):
        name = "bad-sector-emulation"
        description_msg = _("")
        help_msg = _("displays/sets bad sector emulation flag")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )

    def __call__(self, proxy, arguments):
        # Set bad sector emulation flag
        if hasattr(arguments, "new_value"):
            try:
                enabled = str2bool(arguments.new_value)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments[1]))

            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting bad sector emulation flag of all devices to %i.") % (enabled))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "bad-sector-emulation", GLib.Variant("b", enabled))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set bad sector emulation flag of device %i to %i: %s") %
                            (device, enabled, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting bad sector emulation flag of device %i to %i.") % (device, enabled))
                try:
                    proxy.DeviceSetOption(device, "bad-sector-emulation", GLib.Variant("b", enabled))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set bad sector emulation flag of device %i to %i: %s") %
                        (device, enabled, e),
                    )

        # Get bad sector emulation flag
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' bad sector emulation flag:"))
                print("%-5s %-10s" % (_("DEV"), _("ENABLED")))

                for device in range(num_devices):
                    try:
                        enabled = proxy.DeviceGetOption(device, "bad-sector-emulation")
                    except GLib.Error as e:
                        print_warning(_("Failed to get bad sector emulation flag of device %i: %s") % (device, e))
                        continue

                    print("%-5s %i" % (device, enabled))
            else:
                try:
                    device = int(arguments.device, 0)
                    enabled = proxy.DeviceGetOption(device, "bad-sector-emulation")
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get bad sector emulation flag of device %i: %s") % (device, e))
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Bad sector emulation flag of device %i: %i") % (device, enabled))


########################################################################
#                            DVD report CSS                            #
########################################################################
class cmd_dvd_report_css:
    def __init__(self, subparsers):
        name = "dvd-report-css"
        description_msg = _("")
        help_msg = _("displays/sets DVD report CSS/CPPM flag")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_value",
            nargs="?",
            type=str,
            help=_("new value"),
        )

    def __call__(self, proxy, arguments):
        # Set DVD report CSS flag
        if hasattr(arguments, "new_value"):
            try:
                enabled = str2bool(arguments.new_value)
            except ValueError:
                raise CDEmuError(_("String '%s' is not a number") % (arguments[1]))

            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting DVD report CSS/CPPM flag of all devices to %i.") % (enabled))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "dvd-report-css", GLib.Variant("b", enabled))
                    except GLib.Error as e:
                        print_warning(
                            _("Failed to set DVD report CSS/CPPM flag of device %i to %i: %s") %
                            (device, enabled, e),
                        )
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting DVD report CSS/CPPM flag of device %i to %i.") % (device, enabled))
                try:
                    proxy.DeviceSetOption(device, "dvd-report-css", GLib.Variant("b", enabled))
                except GLib.Error as e:
                    raise CDEmuError(
                        _("Failed to set DVD report CSS/CPPM flag of device %i to %i: %s") %
                        (device, enabled, e),
                    )

        # Get DVD report CSS flag
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' DVD report CSS/CPPM flag:"))
                print("%-5s %-10s" % (_("DEV"), _("ENABLED")))

                for device in range(num_devices):
                    try:
                        enabled = proxy.DeviceGetOption(device, "dvd-report-css")
                    except GLib.Error as e:
                        print_warning(_("Failed to get DVD report CSS/CPPM flag of device %i: %s") % (device, e))
                        continue

                    print("%-5s %i" % (device, enabled))
            else:
                try:
                    device = int(arguments.device, 0)
                    enabled = proxy.DeviceGetOption(device, "dvd-report-css")
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get DVD report CSS/CPPM flag of device %i: %s") % (device, e))
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("DVD report CSS/CPPM flag of device %i: %i") % (device, enabled))


########################################################################
#                               Device ID                              #
########################################################################
class cmd_device_id:
    def __init__(self, subparsers):
        name = "device-id"
        description_msg = _("")
        help_msg = _("displays/sets device ID")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument(
            "device",
            type=str,
            help=_("device"),
        )
        parser.add_argument(
            "new_id",
            nargs='*',
            type=str,
            help=_("new device ID: vendor ID, product ID, revision and vendor-specific string"),
        )

    def __call__(self, proxy, arguments):
        # Set device ID
        if hasattr(arguments, "new_id"):
            device_id = tuple(arguments.new_id)

            if len(device_id) != 4:
                raise CDEmuError(_("New device ID must consist of exactly four strings!"))

            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print(_("Setting device ID of all devices to %s.") % (device_id))
                for device in range(num_devices):
                    try:
                        proxy.DeviceSetOption(device, "device-id", GLib.Variant("(ssss)", device_id))
                    except GLib.Error as e:
                        print_warning(_("Failed to set device ID of device %i to %s: %s") % (device, device_id, e))
                        continue
            else:
                try:
                    device = int(arguments.device, 0)
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Setting device ID of device %i to %s.") % (device, device_id))
                try:
                    proxy.DeviceSetOption(device, "device-id", GLib.Variant("(ssss)", device_id))
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to set device ID of device %i to %s: %s") % (device, device_id, e))

        # Get device ID
        else:
            # Particular device vs. all devices
            if arguments.device == "all":
                try:
                    num_devices = proxy.GetNumberOfDevices()
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get number of devices: %s") % (e))

                print_header(_("Devices' IDs:"))
                print("%-5s %s" % (_("DEV"), _("DEVICE ID")))

                for device in range(num_devices):
                    try:
                        values = proxy.DeviceGetOption(device, "device-id")
                        device_id = [str(value) for value in values]
                    except GLib.Error as e:
                        print_warning(_("Failed to get device ID of device %i: %s") % (device, e))
                        continue

                    print("%-5s %s" % (device, device_id))
            else:
                try:
                    device = int(arguments.device, 0)
                    values = proxy.DeviceGetOption(device, "device-id")
                    device_id = [str(value) for value in values]
                except GLib.Error as e:
                    raise CDEmuError(_("Failed to get device ID of device %i: %s") % (device, e))
                except ValueError:
                    raise CDEmuError(_("String '%s' is not a number") % (arguments.device))

                print(_("Device ID of device %i: %s") % (device, device_id))


########################################################################
#                          Enumerate parsers                           #
########################################################################
class cmd_enum_parsers:
    def __init__(self, subparsers):
        name = "enum-parsers"
        description_msg = _("")
        help_msg = _("enumerates supported parsers")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Display supported parsers
        try:
            parsers = proxy.EnumSupportedParsers()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to enumerate supported parsers: %s") % (e))
            return False

        # Print all parsers
        print_header(_("Supported parsers:"))
        for info in parsers:
            print("  %s: %s" % (info[0], info[1]))
            for description, mime_type in info[2]:
                print("    %s: %s" % (mime_type, description))


########################################################################
#                           Enumerate writers                          #
########################################################################
class cmd_enum_writers:
    def __init__(self, subparsers):
        name = "enum-writers"
        description_msg = _("")
        help_msg = _("enumerates supported writers")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Display supported writers
        try:
            writers = proxy.EnumSupportedWriters()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to enumerate supported writers: %s") % (e))

        # Print all writers
        print_header(_("Supported writers:"))
        for info in writers:
            print("  %s: %s" % (info[0], info[1]))


########################################################################
#                       Enumerate filter streams                       #
########################################################################
class cmd_enum_filter_streams:
    def __init__(self, subparsers):
        name = "enum-filter-streams"
        description_msg = _("")
        help_msg = _("enumerates supported filter streams")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Display supported filter streams
        try:
            filter_streams = proxy.EnumSupportedFilterStreams()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to enumerate supported filter streams: %s") % (e))

        # Print all filter streams
        print_header(_("Supported filter streams:"))
        for info in filter_streams:
            print("  %s: %s" % (info[0], info[1]))
            print(_("    write support: %d") % (info[2]))
            for description, mime_type in info[3]:
                print("    %s: %s" % (mime_type, description))


########################################################################
#                     Enumerate daemon debug masks                     #
########################################################################
class cmd_enum_daemon_debug_masks:
    def __init__(self, subparsers):
        name = "enum-daemon-debug-masks"
        description_msg = _("")
        help_msg = _("enumerates valid daemon debug masks")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Print module's debug masks
        try:
            debug_masks = proxy.EnumDaemonDebugMasks()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to enumerate supported daemon debug masks: %s") % (e))

        print_header(_("Supported daemon debug masks:"))
        for debug_mask in debug_masks:
            print("  %-25s: 0x%04X" % (debug_mask[0], debug_mask[1]))


########################################################################
#                     Enumerate library debug masks                    #
########################################################################
class cmd_enum_library_debug_masks:
    def __init__(self, subparsers):
        name = "enum-library-debug-masks"
        description_msg = _("")
        help_msg = _("enumerates valid library debug masks")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Print module's debug masks
        try:
            debug_masks = proxy.EnumLibraryDebugMasks()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to enumerate supported library debug masks: %s") % (e))

        print_header(_("Supported library debug masks:"))
        for debug_mask in debug_masks:
            print("  %-25s: 0x%04X" % (debug_mask[0], debug_mask[1]))


########################################################################
#                      Enumerate writer parameters                     #
########################################################################
class cmd_enum_writer_parameters:
    def __init__(self, subparsers):
        name = "enum-writer-parameters"
        description_msg = _("")
        help_msg = _("prints the list of writer's parameters")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

        parser.add_argument("writer", type=str, help=_("writer ID"))

    def __call__(self, proxy, arguments):
        # Get parameter sheet for the specified writer
        try:
            parameter_sheet = proxy.EnumWriterParameters(arguments.writer)
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get parameter sheet for writer: %s") % (e))

        # Print the sheet
        print_header(_("Writer's parameter sheet:"))

        for parameter in parameter_sheet:
            print("")
            print_header("%s" % (parameter[0]))
            print(_("  Name: %s") % (parameter[1]))
            print(_("  Description: %s") % (parameter[2]))
            print(_("  Type: %s") % (self._parameter_type_name(parameter)))
            print(_("  Default value: %s") % (parameter[3]))

            for enum_value in parameter[4]:
                print(_("  Possible value: %s") % (enum_value))

    @staticmethod
    def _parameter_type_name(parameter):
        if len(parameter[4]):
            return _("enum")
        elif isinstance(parameter[3], str):
            return _("string")
        elif isinstance(parameter[3], bool):
            return _("boolean")
        elif isinstance(parameter[3], int):
            return _("integer")
        else:
            return _("unknown")


########################################################################
#                               Version                                #
########################################################################
class cmd_version:
    def __init__(self, subparsers):
        name = "version"
        description_msg = "Displays daemon and library version information."
        help_msg = _("displays version information")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        # Print version information
        try:
            library_version = proxy.GetLibraryVersion()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get library version: %s") % (e))

        try:
            daemon_version = proxy.GetDaemonVersion()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to get daemon version: %s") % (e))

        print(_("Library version: %s") % (library_version))
        print(_("Daemon version: %s") % (daemon_version))


########################################################################
#                              Add device                              #
########################################################################
class cmd_add_device:
    def __init__(self, subparsers):
        name = "add-device"
        description_msg = _("")
        help_msg = _("creates another virtual device")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        try:
            proxy.AddDevice()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to add device: %s") % (e))

        print(_("Device added successfully."))


########################################################################
#                            Remove device                             #
########################################################################
class cmd_remove_device:
    def __init__(self, subparsers):
        name = "remove-device"
        description_msg = _("")
        help_msg = _("removes the last virtual device")

        parser = subparsers.add_parser(
            name,
            description=description_msg,
            help=help_msg,
            formatter_class=argparse.ArgumentDefaultsHelpFormatter,
            argument_default=argparse.SUPPRESS,
        )
        parser.set_defaults(command_function=self)

    def __call__(self, proxy, arguments):
        try:
            proxy.RemoveDevice()
        except GLib.Error as e:
            raise CDEmuError(_("Failed to remove device: %s") % (e))

        print(_("Device removed successfully."))


########################################################################
#                         Command-line parser                          #
########################################################################
class CDEmuCommandLineParser(argparse.ArgumentParser):
    def error(self, message):
        self.print_usage(sys.stderr)
        print_error(message)
        sys.exit(2)


class ActionDeviceId(argparse.Action):
    def __call__(self, parser, namespace, values, option_string=None):
        print("%r %r %r" % (namespace, values, option_string))
        setattr(namespace, self.dest, values)


def create_parser():
    parser = CDEmuCommandLineParser(
        description=_("Command-line CDEmu client."),
        formatter_class=argparse.ArgumentDefaultsHelpFormatter,
        argument_default=argparse.SUPPRESS,
    )
    parser.add_argument(
        "--bus",
        metavar="BUS",
        choices=["system", "session"],
        help=_("sets D-BUS bus type to use"),
    )
    parser.add_argument(
        "--version",
        action="version",
        version="%s %s - (C) 2006-%d Rok Mandeljc" % (app_name, app_version, datetime.date.today().year),
    )

    subparsers = parser.add_subparsers(
        title=_("valid commands"),
        metavar="command",
        help=_("command help"),
    )
    subparsers.required = True

    # Command sub-parsers
    cmd_load(subparsers)  # Load
    cmd_create_blank(subparsers)  # Create blank
    cmd_unload(subparsers)  # Unload
    cmd_display_status(subparsers)  # Status
    cmd_add_device(subparsers)  # Add device
    cmd_remove_device(subparsers)  # Remove device
    cmd_device_mapping(subparsers)  # Device mapping
    cmd_daemon_debug_mask(subparsers)  # Daemon debug mask
    cmd_library_debug_mask(subparsers)  # Library debug mask
    cmd_dpm_emulation(subparsers)  # DPM emulation
    cmd_tr_emulation(subparsers)  # TR emulation
    cmd_bad_sector_emulation(subparsers)  # Bad sector emulation
    cmd_dvd_report_css(subparsers)  # DVD report CSS/CPPM
    cmd_device_id(subparsers)  # Device ID
    cmd_enum_parsers(subparsers)  # Enumerate parsers
    cmd_enum_writers(subparsers)  # Enumerate writers
    cmd_enum_filter_streams(subparsers)  # Enumerate filter streams
    cmd_enum_daemon_debug_masks(subparsers)  # Enumerate daemon debug masks
    cmd_enum_library_debug_masks(subparsers)  # Enumerate library debug masks
    cmd_enum_writer_parameters(subparsers)  # Enumerate writer parameters
    cmd_version(subparsers)  # Version

    return parser


########################################################################
#               CDEmuDaemonProxy: Daemon proxy object                  #
########################################################################
class CDEmuDaemonProxy:
    _name = 'net.sf.cdemu.CDEmuDaemon'
    _object_path = '/Daemon'

    def __init__(self, bus_type):
        try:
            if bus_type == "system":
                bus = Gio.bus_get_sync(Gio.BusType.SYSTEM, None)
            elif bus_type == "session":
                bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)
            else:
                print_warning(_("Invalid bus parameter '%s', using default!") % (bus_type))
                bus = Gio.bus_get_sync(Gio.BusType.SESSION, None)  # Use session bus by default

            self.proxy = Gio.DBusProxy.new_sync(
                bus,
                0,
                None,
                "net.sf.cdemu.CDEmuDaemon",
                "/Daemon",
                "net.sf.cdemu.CDEmuDaemon",
                None,
            )
        except GLib.Error as e:
            raise CDEmuError(_("Failed to connect to CDEmu daemon: %s") % (e))

        # Get daemon interface version
        try:
            interface_version = self.GetDaemonInterfaceVersion2()
        except GLib.Error as e:
            raise CDEmuError(
                _("Failed to acquire daemon interface version (this most likely means your daemon is out-of-date): %s") %  # noqa: E501
                (e)
            )

        # Check daemon interface version
        if (
            # The major version must be exact match...
            interface_version[0] != supported_daemon_interface_version[0] or
            # ... and the daemon's interface version needs to be greater
            # or equal to what is supported by client.
            interface_version[1] < supported_daemon_interface_version[1]
        ):
            raise CDEmuError(
                _("CDEmu daemon interface version %i.%i detected, but version %i.%i is required!") %
                (
                    interface_version[0],
                    interface_version[1],
                    supported_daemon_interface_version[0],
                    supported_daemon_interface_version[1],
                ),
            )

    def GetDaemonVersion(self):
        return self.proxy.GetDaemonVersion()

    def GetLibraryVersion(self):
        return self.proxy.GetLibraryVersion()

    def GetDaemonInterfaceVersion2(self):
        return self.proxy.GetDaemonInterfaceVersion2()

    def EnumDaemonDebugMasks(self):
        return self.proxy.EnumDaemonDebugMasks()

    def EnumLibraryDebugMasks(self):
        return self.proxy.EnumLibraryDebugMasks()

    def EnumSupportedParsers(self):
        return self.proxy.EnumSupportedParsers()

    def EnumSupportedWriters(self):
        return self.proxy.EnumSupportedWriters()

    def EnumSupportedFilterStreams(self):
        return self.proxy.EnumSupportedFilterStreams()

    def EnumWriterParameters(self, writer_id):
        return self.proxy.EnumWriterParameters('(s)', writer_id)

    def GetNumberOfDevices(self):
        return self.proxy.GetNumberOfDevices()

    def DeviceGetMapping(self, device_number):
        return self.proxy.DeviceGetMapping('(i)', device_number)

    def DeviceGetStatus(self, device_number):
        return self.proxy.DeviceGetStatus('(i)', device_number)

    def DeviceLoad(self, device_number, filenames, parameters):
        return self.proxy.DeviceLoad('(iasa{sv})', device_number, filenames, parameters)

    def DeviceCreateBlank(self, device_number, filename, parameters):
        return self.proxy.DeviceCreateBlank('(isa{sv})', device_number, filename, parameters)

    def DeviceUnload(self, device_number):
        return self.proxy.DeviceUnload('(i)', device_number)

    def DeviceGetOption(self, device_number, option_name):
        return self.proxy.DeviceGetOption('(is)', device_number, option_name)

    def DeviceSetOption(self, device_number, option_name, option_value):
        return self.proxy.DeviceSetOption('(isv)', device_number, option_name, option_value)

    def AddDevice(self):
        return self.proxy.AddDevice()

    def RemoveDevice(self):
        return self.proxy.RemoveDevice()


########################################################################
#                               Main                                   #
########################################################################
def main():
    bus_type = "session"  # Use session bus as hard-coded default.

    # *** Parse config file, if available ***
    # Load options; try "~/.cdemu-client" first, followed by "/etc/cdemu-client.conf".
    paths = (
        os.path.expanduser("~/.cdemu-client"),
        "/etc/cdemu-client.conf",
    )
    for path in paths:
        if os.path.exists(path):
            break
    else:
        path = None

    if path is not None:
        try:
            config = configparser.ConfigParser()
            config.read(path)

            # Read default bus type
            if config.has_option("defaults", "bus"):
                bus_type = config.get("defaults", "bus")
        except configparser.Error as e:
            # No harm, just print a warning
            print_warning(_("Failed to load configuration from file '%s': %s") % (path, e))

    # *** Parse command-line ***
    arguments = create_parser().parse_args()

    # Do we need to override bus type?
    if hasattr(arguments, "bus"):
        bus_type = arguments.bus

    # *** Connect to daemon ***
    proxy = CDEmuDaemonProxy(bus_type)

    # *** Execute command ***
    arguments.command_function(proxy, arguments)


if __name__ == "__main__":
    try:
        main()
        sys.exit(0)
    except CDEmuError as e:
        print_error(e)

        # Check if we need to report error through zenity-based dialog as well.
        if os.environ.get("CDEMU_USE_ZENITY", "0") != "0":
            try:
                subprocess.run(
                    ["zenity", "--error", "--title", _("CDEmu client"), "--text", str(e)],
                    check=True,
                    stdin=subprocess.DEVNULL,
                    stdout=subprocess.DEVNULL,
                    stderr=subprocess.DEVNULL,
                )
            except Exception:
                print_warning(_("Failed to report error via zenity: %s") % (e, ))

        sys.exit(-1)
