# Source code for pympress.app

    # -*- coding: utf-8 -*-
    #
    #       pympress
    #
    #       Copyright 2009, 2010 Thomas Jost <thomas.jost@gmail.com>
    #       Copyright 2015 Cimbali <me@cimba.li>
    #
    #       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.
    """
    :mod:`pympress.app` -- The Gtk.Application managing the lifetime and CLI
    ------------------------------------------------------------------------
    """

    import logging
    logger = logging.getLogger(__name__)

    import os
    import sys
    import signal
    import platform

    import gi
    import cairo
    gi.require_version('Gtk', '3.0')
    from gi.repository import Gtk, Gdk, GLib, Gio

    from pympress import util, config, document, ui, builder

    class Pympress(Gtk.Application):
        """ Class representing the single pympress Gtk application.
        """
        #: The :class:`~pympress.ui.UI` object that is the interface of pympress
        gui = None
        #: The :class:`~pympress.config.Config` object that holds pympress conferences
        config = None
        #: `list` of actions to be passsed to the GUI that were queued before GUI was created
        action_startup_queue = []
        #: `bool` to automatically upgrade log level (DEBUG / INFO at init, then ERROR), False if user set log level
        auto_log_level = True

        options = {
            # long_name:  (short_name (int), flags (GLib.OptionFlags), arg (GLib.OptionArg)
            'talk-time':  (ord('t'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
            'notes':      (ord('N'), GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
            'log':        (0,        GLib.OptionFlags.NONE, GLib.OptionArg.STRING),
            'version':    (ord('v'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'pause':      (ord('P'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'reset':      (ord('r'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'next':       (ord('n'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'prev':       (ord('p'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'first':      (ord('f'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'last':       (ord('l'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'blank':      (ord('b'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
            'quit':       (ord('q'), GLib.OptionFlags.NONE, GLib.OptionArg.NONE),
        }

        option_descriptions = {
            #  long_name: (description, arg_description)
            'talk-time': (_('The estimated (intended) talk time in minutes') + ' ' +
                          _('(and optionally seconds)'), 'mm[:ss]'),
            'notes':     (_('Set the position of notes on the pdf page') + ' ' +
                          _('(none, left, right, top, bottom, after, odd, or prefix).') + ' ' +
                          _('Overrides the detection from the file.'), '<position>'),
            'log':       (_('Set level of verbosity in log file:') + ' ' +
                          _('{}, {}, {}, {}, or {}').format('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL'), '<level>'),
            'version':   (_('Print version and exit'), None),
            'pause':     (_('Toggle pause of talk timer'), None),
            'reset':     (_('Reset talk timer'), None),
            'next':      (_('Next slide'), None),
            'prev':      (_('Previous slide'), None),
            'first':     (_('First slide'), None),
            'last':      (_('Last slide'), None),
            'blank':     (_('Blank/unblank content screen'), None),
            'quit':      (_('Close opened pympress instance'), None),
        }

        version_string = ' '.join([
            'Pympress:', util.get_pympress_meta()['version'],
            '; Python:', platform.python_version(),
            '; OS:', platform.system(), platform.release(), platform.version(),
            '; Gtk {}.{}.{}'.format(Gtk.get_major_version(), Gtk.get_minor_version(), Gtk.get_micro_version()),
            '; GLib {}.{}.{}'.format(GLib.MAJOR_VERSION, GLib.MINOR_VERSION, GLib.MICRO_VERSION),
            '; Poppler', document.Poppler.get_version(), document.Poppler.get_backend().value_nick,
            '; Cairo', cairo.cairo_version_string(), ', pycairo', cairo.version,
        ])

        def __init__(self):
            GLib.set_application_name('pympress')
            # GLib.set_prgname('pympress')  # Let prgname be auto-determined from sys.argv[0]
            Gtk.Application.__init__(self, application_id='io.github.pympress',
                                     flags=Gio.ApplicationFlags.HANDLES_OPEN | Gio.ApplicationFlags.CAN_OVERRIDE_APP_ID)

            self.register(None)

            if not self.get_is_remote():
                builder.Builder.setup_actions({
                    'log-level': dict(activate=self.set_log_level, state=logger.getEffectiveLevel(), parameter_type=int),
                }, action_map=self)

            # Connect proper exit function to interrupt
            signal.signal(signal.SIGINT, self.quit)

            for opt in self.options:
                self.add_main_option(opt, *self.options[opt], *self.option_descriptions.get(opt, ['', None]))

        def quit(self, *args):  # noqa: A003 -- we need to override app.quit()
            """ Quit and ignore other arguments e.g. sent by signals.
            """
            if self.gui is not None and self.gui.unsaved_changes():
                return

            Gtk.Application.quit(self)
            return False

        def do_startup(self):
            """ Common start-up tasks for primary and remote instances.

            NB. super(self) causes segfaults, Gtk.Application needs to be used as base.
            """
            self.config = config.Config()

            # prefere X11 on posix systems because Wayland still has some shortcomings for us,
            # specifically libVLC and the ability to disable screensavers
            if util.IS_POSIX:
                Gdk.set_allowed_backends('x11,*')

            logger.info(self.version_string)

            # If there is no display, we can’t get further than this.
            if os.getenv('PYMPRESS_HEADLESS_TEST', ''):
                sys.exit(0)

            Gtk.Application.do_startup(self)

        def do_activate(self, timestamp=GLib.get_current_time()):
            """ Activate: show UI windows.

            Build them if they do not exist, otherwise bring to front.
            """
            if self.gui is None:
                if self.auto_log_level:
                    self.activate_action('log-level', logging.INFO)
                    self.action_startup_queue.append(('log-level', logging.ERROR))

                # Build the UI and windows
                self.gui = ui.UI(self, self.config)

                while self.action_startup_queue:
                    self.activate_action(*self.action_startup_queue.pop(0))

            Gtk.Application.do_activate(self)
            self.gui.p_win.present_with_time(timestamp)

        def set_action_enabled(self, name, value):
            """ Parse an action name and set its enabled state to True or False.

            Args:
                name (`str`): the name of the stateful action
                value (`bool`): wheether the action should be enabled or disabled
            """
            self.lookup_action(name).set_enabled(value)

        def set_action_state(self, name, value):
            """ Parse an action name and set its state wrapped in a :class:`~GLib.Variant`.

            Args:
                name (`str`): the name of the stateful action
                value (`str`, `int`, `bool` or `float`): the value to set.
            """
            self.lookup_action(name).change_state(GLib.Variant(builder.Builder._glib_type_strings[type(value)], value))

        def get_action_state(self, name):
            """ Parse an action name and return its unwrapped state from the :class:`~GLib.Variant`.

            Args:
                name (`str`): the name of the stateful action

            Returns:
                `str`, `int`, `bool` or `float`: the value contained in the action
            """
            state = self.lookup_action(name).get_state()
            return builder.Builder._glib_type_getters[state.get_type_string()](state)

        def activate_action(self, name, parameter=None):
            """ Parse an action name and activate it, with parameter wrapped in a :class:`~GLib.Variant` if it is not None.

            Args:
                name (`str`): the name of the stateful action
                parameter: an object or None to pass as a parameter to the action, wrapped in a GLib.Variant
            """
            if not self.get_is_remote() and self.gui is None and name not in ['log-level']:
                self.action_startup_queue.append((name, parameter))
                return

            if parameter is not None:
                parameter = GLib.Variant(builder.Builder._glib_type_strings[type(parameter)], parameter)

            Gio.ActionGroup.activate_action(self, name, parameter)

        def do_open(self, files, n_files, hint):
            """ Handle opening files. In practice we only open once, the last one.

            Args:
                files (`list` of :class:`~Gio.File`): representing an array of files to open
                n_files (`int`): the number of files passed.
                hint (`str`): a hint, such as view, edit, etc. Should always be the empty string.
            """
            if not n_files:
                return

            self.do_activate(timestamp=GLib.get_current_time())
            self.gui.swap_document(files[-1].get_uri())

        def do_shutdown(self):
            """ Perform various cleanups and save preferences.
            """
            if self.gui is not None:
                self.gui.cleanup()

            self.config.save_config()
            Gtk.Application.do_shutdown(self)
            util.close_opened_resources()

        def set_log_level(self, action, param):
            """ Action that sets the logging level (on the root logger of the active instance)

            Args:
                action (:class:`~Gio.Action`): The action activatd
                param (:class:~`GLib.Variant`): The desired level as an int wrapped in a GLib.Variant
            """
            logging.getLogger(None).setLevel(param.get_int64())
            action.change_state(param)

        def do_handle_local_options(self, opts_variant_dict):
            """ Parse command line options, returned as a VariantDict

            Returns:
                `tuple`: estimated talk time, log level, notes positions.
            """
            # convert GVariantDict -> GVariant -> dict
            opts = opts_variant_dict.end().unpack()

            simple_actions = {
                'pause': 'pause-timer',
                'reset': 'reset-timer',
                'next': 'next-page',
                'prev': 'prev-page',
                'blank': 'blank-screen',
                'quit': 'quit',
                'first': 'first-page',
                'last': 'last-page',
            }

            for opt, arg in opts.items():
                if opt == "version":
                    print(self.version_string)
                    return 0

                elif opt == "log":
                    numeric_level = getattr(logging, arg.upper(), None)
                    if isinstance(numeric_level, int):
                        self.auto_log_level = False
                        self.activate_action('log-level', numeric_level)
                    else:
                        print(_("Invalid log level \"{}\", try one of {}").format(
                            arg, "DEBUG, INFO, WARNING, ERROR, CRITICAL"
                        ))

                elif opt == "notes":
                    arg = arg.lower()[:1]
                    if arg == 'n': self.activate_action('notes-pos', 'none')
                    if arg == 'l': self.activate_action('notes-pos', 'left')
                    if arg == 'r': self.activate_action('notes-pos', 'right')
                    if arg == 't': self.activate_action('notes-pos', 'top')
                    if arg == 'b': self.activate_action('notes-pos', 'bottom')
                    if arg == 'a': self.activate_action('notes-pos', 'after')
                    if arg == 'o': self.activate_action('notes-pos', 'odd')
                    if arg == 'p': self.activate_action('notes-pos', 'map')  # prefix -> map

                elif opt == "talk-time":
                    t = ["0" + n.strip() for n in arg.split(':')]
                    try:
                        m = int(t[0])
                        s = int(t[1])
                    except ValueError:
                        print(_("Invalid time (mm or mm:ss expected), got \"{}\"").format(arg))
                        return 2
                    except IndexError:
                        s = 0
                    self.activate_action('set-talk-time', m * 60 + s)

                elif opt in simple_actions:
                    self.activate_action(simple_actions[opt])

            return -1