#!/usr/bin/env python3
"""
Utility file that provides various functions.
"""

import os
import sys
import traceback
import shlex
from functools import partial
from subprocess import STDOUT, CalledProcessError, check_output
from time import sleep, time
from types import FunctionType
from typing import TypeVar

# We do this just to satisfy the typing hint but without the need to import entire class.
gi_repository_Atspi_Accessible = TypeVar(  # pylint: disable=invalid-name
    "gi_repository_Atspi_Accessible"
)
Application = TypeVar("Application")

QE_DEVELOPMENT = not os.path.isdir("/mnt/tests/")


def get_application(context, application) -> Application:
    """
    Get Application class instance of an application, based upon given name.

    :type context: <behave.runner.Context>
    :param context: Context object that is passed from common_steps.

    :type application: str
    :param application: String of application identification: name.

    :rtype: <qecore.application.Application>
    :return: Application class instance

    .. note::

        Do **NOT** call this by yourself. This function is called by :mod:`common_steps`.
    """

    app_class_to_return = None
    try:
        # Get app from environment file.
        app_class_to_return = getattr(context, application)
    except AttributeError:
        # Get app from sandbox application list.
        for app in context.sandbox.applications:
            if app.component == application:
                app_class_to_return = app
                break
    except TypeError:
        app_class_to_return = context.sandbox.default_application
        assert context.sandbox.default_application is not None, "\n".join(
            (
                "Default application was not found. Check your environment file!"
                "This is indication that no application was defined in environment.py file."
            )
        )

    assert app_class_to_return is not None, "\n".join(
        (
            f"Application '{application}' was not found.",
            "Possibly wrongly defined application in environment.py file",
            "or incorrect use of decorator in .feature file.",
        )
    )

    assert not isinstance(
        app_class_to_return, str
    ), "Application class was not found. Usually indication of not installed application."

    return app_class_to_return


def get_application_root(  # pylint: disable=unused-argument
    context, application
) -> gi_repository_Atspi_Accessible:
    """
    Get Accessibility object of an application, based upon given name.

    :type context: <behave.runner.Context>
    :param context: Context object that is passed from common steps.

    :type application: str
    :param application: String of application identification: name.

    :rtype: <dogtail.tree.root.application>
    :return: Return root object of application.

    .. note::

        Do **NOT** call this by yourself. This function is called by :mod:`common_steps`.
    """

    from dogtail.tree import (  # pylint: disable=import-error,import-outside-toplevel
        root,
    )

    try:
        root_to_return = root.application(application)
    except Exception as error:  # pylint: disable=broad-except
        traceback.print_exc(file=sys.stdout)
        raise UserWarning(
            "".join(
                (
                    "Application was not found in accessibility. ",
                    "Check your environment or feature file!",
                )
            )
        ) from error

    return root_to_return


def run(command, timeout=None) -> str:
    """
    Execute a command and get output.

    :type command: str
    :param command: Command to be executed.

    :rtype: str
    :return: Return string value of command output or exception error output.
    """

    try:
        output = check_output(command, shell=True, stderr=STDOUT, encoding="utf-8", timeout=timeout)
        return output
    except CalledProcessError as error:
        return error.output


def run_verbose(command, timeout=None) -> tuple:
    """
    Execute a command and get output in verbose format.

    :type command: str
    :param command: Command to be executed.

    :rtype: list
    :return: Return list with following format (output, return code, exception).
    """

    try:
        output = check_output(command, shell=True, stderr=STDOUT, encoding="utf-8", timeout=timeout)
        return (output, 0, None)
    except CalledProcessError as error:
        return (error.output, error.returncode, error)


def run_embed(context, command, *a, **kw):
    """
    Execute a command and get output.
    This version of command run will also embed results to the html log.

    :type context: <behave.runner.Context>
    :param context: Context object that will be used embedding.

    :type command: str
    :param command: Command to be executed.

    :rtype: list
    :return: Return list with following format (output, return code, exception).
    """

    try:
        output = check_output(
            command, shell=True, stderr=STDOUT, *a, **kw, encoding="utf-8"
        )
        return_code = 0
        error = None
    except CalledProcessError as process_error:
        output = process_error.output
        return_code = process_error.returncode
        error = process_error

    context.embed("text/plain", f"$?={return_code}", caption=f"{command} result")
    context.embed("text/plain", output, caption=f"{command} output")

    return (error.output, error.returncode, error)


def overview_action(action="hide") -> None:
    """
    Hide or show application overview.

    :type action: str
    :param action: Hide or show application overview.

    This function takes only 'hide' and 'show' value.
    """

    # DBus calls are deprecated - https://gitlab.gnome.org/GNOME/gnome-shell/-/merge_requests/1970

    assert action in ["hide", "show"], "".join(
        (
            "Unknown option, only defined ones are 'show' or 'hide'.",
            f"You tried to use: '{action}'",
        )
    )

    from dogtail.tree import (  # pylint: disable=import-error,import-outside-toplevel
        root,
    )

    gnome_shell = root.application("gnome-shell")

    activities_label = gnome_shell.findChildren(
        lambda x: x.name == "Activities" and x.roleName == "label"
    )

    if len(activities_label) == 1:
        from dogtail.predicate import (  # pylint: disable=import-error,import-outside-toplevel
            GenericPredicate,
        )

        # Find Ancestor unfortunatelly takes only Predicates not lambdas alone, \
        # will use it since implementing it here would be too long.
        activities_toggle_button = activities_label[0].findAncestor(
            GenericPredicate(roleName="toggle button")
        )

        for _ in range(5):
            if (action == "hide" and activities_toggle_button.checked) or (
                action == "show" and not activities_toggle_button.checked
            ):
                activities_toggle_button.click()
                sleep(1)

            else:
                return

    else:
        pass  # possibly in gnome-classic


