#!/usr/bin/python3

import fcntl
import gettext
import gi
import io
import json
import os
import qrcode
import re
import subprocess
import sys
import threading

from packaging.version import Version

gi.require_version('Gtk', '3.0')
gi.require_version('GdkPixbuf', '2.0')

from gi.repository import GdkPixbuf, GLib, Gtk

gettext.bindtextdomain('puavo-laptop-setup', '/usr/share/locale')
gettext.textdomain('puavo-laptop-setup')
_tr = gettext.gettext

# these are translations for aliases and include these here so that
# msgmerge knows we need these (needed_translations variable is not used)
needed_translations = [
  _tr('latest'), _tr('stable'), _tr('penultimate'), _tr('test')
]

def puavoconf_get(puavoconf_key):
    # We do this first to see if we get an error... if this puavo-conf
    # key does not exist, this raises an exception.
    default_value = subprocess.check_output([ 'puavo-conf', puavoconf_key ]) \
                              .rstrip().decode('utf-8')
    try:
        with open('/state/etc/puavo/local/puavo_conf.json') as file:
            conf = json.load(file)
        return conf[puavoconf_key]
    except:
        # Fallback value from puavo-conf if local value could not be read.
        return default_value


def we_have_windows():
    return os.path.exists('/images/boot/.puavo_windows_partition')


def is_encrypted_installation():
    try:
        result = subprocess.check_output(
            ['/usr/lib/puavo-ltsp-install/is-encrypted-install', '/images'],
            stderr=subprocess.PIPE,
            text=True
        )
        return result.strip() == 'true'
    except subprocess.CalledProcessError:
        return False
    except FileNotFoundError:
        return False


