# vim:et sts=4 sw=4
#
# ibus-typing-booster - A completion input method for IBus
#
# Copyright (c) 2020 Mike FABIAN <mfabian@redhat.com>
#
# 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 3 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, see <http://www.gnu.org/licenses/>

'''
A test program to test input purpose and hints
'''

from typing import Dict
from typing import Any
from typing import Optional
from typing import TYPE_CHECKING
from types import FrameType
import sys
import signal
import logging
import logging.handlers

from gi import require_version
require_version('GLib', '2.0')
# pylint: disable=wrong-import-position
from gi.repository import GLib # type: ignore

# set_prgname before importing other modules to show the name in warning
# messages when import modules are failed. E.g. Gtk.
GLib.set_application_name('InputPurposeTest')
# This makes gnome-shell load the .desktop file when running under Wayland:
GLib.set_prgname('InputPurposeTest')

# pylint: disable=import-error
sys.path = [sys.path[0]+'/../engine'] + sys.path
import itb_util
from itb_gtk import Gtk, GTK_MAJOR # type: ignore
if TYPE_CHECKING:
    # These imports are only for type checkers (mypy). They must not be
    # executed at runtime because itb_gtk controls the Gtk/Gdk versions.
    # pylint: disable=reimported
    from gi.repository import Gtk  # type: ignore
    # pylint: enable=reimported
from g_compat_helpers import (
    add_child,
    show_all,
)
# pylint: enable=import-error
# pylint: enable=wrong-import-position

LOGGER = logging.getLogger('ibus-typing-booster')

GLIB_MAIN_LOOP: Optional[GLib.MainLoop] = None

