#!/usr/bin/env python
#
# ^^^ This is working with python2 and python3 so we choose a shebang
#     that will find either version.
#     Citing PEP394: "One exception to this is scripts that are
#     deliberately written to be source compatible with both Python
#     2.x and 3.x. Such scripts may continue to use python on their
#     shebang line.

# Copyright (C) 2008 Google Inc.
# Copyright (C) 2019-2021 Ulrich Sibiller <uli42@gmx.de>
#
# 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.

# This version is based on the nxdialog.py of the long abandoned
# Google project "neatx" (https://code.google.com/archive/p/neatx/).
# List of changes:
# - pulled in the few parts of the neatx python modules that are actually
#   required to make it a standlone script
# - added usage output
# - dropped logging code, print errors to stderr
# - can handle the "yesno" dialog type
# - added missing docstrings
# - pylint improvements
# - removed neatx entry from the pulldown menu
# - use PyGObject instead of PyGtk and thus Gtk3
# - replace optparse by argparse
# - make code compatible to python2 and python3.

"""nxdialog program for handling dialog display."""

# If an "NX_CLIENT" environment variable is not provided to nxagent
# nxcomp library assumes this script is located in /usr/NX/bin/nxclient
#
# Examples:
# nxdialog --dialog yesno --message "text" --caption "title" --parent 0
# nxdialog --dialog pulldown --message "text" --caption "title" --window 0x123456 --parent 0

from __future__ import print_function

import argparse
import os
import signal
import sys

import gi
gi.require_version('Gtk', '3.0')
# pylint: disable=wrong-import-position
from gi.repository import Gtk, Gdk, GdkX11

PROGRAM = "nxdialog"

DISCONNECT = 1
TERMINATE = 2

EXIT_SUCCESS = 0
EXIT_FAILURE = 1

CANCEL_TEXT = "Cancel"
DISCONNECT_TEXT = "Disconnect"
TERMINATE_TEXT = "Terminate"
YES_TEXT = "Yes"
NO_TEXT = "No"

DLG_TYPE_ERROR = "error"
DLG_TYPE_OK = "ok"
DLG_TYPE_PANIC = "panic"
DLG_TYPE_PULLDOWN = "pulldown"
DLG_TYPE_QUIT = "quit"
DLG_TYPE_YESNO = "yesno"
DLG_TYPE_YESNOSUSPEND = "yesnosuspend"

VALID_DLG_TYPES = frozenset([
    DLG_TYPE_ERROR,
    DLG_TYPE_OK,
    DLG_TYPE_PANIC,
    DLG_TYPE_PULLDOWN,
    DLG_TYPE_QUIT,
    DLG_TYPE_YESNO,
    DLG_TYPE_YESNOSUSPEND,
])


class PullDownMenu(object):
    """ Shows a popup menu to disconnect/terminate session. """

    def __init__(self, window_id):
        """ Initializes this class.

        @type window_id: int
        @param window_id: X11 window id of target window

        """
        self.window_id = window_id
        self.result = None

    def show(self):
        """ Shows popup and returns result. """

        display = Gdk.Display.get_default()
        win = GdkX11.X11Window.foreign_new_for_display(display, int(self.window_id, 0))

        menu = Gtk.Menu()
        menu.connect("deactivate", self.menu_deactivate)

        # TODO: Show title item in bold font
        title = Gtk.MenuItem(label="Session control")
        title.set_sensitive(False)
        menu.append(title)

        disconnect = Gtk.MenuItem(label=DISCONNECT_TEXT)
        disconnect.connect("activate", self.item_activate, DISCONNECT)
        menu.append(disconnect)

        terminate = Gtk.MenuItem(label=TERMINATE_TEXT)
        terminate.connect("activate", self.item_activate, TERMINATE)
        menu.append(terminate)

        menu.append(Gtk.SeparatorMenuItem())

        cancel = Gtk.MenuItem(label=CANCEL_TEXT)
        menu.append(cancel)

        menu.show_all()

        menu.popup(parent_menu_shell=None, parent_menu_item=None,
                   func=self.pos_menu, data=win,
                   button=0, activate_time=Gtk.get_current_event_time())

        Gtk.main()

        return self.result

    def item_activate(self, _, result):
        """ called when a menu item is selected """
        self.result = result
        Gtk.main_quit()

    @staticmethod
    def menu_deactivate(_):
        """ called when menu is deactivated """
        Gtk.main_quit()

    @staticmethod
    # pylint: disable=unused-argument
    def pos_menu(menu, _xpos, _ypos, *data):
        """ Positions menu at the top center of the parent window. """
        parent = data[0]

        # Get parent geometry and origin
        _, _, win_width, _ = parent.get_geometry()
        _, win_x, win_y = parent.get_origin()

        # Calculate width of menu
        #menu_width = menu.get_preferred_width().natural_width
        menu_width = menu.get_allocated_width()

        # Calculate center
        center_x = int(win_x + ((win_width - menu_width) / 2))

        return (center_x, win_y, True)