class PuavoLaptopSetup:
    def __init__(self, app_window, builder):
        self.app_window = app_window
        self.builder = builder
        self.puavo_conf = {
            'puavo.abitti2.version':             None,
            'puavo.grub.boot_default':           None,
            'puavo.grub.developer_mode.enabled': None,
            'puavo.grub.windows.enabled':        None,
        }
        self.abitti_info = None
        self.initializing_ui = True
        self.save_state_flag = True


    def get_list_of_abitti_choices(self):
        try:
            choices = self.abitti_info['choices']
            if not isinstance(choices, dict):
                raise Exception('choices is not a dict')
            for k, v in choices.items():
                if not 'version' in v:
                    raise Exception('no version in %s' % k)
                if not isinstance(v['version'], str):
                    raise Exception('version is not a string for %s' % k)

            versions = [
                { 'choice': k, 'version': v['version'] } \
                    for k, v in choices.items()
            ]

            return sorted(versions, key=lambda d: Version(d['version']),
                                    reverse=True)
        except Exception as e:
            print("could not determine Abitti 2 versions: %s" % e,
                    file=sys.stderr)
            return []

    def prepare(self):
        self.puavo_conf_grid = builder.get_object('puavo_conf_grid')

        try:
            self.prepare_abitti()
        except Exception as e:
            print('could not initialize abitti UI: %s' % e,
                  file=sys.stderr)

        self.boot_abitti_button   = builder.get_object('boot_abitti')
        self.boot_puavo_os_button = builder.get_object('boot_puavo_os')
        self.boot_windows_button  = builder.get_object('boot_windows')

        self.developer_mode_switch = builder.get_object('developer_mode_switch')
        self.use_windows_switch = builder.get_object('use_windows_switch')

        self.reset_to_factory_settings \
          = self.builder.get_object('reset_to_factory_settings')
        self.reset_to_factory_settings.connect('clicked',
          self.confirm_reset_to_factory_settings)

        self.disk_encryption_button = self.builder.get_object("disk_encryption_button")
        self.disk_encryption_button.connect("clicked", self.show_disk_encryption_dialog)

        self.disk_encryption_show_qr_code_button = self.builder.get_object(
            "disk_encryption_show_qr_code"
        )
        self.disk_encryption_show_qr_code_button.connect(
            "clicked", self.show_recovery_qr_code
        )

        self.disk_encryption_change_pin_button = self.builder.get_object(
            "disk_encryption_change_pin"
        )
        self.disk_encryption_change_pin_button.connect(
            "clicked", self.request_pin_change
        )

        try:
            if puavoconf_get('puavo.laptop-setup.enable_setting.pin_change') == 'true':
                self.disk_encryption_change_pin_button.show()
        except subprocess.CalledProcessError:
            pass

        self.boot_abitti_button.connect(  'toggled', self.boot_abitti_chosen)
        self.boot_puavo_os_button.connect('toggled', self.boot_puavo_os_chosen)
        self.boot_windows_button.connect( 'toggled', self.boot_windows_chosen)
        self.developer_mode_switch.connect('notify::active',
            self.developer_mode_switch_changed)
        self.use_windows_switch.connect('notify::active',
            self.use_windows_switch_changed)

        self.remove_entries_without_puavoconf()
        self.remove_disabled_entries()
        self.update_gui_state_from_puavoconf()

        GLib.timeout_add(500, self.save_state)
        self.initializing_ui = False


    def prepare_abitti(self):
        self.abitti_ui_initialized = False

        self.abitti_info = self.get_abitti_info()
        if not self.abitti_info:
            return

        self.abitti_alias_versions = self.get_abitti_alias_versions()
        self.abitti_school_versions_str \
          = self.get_abitti_school_versions_string()

        self.abitti_version_school_button \
          = builder.get_object('abitti_version_school_button')
        self.abitti_version_school \
          = builder.get_object('abitti_version_school')
        self.abitti_version_alias_button \
          = builder.get_object('abitti_version_alias')
        self.abitti_version_any_button \
          = builder.get_object('abitti_version_any')
        self.abitti_alias_version_box = builder.get_object('abitti_alias_version_box')
        self.abitti_any_version_box = builder.get_object('abitti_any_version_box')

        if self.abitti_school_versions_str:
            self.abitti_version_school.set_label(
                self.abitti_school_versions_str)

        for alias, version in sorted(self.abitti_alias_versions.items()):
            label = '%s (%s)' % (_tr(alias), version)
            self.abitti_alias_version_box.append(alias, label)

        for abitti_choice in self.get_list_of_abitti_choices():
            self.abitti_any_version_box.append(abitti_choice['choice'],
                                               abitti_choice['version'])

        self.abitti_version_alias_button.connect('toggled',
            self.abitti_version_alias_chosen)
        self.abitti_version_school_button.connect('toggled',
            self.abitti_version_school_chosen)
        self.abitti_version_any_button.connect('toggled',
            self.abitti_version_any_chosen)

        self.abitti_alias_version_box.connect('changed',
            self.abitti_alias_version_box_changed)
        self.abitti_any_version_box.connect('changed',
            self.abitti_any_version_box_changed)

        self.abitti_ui_initialized = True


    def get_abitti_info(self):
        try:
            with open('/images/abitti2/abitti2.json') as file:
                return json.load(file)
        except:
            return None


    def lookup_abitti2_alias(self, alias):
        try:
            versions_by_alias = self.get_abitti_alias_versions()
            return versions_by_alias[alias]
        except Exception as e:
            print('could not read the Abitti version for alias %s: %s' \
                    % (alias, e),
                  file=sys.stderr)
            raise e


    def get_abitti_alias_versions(self):
        version_by_alias = {}

        if not 'choices' in self.abitti_info:
            raise Exception('no choices')
        if not isinstance(self.abitti_info['choices'], dict):
            raise Exception('choices is not a dict')
        choices_map = self.abitti_info['choices']

        aliases_info = self.abitti_info['aliases']
        if type(aliases_info) != dict:
            raise Exception('aliases is not a dict')

        for alias, choice in aliases_info.items():
            if not isinstance(choice, str):
                raise Exception('choice is not a string')
            if not choice in choices_map:
                raise Exception('no such choice available: %s' % candidate)
            if not isinstance(choices_map[choice], dict):
                raise Exception('choice is not a dict')
            if not 'version' in choices_map[choice]:
                raise Exception('no version set for choice')
            if not isinstance(choices_map[choice]['version'], str):
                raise Exception('version set for choice is not a string')
            version_by_alias[alias] = choices_map[choice]['version']

        return version_by_alias


    def get_abitti_school_versions_string(self):
        # The "school version" might also come from organisation or
        # device-specific settings.
        try:
            with open('/state/etc/puavo/device.json') as file:
                device = json.load(file)
            versions = device['conf']['puavo.abitti2.version']
            if type(versions) != str:
                raise Exception('puavo.abitti2.version puavo-conf is not a string')
            alias_versions = [
              self.lookup_abitti2_alias(x) for x in versions.split()
            ]
            join_str = ' ' + _tr('and') + ' '
            return join_str.join(alias_versions)
        except Exception as e:
            print('could not read the school Abitti versions: %s' % e,
                  file=sys.stderr)
            return None


    def abitti_version_alias_chosen(self, widget):
        if not widget.get_active():
            return
        self.abitti_alias_version_box.grab_focus()
        self.set_abitti_version( self.abitti_alias_version_box.get_active_id() )


    def abitti_version_school_chosen(self, widget):
        if not widget.get_active():
            return
        widget.grab_focus()
        self.set_abitti_version(None)


    def abitti_version_any_chosen(self, widget):
        if not widget.get_active():
            return
        self.abitti_any_version_box.grab_focus()
        self.set_abitti_version( self.abitti_any_version_box.get_active_id() )


    def set_abitti_version(self, version):
        self.puavo_conf['puavo.abitti2.version'] = version
        self.save_state_flag = True


    def abitti_alias_version_box_changed(self, widget):
        if not self.initializing_ui:
            self.abitti_version_alias_button.set_active(True)
        self.set_abitti_version(
            self.abitti_alias_version_box.get_active_id())


    def abitti_any_version_box_changed(self, widget):
        if not self.initializing_ui:
            self.abitti_version_any_button.set_active(True)
        self.set_abitti_version( self.abitti_any_version_box.get_active_id() )


    def boot_abitti_chosen(self, widget):
        if not widget.get_active():
            return
        self.puavo_conf['puavo.grub.boot_default'] = 'abitti2'
        self.save_state_flag = True


    def boot_puavo_os_chosen(self, widget):
        if not widget.get_active():
            return
        self.puavo_conf['puavo.grub.boot_default'] = 'puavo-os'
        self.save_state_flag = True


    def boot_windows_chosen(self, widget):
        if not widget.get_active():
            return
        self.puavo_conf['puavo.grub.boot_default'] = 'windows'
        self.save_state_flag = True


    def developer_mode_switch_changed(self, widget, gparam):
        if widget.get_active():
            self.puavo_conf['puavo.grub.developer_mode.enabled'] = 'true'
        else:
            self.puavo_conf['puavo.grub.developer_mode.enabled'] = 'false'
        self.save_state_flag = True


    def use_windows_switch_changed(self, widget, gparam):
        if widget.get_active():
            self.puavo_conf['puavo.grub.windows.enabled'] = 'true'
            self.boot_windows_button.set_sensitive(True)

            if not self.initializing_ui:
                msg = _tr('It is now possible to choose Windows from' \
                          ' the boot menu when starting up the computer.')
                if self.boot_abitti_button:
                    msg += '  ' \
                       + _tr('Abitti 2 is not updated when running Windows,' \
                             ' you have to boot into the current system to update it.')
                dialog = Gtk.MessageDialog(self.app_window, 0,
                  Gtk.MessageType.WARNING, Gtk.ButtonsType.OK,
                  msg)
                dialog.run()
                dialog.destroy()

        else:
            self.puavo_conf['puavo.grub.windows.enabled'] = 'false'
            self.boot_windows_button.set_sensitive(False)
            if self.boot_windows_button.get_active():
                self.boot_puavo_os_button.set_active(True)
        self.save_state_flag = True


    def remove_entries_without_puavoconf(self):
        remove_gui_elements = {}

        if not self.abitti_ui_initialized:
            del self.puavo_conf['puavo.abitti2.version']

        if not we_have_windows():
            del self.puavo_conf['puavo.grub.windows.enabled']

        if not 'puavo.abitti2.version' in self.puavo_conf:
            self.boot_abitti_button.destroy()
            self.boot_abitti_button = None
            remove_gui_elements['puavo.abitti2.version'] = True

        if 'puavo.abitti2.version' in self.puavo_conf \
          and not self.abitti_school_versions_str:
            self.abitti_version_school_button.hide()

        if not 'puavo.grub.windows.enabled' in self.puavo_conf:
            self.boot_windows_button.destroy()
            self.boot_windows_button = None
            remove_gui_elements['puavo.grub.windows.enabled'] = True

        if not self.boot_abitti_button and not self.boot_windows_button:
            del self.puavo_conf['puavo.grub.boot_default']
            remove_gui_elements['puavo.grub.boot_default'] = True

        for puavo_conf_key in self.puavo_conf.copy():
            try:
                self.puavo_conf[puavo_conf_key] = puavoconf_get(puavo_conf_key)
            except subprocess.CalledProcessError:
                print('could not get puavo-conf key for %s' % puavo_conf_key,
                      file=sys.stderr)
                del self.puavo_conf[puavo_conf_key]
                remove_gui_elements[puavo_conf_key] = True

        for puavo_conf_key in remove_gui_elements:
            self.remove_grid_entry(self.puavo_conf_grid,
                                   self.builder.get_object(puavo_conf_key))


    def remove_disabled_entries(self):
        disableconfs = {
            'puavo.laptop-setup.enable_setting.abitti_version':  'puavo.abitti2.version',
            'puavo.laptop-setup.enable_setting.boot_default':    'puavo.grub.boot_default',
            'puavo.laptop-setup.enable_setting.windows_enabled': 'puavo.grub.windows.enabled',
        }

        for disabler_key in disableconfs:
            try:
                if puavoconf_get(disabler_key) != 'false':
                    continue
            except subprocess.CalledProcessError:
                print("could not get puavo-conf key for %s" % disabler_key,
                        file=sys.stderr)
                continue

            self.remove_grid_entry(self.puavo_conf_grid,
                self.builder.get_object(disableconfs[disabler_key]))
            if disabler_key != 'puavo.laptop-setup.enable_setting.abitti_version':
                continue

            self.set_abitti_version(None) # default to school version

            if not self.abitti_ui_initialized:
                continue
            school_versions_str = self.abitti_school_versions_str
            if not school_versions_str:
                continue

            version_display_obj = self.builder.get_object('puavo.abitti2.version.display')
            version_name_obj = self.builder.get_object('puavo.abitti2.version.name')
            Gtk.Widget.set_visible(version_display_obj, True)
            Gtk.Widget.set_visible(version_name_obj,    True)
            version_name_obj.set_text(school_versions_str)


    def update_gui_state_from_puavoconf(self):
        for key, value in self.puavo_conf.items():
            if key == 'puavo.abitti2.version':
                if not self.abitti_ui_initialized:
                    continue
                if value == None:
                    self.abitti_version_school_button.set_active(True)
                    self.abitti_alias_version_box.set_active(0)
                    self.abitti_any_version_box.set_active(0)
                    continue
                if value in self.abitti_alias_versions:
                    self.abitti_version_alias_button.set_active(True)
                    self.abitti_alias_version_box.set_active_id(value)
                    self.abitti_any_version_box.set_active(0)
                    continue
                self.abitti_version_any_button.set_active(True)
                self.abitti_alias_version_box.set_active(0)
                self.abitti_any_version_box.set_active_id(value)
                continue

            if key == 'puavo.grub.boot_default':
                if value == 'windows' and self.boot_windows_button:
                    self.boot_windows_button.set_active(True)
                elif value == 'abitti2' and self.boot_abitti_button:
                    self.boot_abitti_button.set_active(True)
                else:
                    self.boot_puavo_os_button.set_active(True)
                continue

            if key == 'puavo.grub.windows.enabled':
                self.use_windows_switch.set_state(value == 'true')
                continue

            if key == 'puavo.grub.developer_mode.enabled':
                self.developer_mode_switch.set_state(value == 'true')


    def remove_grid_entry(self, grid, label):
        i = 0
        while True:
            child_at = grid.get_child_at(0, i)
            if child_at == None:
                return
            if child_at == label:
                grid.remove_row(i)
                return
            i += 1


    def save_state(self):
        if not self.save_state_flag:
            return True
        try:
            self.save_state_now()
            self.save_state_flag = False
        except:
            dialog = Gtk.MessageDialog(self.app_window, 0,
              Gtk.MessageType.ERROR, Gtk.ButtonsType.OK,
              _tr('error saving changes'))
            dialog.run()
            dialog.destroy()

        return True


    def save_state_now(self):
        for key, value in self.puavo_conf.items():
            if value == None:
                cmd = [ 'sudo', '-n', 'puavo-conf-local', '-u', key ]
            else:
                cmd = [ 'sudo', '-n', 'puavo-conf-local', key, value ]
            subprocess.check_call(cmd)


    def confirm_reset_to_factory_settings(self, widget):
        target_chooser_dialog
        response = target_chooser_dialog.run()
        target_chooser_dialog.hide()
        if response == Gtk.ResponseType.OK:
          self.run_reset_to_factory_settings()


    def run_reset_to_factory_settings(self):
        factory_reset_text_view = builder.get_object('factory_reset_text_view')
        factory_reset_status_bar = builder.get_object('factory_reset_status_bar')
        factory_reset_text_buffer = Gtk.TextBuffer()
        factory_reset_text_view.set_buffer(factory_reset_text_buffer)
        context_id = factory_reset_status_bar.get_context_id('progress')

        def text_buffer_write(line):
            step_log_line_match = re.match(r'^> info: step (\d+/\d+)$', line)
            if step_log_line_match is not None:
                steps = step_log_line_match.group(1)
                text = _tr('Factory reset is running step')
                status_message = f"{text} {steps}..."
                factory_reset_status_bar.push(context_id, status_message)

            factory_reset_text_buffer.insert_at_cursor(line)

            return False

        def text_view_scroll_to_bottom():
            factory_reset_text_view.scroll_mark_onscreen(factory_reset_text_buffer.get_insert())
            return False

        def on_cmd_start():
            status_message = _tr('Factory reset is running')
            factory_reset_status_bar.push(context_id, f"{status_message}...")

            return False

        def on_cmd_start_error(exception):
            factory_reset_dialog.set_deletable(True)
            factory_reset_dialog_button.set_sensitive(True)
            status_message = _tr('Failed to start factory reset')
            factory_reset_status_bar.push(context_id, f"{status_message}.")
            text_buffer_write(str(exception))
            dialog_title = _tr('Factory reset failed')
            dialog = Gtk.MessageDialog(
                parent=factory_reset_dialog,
                title=f"{dialog_title}!",
                flags=0,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.OK,
                text=f"{status_message}.",
            )
            dialog.run()
            dialog.destroy()

            return False

        def on_cmd_exit(returnvalue):
            factory_reset_dialog.set_deletable(True)
            factory_reset_dialog_button.set_sensitive(True)
            if returnvalue == 0:
                texts = [_tr('Factory reset succeeded.')]
                if target_checkbutton_puavo_os.get_active():
                    texts.append(_tr('The system is going to reboot in few seconds...'))
                text = ' '.join(texts)
                factory_reset_status_bar.push(context_id, text)
                dialog = Gtk.MessageDialog(
                    parent=factory_reset_dialog,
                    flags=0,
                    message_type=Gtk.MessageType.INFO,
                    buttons=Gtk.ButtonsType.OK,
                    text=text,
                )
            else:
                status_message = _tr('Factory reset failed')
                factory_reset_status_bar.push(context_id, f"{status_message}.")
                help_message = _tr('Further details can be found from the factory reset log. Please contact support for assistance.')
                dialog = Gtk.MessageDialog(
                    parent=factory_reset_dialog,
                    flags=0,
                    message_type=Gtk.MessageType.ERROR,
                    buttons=Gtk.ButtonsType.OK,
                    text=f"{status_message}. {help_message}",
                )
            dialog.run()
            dialog.destroy()

            return False

        def run_cmd():
            os_targets = []
            if target_checkbutton_windows.get_active():
                os_targets.append('Windows')
            if target_checkbutton_puavo_os.get_active():
                os_targets.append('PuavoOS')

            cmd = [ 'pkexec',
                    '/usr/sbin/puavo-reset-laptop-to-factory-defaults',
                    '--force',
                    '--ignore-send-error',
                    '--os-targets',
                    ','.join(os_targets),
                   ]

            cmd_process = None
            try:
                cmd_process = subprocess.Popen(cmd, stderr=subprocess.STDOUT, stdout=subprocess.PIPE, text=True)
            except Exception as e:
                GLib.idle_add(on_cmd_start_error, e)
            else:
                GLib.idle_add(on_cmd_start)
                cmd_process.stdout.reconfigure(line_buffering=True)

                previous_line = None
                for line in cmd_process.stdout:
                    if line != previous_line:
                        GLib.idle_add(text_buffer_write, line)
                        GLib.idle_add(text_view_scroll_to_bottom)
                    previous_line = line

            finally:
                if cmd_process is not None:
                    ## Wait forever? Killing
                    ## puavo-reset-latop-to-factory-defaults might lead to
                    ## a corrupt system.
                    returncode = cmd_process.wait()
                    GLib.idle_add(on_cmd_exit, returncode)

        thread = threading.Thread(target=run_cmd, daemon=True)
        thread.start()
        factory_reset_dialog.run()
        factory_reset_dialog.hide()

    def show_disk_encryption_dialog(self, widget):
        disk_encryption_dialog = self.builder.get_object("disk_encryption_dialog")
        disk_encryption_dialog.run()
        disk_encryption_dialog.hide()

    def request_pin_change(self, widget):
        # Show a confirmation dialog explaining that PIN change happens
        # during boot, and ask whether to reboot now.
        dialog = Gtk.MessageDialog(
            parent=self.builder.get_object("disk_encryption_dialog"),
            flags=0,
            message_type=Gtk.MessageType.QUESTION,
            text=_tr("Change PIN on next boot?"),
        )
        dialog.format_secondary_text(
            _tr(
                "The disk encryption PIN can only be changed during "
                "boot. A PIN change request will be stored and the "
                "system will prompt you to set a new PIN on the next "
                "boot."
            )
        )

        dialog.add_button(_tr("Cancel"), Gtk.ResponseType.CANCEL)
        dialog.add_button(_tr("Request PIN change"), Gtk.ResponseType.ACCEPT)
        dialog.add_button(_tr("Request and reboot now"), Gtk.ResponseType.YES)

        response = dialog.run()
        dialog.destroy()

        if response in (Gtk.ResponseType.ACCEPT, Gtk.ResponseType.YES):
            try:
                subprocess.check_output(
                    ["pkexec", "/usr/lib/puavo-ltsp-install/puavo-change-unlock-pin"],
                    stderr=subprocess.PIPE,
                )
            except subprocess.CalledProcessError as exception:
                error_message = (
                    exception.stderr.decode("utf-8")
                    if exception.stderr
                    else str(exception)
                )
                error_dialog = Gtk.MessageDialog(
                    parent=self.builder.get_object("disk_encryption_dialog"),
                    flags=0,
                    message_type=Gtk.MessageType.ERROR,
                    buttons=Gtk.ButtonsType.OK,
                    text=_tr("Failed to request PIN change"),
                )
                error_dialog.format_secondary_text(error_message)
                error_dialog.run()
                error_dialog.destroy()
                return

            if response == Gtk.ResponseType.YES:
                subprocess.Popen(["systemctl", "reboot"])
            else:
                info_dialog = Gtk.MessageDialog(
                    parent=self.builder.get_object("disk_encryption_dialog"),
                    flags=0,
                    message_type=Gtk.MessageType.INFO,
                    buttons=Gtk.ButtonsType.OK,
                    text=_tr("PIN change requested"),
                )
                info_dialog.format_secondary_text(
                    _tr("You will be prompted to change the PIN on the next boot.")
                )
                info_dialog.run()
                info_dialog.destroy()

    def load_recovery_data(self):
        recovery_data = subprocess.check_output(
            ['/usr/lib/puavo-ltsp-install/puavo-read-recovery-bundle'],
            stderr=subprocess.PIPE
        )
        return recovery_data.decode('utf-8')

    def show_recovery_qr_code(self, widget):
        # Load the recovery data
        try:
            recovery_data = self.load_recovery_data()
        except subprocess.CalledProcessError as exception:
            error_message = exception.stderr.decode('utf-8') \
                if exception.stderr else str(exception)
            error_title = _tr('Failed to load recovery data')
            dialog = Gtk.MessageDialog(
                parent=self.app_window,
                flags=0,
                message_type=Gtk.MessageType.ERROR,
                buttons=Gtk.ButtonsType.OK,
                text=f"{error_title}: {error_message}",
            )
            dialog.run()
            dialog.destroy()
            return

        recovery_qr_code_dialog = self.builder.get_object(
            'recovery_qr_code_dialog')
        recovery_qr_code_image = self.builder.get_object(
            'recovery_qr_code_image')

        # Generate QR code with high error correction
        qr = qrcode.QRCode(
            version=1,
            error_correction=qrcode.constants.ERROR_CORRECT_H,
            box_size=4,
            border=4,
        )
        qr.add_data(recovery_data)
        qr.make(fit=True)

        # Create image from QR code
        qr_code_image = qr.make_image(fill_color="black", back_color="white")
        qr_code_image_bytes = io.BytesIO()
        qr_code_image.save(qr_code_image_bytes, format='PNG')
        qr_code_image_bytes.seek(0)

        loader = GdkPixbuf.PixbufLoader.new_with_type('png')
        loader.write(qr_code_image_bytes.read())
        loader.close()
        pixbuf = loader.get_pixbuf()

        recovery_qr_code_image.set_from_pixbuf(pixbuf)

        recovery_qr_code_dialog.run()
        recovery_qr_code_dialog.hide()