class InputPurposeTest(Gtk.Window): # type: ignore
    '''
    User interface of the setup tool
    '''
    def __init__(self) -> None:
        Gtk.Window.__init__(self, title='Input Purpose Test')
        self.set_name('InputPurposeTest')
        self.set_modal(False)
        self.set_title('Input Purpose Test')
        if GTK_MAJOR >= 4:
            self.connect('close_request', self.on_close)
        else:
            self.connect('delete-event', self.on_close)

        main_container = Gtk.Box()
        main_container.set_orientation(Gtk.Orientation.VERTICAL)
        main_container.set_spacing(0)
        add_child(self, main_container)

        margin = 5

        self._input_purpose = itb_util.InputPurpose.FREE_FORM
        input_purpose_combobox = Gtk.ComboBox()
        input_purpose_combobox.set_margin_start(margin)
        input_purpose_combobox.set_margin_end(margin)
        input_purpose_combobox.set_margin_top(margin)
        input_purpose_combobox.set_margin_bottom(margin)
        self._input_purpose_store = Gtk.ListStore(str, int)
        for purpose in list(itb_util.InputPurpose):
            self._input_purpose_store.append([purpose.name, purpose])
        input_purpose_combobox.set_model(self._input_purpose_store)
        renderer_text = Gtk.CellRendererText()
        input_purpose_combobox.pack_start(renderer_text, True)
        input_purpose_combobox.add_attribute(renderer_text, "text", 0)
        for i, item in enumerate(self._input_purpose_store):
            if self._input_purpose == item[1]:
                input_purpose_combobox.set_active(i)
        input_purpose_combobox.connect(
            'changed', self.on_input_purpose_combobox_changed)

        add_child(main_container, input_purpose_combobox)

        self._input_hints = itb_util.InputHints.NONE

        input_hints_checkbuttons: Dict[str, Gtk.CheckButton] = {}
        for hint in itb_util.InputHints:
            if hint.name is None or hint.name == 'NONE':
                continue
            input_hints_checkbuttons[hint.name] = Gtk.CheckButton(
                label=hint.name)
            input_hints_checkbuttons[hint.name].set_margin_start(margin)
            input_hints_checkbuttons[hint.name].set_margin_end(margin)
            input_hints_checkbuttons[hint.name].set_margin_top(margin)
            input_hints_checkbuttons[hint.name].set_margin_bottom(margin)
            input_hints_checkbuttons[hint.name].set_active(False)
            input_hints_checkbuttons[hint.name].set_hexpand(False)
            input_hints_checkbuttons[hint.name].set_vexpand(False)
            input_hints_checkbuttons[hint.name].connect(
                'toggled', self.on_checkbutton, hint)
            add_child(main_container, input_hints_checkbuttons[hint.name])

        self._test_entry = Gtk.Entry()
        self._test_entry.set_margin_start(margin)
        self._test_entry.set_margin_end(margin)
        self._test_entry.set_margin_top(margin)
        self._test_entry.set_margin_bottom(margin)
        self._test_entry.set_visible(True)
        self._test_entry.set_can_focus(True)
        self._test_entry.set_hexpand(False)
        self._test_entry.set_vexpand(False)
        self._test_entry.set_input_purpose(self._input_purpose)
        self._test_entry.set_input_hints(self._input_hints)
        self._test_entry.connect('notify::text', self.on_test_entry)

        add_child(main_container, self._test_entry)

        self._test_text_view = Gtk.TextView()
        self._test_text_view.set_wrap_mode(Gtk.WrapMode.WORD_CHAR)
        margin = 10
        self._test_text_view.set_margin_start(margin)
        self._test_text_view.set_margin_end(margin)
        self._test_text_view.set_margin_top(margin)
        self._test_text_view.set_margin_bottom(margin)
        self._test_text_view_buffer = Gtk.TextBuffer()
        self._test_text_view.set_buffer(self._test_text_view_buffer)
        self._test_text_view.set_visible(True)
        self._test_text_view.set_can_focus(True)
        self._test_text_view.set_hexpand(False)
        self._test_text_view.set_vexpand(True)
        self._test_text_view.set_input_purpose(self._input_purpose)
        self._test_text_view.set_input_hints(self._input_hints)
        self._test_text_view_buffer.connect(
            'changed', self.on_test_text_view_buffer_changed)

        add_child(main_container, self._test_text_view)

        show_all(self)

    def on_close(self, *_args: Any) -> None: # pylint: disable=no-self-use
        '''Main window has been closed, quit the glib main loop'''
        LOGGER.info('Window deleted by the window manager.')
        if GLIB_MAIN_LOOP is not None:
            GLIB_MAIN_LOOP.quit()
        else:
            raise RuntimeError("GLIB_MAIN_LOOP not initialized!")

    def on_test_entry( # pylint: disable=no-self-use
            self, widget: Gtk.Entry, _property_spec: Any) -> None:
        '''
        Called when something in the test entry has changed
        '''
        LOGGER.info('Test entry contains: “%s”', widget.get_text())

    def on_test_text_view_buffer_changed( # pylint: disable=no-self-use
            self, widget: Gtk.TextBuffer) -> None:
        '''
        Called when something in the test entry has changed
        '''
        LOGGER.info('Test text view contains: “%s”',
                    widget.get_text(
                        widget.get_start_iter(),
                        widget.get_end_iter(),
                        True))

    def on_input_purpose_combobox_changed(
            self, widget: Gtk.ComboBox) -> None:
        '''
        The combobox to choose the input purpose has been changed.
        '''
        tree_iter = widget.get_active_iter()
        if tree_iter is not None:
            model = widget.get_model()
            self._input_purpose = model[tree_iter][1]
            if self._input_purpose not in list(itb_util.InputPurpose):
                LOGGER.info(
                    'self._input_purpose = %s (Unknown)',
                    self._input_purpose)
                return
            for input_purpose in list(itb_util.InputPurpose):
                if self._input_purpose == input_purpose:
                    LOGGER.info(
                        'self._input_purpose = %s (%s)',
                        self._input_purpose, str(input_purpose))
                    self._test_entry.set_input_purpose(self._input_purpose)
                    self._test_text_view.set_input_purpose(self._input_purpose)
                    input_purpose_entry = (
                        self._test_entry.get_input_purpose())
                    input_purpose_text_view = (
                        self._test_text_view.get_input_purpose())
                    LOGGER.info(
                        'Input purpose changed to %s (%s)',
                        input_purpose_entry, str(input_purpose_entry))
                    if input_purpose_entry != input_purpose_text_view:
                        LOGGER.error(
                            'input_purpose_entry != '
                            'input_purpose_text_view: %s %s',
                            input_purpose_entry, input_purpose_text_view)
                    if input_purpose_entry != self._input_purpose:
                        LOGGER.error(
                            'input_purpose_entry != '
                            'self._input_purpose: %s %s',
                            input_purpose_entry, self._input_purpose)

    def on_checkbutton(
            self, widget: Gtk.CheckButton, hint: int) -> None:
        '''
        One of the check buttons to activate or deactivate an input hint
        has been clicked.
        '''
        LOGGER.info('Clicked checkbutton %s %s', widget, hint)
        if widget.get_active():
            self._input_hints |= hint
        else:
            self._input_hints &= ~hint
        self._test_entry.set_input_hints(Gtk.InputHints(self._input_hints))
        self._test_text_view.set_input_hints(Gtk.InputHints(self._input_hints))
        input_hints_entry = self._test_entry.get_input_hints()
        input_hints_text_view = self._test_text_view.get_input_hints()
        LOGGER.info('New value of self._input_hints=%s',
                    format(int(input_hints_entry), '016b'))
        if int(input_hints_entry) != int(self._input_hints):
            LOGGER.error(
                'input_hints_entry != self._input_hints: %s %s',
                input_hints_entry, self._input_hints)
        if int(input_hints_entry) != int(input_hints_text_view):
            LOGGER.error(
                'input_hints_entry != input_hints_text_view: %s %s',
                input_hints_entry, input_hints_text_view)
        for input_hint in list(itb_util.InputHints):
            if self._input_hints & input_hint:
                LOGGER.info(
                    'hint: %s %s',
                    str(hint), format(int(hint), '016b'))

