#
# Copyright (C) 2012  Red Hat, Inc.
#
# This copyrighted material is made available to anyone wishing to use,
# modify, copy, or redistribute it subject to the terms and conditions of
# the GNU General Public License v.2, or (at your option) any later version.
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY expressed or implied, including the implied warranties 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.  Any Red Hat trademarks that are incorporated in the
# source code or documentation are not subject to the GNU General Public
# License and may only be used or replicated with the express permission of
# Red Hat, Inc.
#

"""
This module provides functions for dealing with keyboard layouts/keymaps in Anaconda.
"""

import re
import langtable

from pyanaconda.core.configuration.anaconda import conf
from pyanaconda import localization
from pyanaconda.core.constants import DEFAULT_KEYBOARD
from pyanaconda.core.util import execWithRedirect
from pyanaconda.modules.common.task import sync_run_task
from pyanaconda.modules.common.constants.services import LOCALIZATION

from pyanaconda.anaconda_loggers import get_module_logger
log = get_module_logger(__name__)


# should match and parse strings like 'cz' or 'cz (qwerty)' regardless of white
# space
LAYOUT_VARIANT_RE = re.compile(r'^\s*([/\w]+)\s*'  # layout plus
                               r'(?:(?:\(\s*([-\w]+)\s*\))'  # variant in parentheses
                               r'|(?:$))\s*')  # or nothing


class KeyboardConfigError(Exception):
    """Exception class for keyboard configuration related problems"""

    pass


class InvalidLayoutVariantSpec(Exception):
    """
    Exception class for errors related to parsing layout and variant specification strings.

    """

    pass


def _is_xwayland():
    """Is Anaconda running in XWayland environment?

    This can't be easily detected from the Anaconda because Anaconda
    is running as XWayland app. Use xisxwayland tool for the detection.
    """
    try:
        rc = execWithRedirect('xisxwayland', [])

        if rc == 0:
            return True

        log.debug(
            "Anaconda doesn't run on XWayland. "
            "See xisxwayland --help for more info."
        )
    except FileNotFoundError:
        log.warning(
            "The xisxwayland tool is not available! "
            "Taking the environment as not Wayland."
        )

    return False


def can_configure_keyboard():
    """Can we configure the keyboard?

    FIXME: This is a temporary solution.

    The is_wayland logic is not part of the configuration so we would
    have to add it to the configuration otherwise it won't be accessible
    in the Anaconda modules.
    """
    if not conf.system.can_configure_keyboard:
        return False

    if conf.system.can_run_on_xwayland and _is_xwayland():
        return False

    return True


def parse_layout_variant(layout_variant_str):
    """
    Parse layout and variant from the string that may look like 'layout' or
    'layout (variant)'.

    :param layout_variant_str: keyboard layout and variant string specification
    :type layout_variant_str: str
    :return: the (layout, variant) pair, where variant can be ""
    :rtype: tuple
    :raise InvalidLayoutVariantSpec: if the given string isn't a valid layout
                                     and variant specification string

    """

    match = LAYOUT_VARIANT_RE.match(layout_variant_str)
    if not match:
        msg = "'%s' is not a valid keyboard layout and variant specification" % layout_variant_str
        raise InvalidLayoutVariantSpec(msg)

    layout, variant = match.groups()

    # groups may be (layout, None) if no variant was specified
    return (layout, variant or "")


def join_layout_variant(layout, variant=""):
    """
    Join layout and variant to form the commonly used 'layout (variant)'
    or 'layout' (if variant is missing) format.

    :type layout: string
    :type variant: string
    :return: 'layout (variant)' or 'layout' string
    :rtype: string

    """

    if variant:
        return "%s (%s)" % (layout, variant)
    else:
        return layout


def normalize_layout_variant(layout_str):
    """
    Normalize keyboard layout and variant specification given as a single
    string. E.g. for a 'layout(variant) string missing the space between the
    left parenthesis return 'layout (variant)' which is a proper layout and
    variant specification we use.

    :param layout_str: a string specifying keyboard layout and its variant
    :type layout_str: string

    """

    layout, variant = parse_layout_variant(layout_str)
    return join_layout_variant(layout, variant)


def populate_missing_items(localization_proxy=None):
    """
    Function that populates virtual console keymap and X layouts if they
    are missing. By invoking systemd-localed methods this function READS AND
    WRITES CONFIGURATION FILES (but tries to keep their content unchanged).

    :param localization_proxy: DBus proxy of the localization module or None

    """
    task_path = localization_proxy.PopulateMissingKeyboardConfigurationWithTask()
    task_proxy = LOCALIZATION.get_proxy(task_path)
    sync_run_task(task_proxy)


def activate_keyboard(localization_proxy):
    """
    Try to setup VConsole keymap and X11 layouts as specified in kickstart.

    :param localization_proxy: DBus proxy of the localization module or None

    """
    task_path = localization_proxy.ApplyKeyboardWithTask()
    task_proxy = LOCALIZATION.get_proxy(task_path)
    sync_run_task(task_proxy)


def set_x_keyboard_defaults(localization_proxy, xkl_wrapper):
    """
    Set default keyboard settings (layouts, layout switching).

    :param localization_proxy: DBus proxy of the localization module or None
    :type ksdata: object instance
    :param xkl_wrapper: XklWrapper instance
    :type xkl_wrapper: object instance
    :raise InvalidLocaleSpec: if an invalid locale is given (see
                              localization.is_valid_langcode)
    """
    x_layouts = localization_proxy.XLayouts
    # remove all X layouts that are not valid X layouts (unsupported)
    valid_layouts = []
    for layout in x_layouts:
        if xkl_wrapper.is_valid_layout(layout):
            valid_layouts.append(layout)
    localization_proxy.XLayouts = valid_layouts

    if valid_layouts:
        # do not add layouts if there are any specified in the kickstart
        # (the x_layouts list comes from kickstart)
        return

    locale = localization_proxy.Language
    layouts = localization.get_locale_keyboards(locale)
    if layouts:
        # take the first locale (with highest rank) from the list and
        # store it normalized
        new_layouts = [normalize_layout_variant(layouts[0])]
        if not langtable.supports_ascii(layouts[0]):
            # The default keymap setting should have "us" before the native layout
            # which does not support ascii,
            # refer: https://bugzilla.redhat.com/show_bug.cgi?id=1039185
            new_layouts.insert(0, DEFAULT_KEYBOARD)
    else:
        log.error("Failed to get layout for chosen locale '%s'", locale)
        new_layouts = [DEFAULT_KEYBOARD]

    localization_proxy.XLayouts = new_layouts

    if can_configure_keyboard():
        xkl_wrapper.replace_layouts(new_layouts)

    # the console layout configured should be "native" by default,
    # setting that explicitly for non-ascii layouts where we prepend "us"
    # refer: https://bugzilla.redhat.com/show_bug.cgi?id=1912609
    if len(new_layouts) >= 2 and not langtable.supports_ascii(new_layouts[1]):
        localization_proxy.VirtualConsoleKeymap = new_layouts[1]

    if len(new_layouts) >= 2 and not localization_proxy.LayoutSwitchOptions:
        # initialize layout switching if needed
        localization_proxy.LayoutSwitchOptions = ["grp:alt_shift_toggle"]

        if can_configure_keyboard():
            xkl_wrapper.set_switching_options(["grp:alt_shift_toggle"])
            # activate the language-default layout instead of the additional
            # one
            xkl_wrapper.activate_default_layout()