def on_dialog_delete_event(dialog, event):
    # We don't want to destroy the dialog because the user might
    # re-enter or retry it. The dialog is created by builder only
    # once.
    #
    # The other way would be to actually destroy them, but then
    # recreating would mean we would need to create a new builder each
    # time the dialog is shown. See
    # https://discourse.gnome.org/t/cannot-reopen-dialog-after-closed-with-x-if-using-gtkbuilder-for-construction/7019
    #
    # I think hiding is fine. Recreating builder and new dialog each
    # time seems unncessary waste of resources.
    dialog.hide()
    return True


def on_target_chooser_dialog_show(_):
    target_checkbutton_windows.set_visible(we_have_windows())
    target_checkbutton_windows.set_active(False)
    target_checkbutton_puavo_os.set_active(False)


def on_target_checkbutton_toggled(_):
    target_chooser_dialog_button_reset.set_sensitive(
        target_checkbutton_windows.get_active() or target_checkbutton_puavo_os.get_active()
    )


builder = Gtk.Builder()
builder.set_translation_domain('puavo-laptop-setup')
builder.add_from_file('/usr/share/puavo-laptop-setup/puavo-laptop-setup.glade')

app_window = builder.get_object('app_window')
app_window.set_icon_name('drive-harddisk-system')
app_window.set_title(_tr('Laptop setup'))
app_window.connect('destroy', Gtk.main_quit)