def show_yes_no_suspend_box(title, text):
    """ Shows a message box to disconnect/terminate session.

    @type title: str
    @param title: Message box title
    @type text: str
    @param text: Message box text
    @return: Chosen action

    """
    dlg = Gtk.MessageDialog(type=Gtk.MessageType.QUESTION,
                            flags=Gtk.DialogFlags.MODAL)
    dlg.set_title(title)
    dlg.set_markup(text)
    dlg.add_button(DISCONNECT_TEXT, DISCONNECT)
    dlg.add_button(TERMINATE_TEXT, TERMINATE)
    dlg.add_button(CANCEL_TEXT, Gtk.ResponseType.CANCEL)

    res = dlg.run()

    if res in (DISCONNECT, TERMINATE):
        return res

    # Everything else is cancel
    return None


def show_yes_no_box(title, text):
    """ Shows a message box with answers yes and no.

    @type title: str
    @param title: Message box title
    @type text: str
    @param text: Message box text
    @return: Chosen action

    """
    dlg = Gtk.MessageDialog(type=Gtk.MessageType.QUESTION,
                            flags=Gtk.DialogFlags.MODAL)
    dlg.set_title(title)
    dlg.set_markup(text)
    dlg.add_button(YES_TEXT, TERMINATE)
    dlg.add_button(NO_TEXT, Gtk.ResponseType.CANCEL)

    res = dlg.run()

    if res == TERMINATE:
        return res

    # Everything else is cancel
    return None


def handle_session_action(agentpid, action):
    """ Execute session action chosen by user.

    @type agentpid: int
    @param agentpid: Nxagent process id as passed by command line
    @type action: int or None
    @param action: Chosen action

    """

    if action == DISCONNECT:
        print("Disconnecting from session, sending SIGHUP to %s" % (agentpid))
        if agentpid != 0:
            os.kill(agentpid, signal.SIGHUP)

    elif action == TERMINATE:
        print("Terminating session, sending SIGTERM to process %s" % (agentpid))
        if agentpid != 0:
            os.kill(agentpid, signal.SIGTERM)

    elif action is None:
        pass

    else:
        raise NotImplementedError()


def show_simple_message_box(icon, title, text):
    """ Shows a simple message box.

    @type icon: QMessageBox.Icon
    @param icon: Icon for message box
    @type title: str
    @param title: Message box title
    @type text: str
    @param text: Message box text

    """
    dlg = Gtk.MessageDialog(type=icon, flags=Gtk.DialogFlags.MODAL,
                            buttons=Gtk.ButtonsType.OK)
    dlg.set_title(title)
    dlg.set_markup(text)
    dlg.run()