def quit_glib_main_loop(
        signum: int, _frame: Optional[FrameType] = None) -> None:
    '''Signal handler for signals from Python’s signal module

    :param signum: The signal number
    :param _frame:  Almost never used (it’s for debugging).
    '''
    if signum is not None:
        try:
            signal_name = signal.Signals(signum).name
        except ValueError: # In case signum isn't in Signals enum
            signal_name = str(signum)
        LOGGER.info('Received signal %s (%s), exiting...', signum, signal_name)
    if GLIB_MAIN_LOOP is not None:
        GLIB_MAIN_LOOP.quit()
    else:
        raise RuntimeError("GLIB_MAIN_LOOP not initialized!")

if __name__ == '__main__':
    LOG_HANDLER_STREAM = logging.StreamHandler(stream=sys.stdout)
    LOG_FORMATTER = logging.Formatter(
        '%(asctime)s %(filename)s '
        'line %(lineno)d %(funcName)s %(levelname)s: '
        '%(message)s')
    LOG_HANDLER_STREAM.setFormatter(LOG_FORMATTER)
    LOGGER.setLevel(logging.DEBUG)
    LOGGER.addHandler(LOG_HANDLER_STREAM)
    LOGGER.info('********** STARTING **********')

    itb_util.set_program_name('inputpurposetes') # only 15 characters

    INPUT_PURPOSE_TEST = InputPurposeTest()
    GLIB_MAIN_LOOP = GLib.MainLoop()
    signal.signal(signal.SIGTERM, quit_glib_main_loop) # kill <pid>
    # Ctrl+C (optional, can also use try/except KeyboardInterrupt)
    # signal.signal(signal.SIGINT, quit_glib_main_loop)
    try:
        GLIB_MAIN_LOOP.run()
    except KeyboardInterrupt:
        # SIGNINT (Control+C) received
        LOGGER.info('Control+C pressed, exiting ...')
        GLIB_MAIN_LOOP.quit()