factory_reset_dialog = builder.get_object('factory_reset_dialog')
factory_reset_dialog.connect('delete-event', on_dialog_delete_event)
factory_reset_dialog.set_size_request(800, 400)

factory_reset_dialog_button = builder.get_object('factory_reset_dialog_button')
## I could figure it out how in earth this should be done with Glade
## for GtkDialog. Some googling seems to indicate that there should be
## "Response ID" etc. available and setting it should connect the
## button signal automatically to dialog.response with that response
## ID, but nope, I could not find that from Glade.
##
## Glade seems quite awkward and clumsy.
factory_reset_dialog_button.connect('clicked', lambda _: factory_reset_dialog.response(Gtk.ResponseType.CLOSE))

target_chooser_dialog = builder.get_object('target_chooser_dialog')
target_chooser_dialog.connect('delete-event', on_dialog_delete_event)
target_chooser_dialog.connect('show', on_target_chooser_dialog_show)

target_chooser_dialog_button_cancel = builder.get_object('target_chooser_dialog_button_cancel')
target_chooser_dialog_button_cancel.connect('clicked', lambda _: target_chooser_dialog.response(Gtk.ResponseType.CANCEL))

target_chooser_dialog_button_reset = builder.get_object('target_chooser_dialog_button_reset')
target_chooser_dialog_button_reset.connect('clicked', lambda _: target_chooser_dialog.response(Gtk.ResponseType.OK))