class NxDialogProgram(object):
    """ the main program """

    def __init__(self):
        self.args = None
        self.options = None

    def main(self):
        """ let's do something """
        try:
            self.options = self.parse_args()

            self.run()

        except (SystemExit, KeyboardInterrupt):
            raise

        except Exception as expt:
            sys.stderr.write("Caught exception: %s" % (expt) + os.linesep)
            sys.exit(EXIT_FAILURE)

    @staticmethod
    def parse_args():
        """ init parser """

        parser = argparse.ArgumentParser(description="Helper for nxagent to display dialogs")

        # nxagent 3.5.99.27 only uses yesno, ok, pulldown and yesnosuspend
        # yesno dialogs will always kill the session if "yes" is selected
        parser.add_argument("--dialog", dest="dialog_type",
                            help='type of dialog to show, one of "yesno", \
                            "ok", "error", "panic", "quit", "pulldown", \
                            "yesnosuspend"')
        parser.add_argument("--message", dest="text",
                            help="message text to display in the dialog")
        parser.add_argument("--caption", dest="caption",
                            help="window title of the dialog")
        parser.add_argument("--display", dest="display",
                            help="X11 display where the dialog should be \
                            shown")
        parser.add_argument("--parent", type=int, dest="agentpid",
                            help="pid of the nxagent")
        parser.add_argument("--window", dest="window",
                            help="id of window where to embed the \
                            pulldown dialog type")
        # -class, -local, -allowmultiple are unused in nxlibs 3.5.99.27
        parser.add_argument("--class", dest="dlgclass", default="info",
                            help="class of the message (info, warning, error) \
                            default: info) [currently unimplemented]")
        parser.add_argument("--local", action="store_true", dest="local",
                            help="specify that proxy mode is used \
                            [currently unimplemented]")
        parser.add_argument("--allowmultiple", action="store_true",
                            dest="allowmultiple",
                            help="allow launching more than one dialog with \
                            the same message [currently unimplemented]")

        return parser.parse_args()

    def show_dialog(self, message_caption, message_text):
        """ Show the dialog or exit with failure if not implemented. """
        dlgtype = self.options.dialog_type
        if dlgtype == DLG_TYPE_OK:
            show_simple_message_box(
                Gtk.MessageType.INFO, message_caption, message_text)

        elif dlgtype in (DLG_TYPE_ERROR, DLG_TYPE_PANIC):
            show_simple_message_box(
                Gtk.MessageType.ERROR, message_caption, message_text)

        elif dlgtype == DLG_TYPE_PULLDOWN:
            handle_session_action(self.options.agentpid,
                                  PullDownMenu(self.options.window).show())

        elif dlgtype == DLG_TYPE_YESNOSUSPEND:
            handle_session_action(self.options.agentpid,
                                  show_yes_no_suspend_box(message_caption, message_text))

        elif dlgtype == DLG_TYPE_YESNO:
            handle_session_action(self.options.agentpid,
                                  show_yes_no_box(message_caption, message_text))

        else:
            # TODO: Implement all dialog types
            sys.stderr.write("Dialog type '%s' not implemented" % (dlgtype))
            sys.exit(EXIT_FAILURE)

    def run(self):
        """ Disconnect/terminate NX session upon user's request. """

        if not self.options.dialog_type:
            sys.stderr.write("Dialog type not supplied via --dialog" + os.linesep)
            sys.exit(EXIT_FAILURE)

        dlgtype = self.options.dialog_type

        if dlgtype not in VALID_DLG_TYPES:
            sys.stderr.write("Invalid dialog type '%s'" % (dlgtype) + os.linesep)
            sys.exit(EXIT_FAILURE)

        if dlgtype in (DLG_TYPE_PULLDOWN,
                       DLG_TYPE_YESNOSUSPEND,
                       DLG_TYPE_YESNO) and self.options.agentpid is None:
            sys.stderr.write("Agent pid not supplied via --parent" + os.linesep)
            sys.exit(EXIT_FAILURE)

        if dlgtype == DLG_TYPE_PULLDOWN and not self.options.window:
            sys.stderr.write("Window id not supplied via --window" + os.linesep)
            sys.exit(EXIT_FAILURE)

        if self.options.caption:
            message_caption = self.options.caption
        else:
            message_caption = sys.argv[0]

        if self.options.text:
            message_text = self.options.text
        else:
            message_text = ""

        if self.options.display:
            os.environ["DISPLAY"] = self.options.display

        self.show_dialog(message_caption, message_text)

NxDialogProgram().main()