# behave-common-steps leftover.
def wait_until(
    tested, element=None, timeout=30, period=0.25, params_in_list=False
) -> bool:
    """
    This function keeps running lambda with specified params until the
    result is True or timeout is reached. Instead of lambda, Boolean variable
    can be used instead.

    Sample usages::

        >>> wait_until(lambda x: x.name != 'Loading...', context.app.instance)
        Pause until window title is not 'Loading...'.
        Return False if window title is still 'Loading...'
        Throw an exception if window doesn't exist after default timeout

        >>> wait_until(lambda element, expected: x.text == expected,
            (element, 'Expected text'), params_in_list=True)
        Wait until element text becomes the expected (passed to the lambda)

        >>> wait_until(dialog.dead)
        Wait until the dialog is dead
    """

    if isinstance(tested, bool):
        curried_func = lambda: tested
    # Or if callable(tested) and element is a list or a tuple.
    elif (
        isinstance(tested, FunctionType)
        and isinstance(element, (tuple, list))
        and params_in_list
    ):
        curried_func = partial(tested, *element)
    # Or if callable(tested) and element is not None?
    elif isinstance(tested, FunctionType) and element is not None:
        curried_func = partial(tested, element)
    else:
        curried_func = tested

    exception_thrown = None
    mustend = int(time()) + timeout
    while int(time()) < mustend:
        try:
            if curried_func():
                return True
        except Exception as error:  # pylint: disable=broad-except
            # If lambda has thrown the exception we'll re-raise it later
            # and forget about if lambda passes.
            exception_thrown = error
        sleep(period)
    if exception_thrown is not None:
        raise exception_thrown  # pylint: disable=raising-bad-type
    else:
        return False


KEY_VALUE = {
    "Alt": 64,
    "Alt_L": 64,
    "Alt_R": 108,
    "Shift": 50,
    "Shift_L": 50,
    "Shift_R": 62,
    "Ctrl": 37,
    "Tab": 23,
    "Super": 133,
}


class HoldKey:
    """
    Simple utility to press a key and do some other actions.

    This is a context manager so the usage is as follows::

        >>> with HoldKey("Alt"):
        >>>     do_some_stuff()

    """

    def __init__(self, key_name) -> None:
        from dogtail.rawinput import (  # pylint: disable=import-error,import-outside-toplevel
            holdKey,
        )

        self.key_name = key_name
        holdKey(self.key_name)

    def __enter__(self):
        return self

    def __exit__(self, my_type, value, trace) -> None:
        from dogtail.rawinput import (  # pylint: disable=import-error,import-outside-toplevel
            releaseKey,
        )

        releaseKey(self.key_name)


class Tuple(tuple):
    """
    Simple tuple class with some defined arithmetic operations.

    :type command: tuple
    :param command: Tuple.
    """

    def __add__(self, other):
        return Tuple(x + y for x, y in zip(self, other))

    def __rmul__(self, other):
        return Tuple(other * x for x in self)

    def __eq__(self, other) -> bool:
        return all(x == y for x, y in zip(self, other))

    def __lt__(self, other) -> bool:
        return self[0] < other[0] or self[1] < other[1]

    def __gt__(self, other) -> bool:
        return self[0] > other[0] or self[1] > other[1]


def get_center(node) -> Tuple:
    """
    Simple utility to get the center of the node.

    :type node: <dogtail.tree.Node>
    :param node: Node passed to the function.

    :rtype: Tuple
    :return: Tuple with coordinates of the center of a Node.
    """

    return Tuple(
        (node.position[0] + node.size[0] / 2, node.position[1] + node.size[1] / 2)
    )


def validate_command(command) -> str:
    """
    Simple utility to try and escape any character that is not alpha character.

    :type command: str
    :param command: Command to be executed.

    :rtype: str
    :return: Validated command.
    """

    parsed_command = shlex.split(command)
    valid_command = ""
    for command_part in parsed_command:
        for character in command_part:
            valid_command += f"\\{character}" if not character.isalpha() else character
        valid_command += " "
    return valid_command


def verify_path(template_path):
    """
    Simple utility to verify if the file exists.

    :type template_path: str
    :param template_path: File location.

    :rtype: str or None
    :return: File path if found, None if not found.
    """

    try:
        assert os.path.isfile(template_path)
    except Exception as error:
        raise UserWarning(f"Desired file was not found: {error}") from error
    return template_path


SPACER = " "


def plain_dump(node) -> None:
    """
    Simple attempt to get more information from a11y tree.

    :type node: <dogtail.tree.root.application>
    :param node: Accessibility node.
    """

    def crawl(node, depth) -> None:
        dump(node, depth)
        for child in node.children:
            crawl(child, depth + 1)

    def dump_std_out(item, depth) -> None:
        print(
            SPACER * depth
            + str(item)
            + f"     [p:{item.position}, s:{item.size}, vis:{item.visible}, show:{item.showing}]"
        )

    dump = dump_std_out
    crawl(node, 0)