target_checkbutton_windows = builder.get_object('target_checkbutton_windows')
target_checkbutton_windows.connect('toggled', on_target_checkbutton_toggled)
target_checkbutton_puavo_os = builder.get_object('target_checkbutton_puavo_os')
target_checkbutton_puavo_os.connect('toggled', on_target_checkbutton_toggled)

recovery_qr_code_dialog = builder.get_object('recovery_qr_code_dialog')
recovery_qr_code_dialog.connect('delete-event', on_dialog_delete_event)

recovery_qr_code_dialog_button_close = builder.get_object('recovery_qr_code_dialog_button_close')
recovery_qr_code_dialog_button_close.connect('clicked', lambda _: recovery_qr_code_dialog.response(Gtk.ResponseType.CLOSE))

disk_encryption_dialog = builder.get_object("disk_encryption_dialog")
disk_encryption_dialog.connect("delete-event", on_dialog_delete_event)

disk_encryption_dialog_button_close = builder.get_object(
    "disk_encryption_dialog_button_close"
)
disk_encryption_dialog_button_close.connect(
    "clicked", lambda _: disk_encryption_dialog.response(Gtk.ResponseType.CLOSE)
)

disk_encryption_button = builder.get_object("disk_encryption_button")

if not is_encrypted_installation():
    disk_encryption_button.hide()

try:
    subprocess.check_call([ 'sudo', '-n', 'puavo-conf-local', '--check'])
except subprocess.CalledProcessError:
    errmsg = _tr('You do not have the required permissions to use this tool.')
    dialog = Gtk.MessageDialog(app_window, 0, Gtk.MessageType.ERROR,
               Gtk.ButtonsType.CANCEL, errmsg)
    dialog.run()
    sys.exit(1)

this_script = open(os.path.realpath(sys.argv[0]), 'r')
try:
    fcntl.flock(this_script, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
    errmsg = ('"%s"' % _tr('Laptop setup')) + ' ' + _tr('is already running')
    dialog = Gtk.MessageDialog(app_window, 0, Gtk.MessageType.ERROR,
               Gtk.ButtonsType.CANCEL, errmsg)
    dialog.run()
    sys.exit(1)

laptop_setup = PuavoLaptopSetup(app_window, builder)
laptop_setup.prepare()
laptop_setup.save_state_flag = True

app_window.show()

Gtk.main()
