# GUI Application automation and testing library
# Copyright (C) 2006-2018 Mark Mc Mahon and Contributors
# https://github.com/pywinauto/pywinauto/graphs/contributors
# http://pywinauto.readthedocs.io/en/latest/credits.html
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# * Redistributions of source code must retain the above copyright notice, this
#   list of conditions and the following disclaimer.
#
# * Redistributions in binary form must reproduce the above copyright notice,
#   this list of conditions and the following disclaimer in the documentation
#   and/or other materials provided with the distribution.
#
# * Neither the name of pywinauto nor the names of its
#   contributors may be used to endorse or promote products derived from
#   this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

"""Base class for all wrappers in all backends"""
from __future__ import unicode_literals
from __future__ import print_function

import abc
import ctypes
import locale
import re
import time
import win32process
import win32gui
import win32con
import six

try:
    from PIL import ImageGrab
except ImportError:
    ImageGrab = None

from . import keyboard
from . import win32defines, win32structures, win32functions
from .timings import Timings
from .actionlogger import ActionLogger
from .mouse import _perform_click_input


#=========================================================================
def remove_non_alphanumeric_symbols(s):
    """Make text usable for attribute name"""
    return re.sub("\W", "_", s)

#=========================================================================
class InvalidElement(RuntimeError):

    """Raises when an invalid element is passed"""
    pass

#=========================================================================
class ElementNotEnabled(RuntimeError):

    """Raised when an element is not enabled"""
    pass

#=========================================================================
class ElementNotVisible(RuntimeError):

    """Raised when an element is not visible"""
    pass

#=========================================================================
@six.add_metaclass(abc.ABCMeta)
class BaseMeta(abc.ABCMeta):

    """Abstract metaclass for Wrapper objects"""

    @staticmethod
    def find_wrapper(element):
        """Abstract static method to find an appropriate wrapper"""
        raise NotImplementedError()

#=========================================================================
@six.add_metaclass(BaseMeta)
class BaseWrapper(object):
    """
    Abstract wrapper for elements.

    All other wrappers are derived from this.
    """

    # Properties required for _MetaWrapper class
    friendlyclassname = None
    windowclasses = []

    # Properties that describe type of the element
    can_be_label = False
    has_title = True

    #------------------------------------------------------------
    def __new__(cls, element_info, active_backend):
        return BaseWrapper._create_wrapper(cls, element_info, BaseWrapper)

    #------------------------------------------------------------
    @staticmethod
    def _create_wrapper(cls_spec, element_info, myself):
        """Create a wrapper object according to the specified element info"""
        # only use the meta class to find the wrapper for BaseWrapper
        # so allow users to force the wrapper if they want
        if cls_spec != myself:
            obj = object.__new__(cls_spec)
            obj.__init__(element_info)
            return obj

        new_class = cls_spec.find_wrapper(element_info)
        obj = object.__new__(new_class)

        obj.__init__(element_info)

        return obj

    #------------------------------------------------------------
    def __init__(self, element_info, active_backend):
        """
        Initialize the element

        * **element_info** is instance of int or one of ElementInfo childs
        """
        self.backend = active_backend
        if element_info:
            #if isinstance(element_info, six.integer_types):
            #    element_info = self.backend.element_info_class(element_info)

            self._element_info = element_info

            self.handle = self._element_info.handle
            self._as_parameter_ = self.handle

            self.ref = None
            self.appdata = None
            self._cache = {}
            self.actions = ActionLogger()
        else:
            raise RuntimeError('NULL pointer was used to initialize BaseWrapper')

    def __repr__(self):
        """Representation of the wrapper object

        The method prints the following info:
        * type name as a module name and a class name of the object
        * title of the control or empty string
        * friendly class name of the control
        * unique ID of the control calculated as a hash value from a backend specific ID.

        Notice that the reported title and class name can be used as hints to prepare
        a windows specification to access the control, while the unique ID is more for
        debugging purposes helping to distinguish between the runtime objects.
        """
        return '<{0}, {1}>'.format(self.__str__(), self.__hash__())

    def __str__(self):
        """Pretty print representation of the wrapper object

        The method prints the following info:
        * type name as a module name and class name of the object
        * title of the wrapped control or empty string
        * friendly class name of the wrapped control

        Notice that the reported title and class name can be used as hints
        to prepare a windows specification to access the control
        """
        module = self.__class__.__module__
        module = module[module.rfind('.') + 1:]
        type_name = module + "." + self.__class__.__name__

        try:
            title = self.texts()[0]
        except IndexError:
            title = ""

        class_name = self.friendly_class_name()

        return "{0} - '{1}', {2}".format(type_name, title, class_name)

    def __hash__(self):
        """Returns the hash value of the handle"""
        # Must be implemented in a sub-class
        raise NotImplementedError()

    #------------------------------------------------------------
    @property
    def writable_props(self):
        """
        Build the list of the default properties to be written.

        Derived classes may override or extend this list depending
        on how much control they need.
        """
        props = ['class_name',
                 'friendly_class_name',
                 'texts',
                 'control_id',
                 'rectangle',
                 'is_visible',
                 'is_enabled',
                 'control_count',
                 ]
        return props

    #------------------------------------------------------------
    @property
    def _needs_image_prop(self):
        """Specify whether we need to grab an image of ourselves when asked
        for properties"""
        return False

    #------------------------------------------------------------
    @property
    def element_info(self):
        """Read-only property to get **ElementInfo** object"""
        return self._element_info

    #------------------------------------------------------------
    def friendly_class_name(self):
        """
        Return the friendly class name for the control

        This differs from the class of the control in some cases.
        class_name() is the actual 'Registered' element class of the control
        while friendly_class_name() is hopefully something that will make
        more sense to the user.

        For example Checkboxes are implemented as Buttons - so the class
        of a CheckBox is "Button" - but the friendly class is "CheckBox"
        """
        if self.friendlyclassname is None:
            self.friendlyclassname = self.class_name()
        return self.friendlyclassname

    #------------------------------------------------------------
    def class_name(self):
        """Return the class name of the elenemt"""
        return self.element_info.class_name

    #------------------------------------------------------------
    def window_text(self):
        """
        Window text of the element

        Quite a few contorls have other text that is visible, for example
        Edit controls usually have an empty string for window_text but still
        have text displayed in the edit window.
        """
        return self.element_info.rich_text

    #------------------------------------------------------------
    def control_id(self):
        """
        Return the ID of the element

        Only controls have a valid ID - dialogs usually have no ID assigned.

        The ID usually identified the control in the window - but there can
        be duplicate ID's for example lables in a dialog may have duplicate
        ID's.
        """
        return self.element_info.control_id

    #------------------------------------------------------------
    def is_visible(self):
        """
        Whether the element is visible or not

        Checks that both the top level parent (probably dialog) that
        owns this element and the element itself are both visible.

        If you want to wait for an element to become visible (or wait
        for it to become hidden) use ``Application.wait('visible')`` or
        ``Application.wait_not('visible')``.

        If you want to raise an exception immediately if an element is
        not visible then you can use the BaseWrapper.verify_visible().
        BaseWrapper.verify_actionable() raises if the element is not both
        visible and enabled.
        """
        return self.element_info.visible #and self.top_level_parent().element_info.visible

    #------------------------------------------------------------
    def is_enabled(self):
        """
        Whether the element is enabled or not

        Checks that both the top level parent (probably dialog) that
        owns this element and the element itself are both enabled.

        If you want to wait for an element to become enabled (or wait
        for it to become disabled) use ``Application.wait('visible')`` or
        ``Application.wait_not('visible')``.

        If you want to raise an exception immediately if an element is
        not enabled then you can use the BaseWrapper.verify_enabled().
        BaseWrapper.VerifyReady() raises if the window is not both
        visible and enabled.
        """
        return self.element_info.enabled #and self.top_level_parent().element_info.enabled

    # -----------------------------------------------------------
    def was_maximized(self):
        """Indicate whether the window was maximized before minimizing or not"""
        if self.handle:
            (flags, _, _, _, _) = win32gui.GetWindowPlacement(self.handle)
            return (flags & win32con.WPF_RESTORETOMAXIMIZED == win32con.WPF_RESTORETOMAXIMIZED)
        else:
            return None

    #------------------------------------------------------------
    def rectangle(self):
        """
        Return the rectangle of element

        The rectangle() is the rectangle of the element on the screen.
        Coordinates are given from the top left of the screen.

        This method returns a RECT structure, Which has attributes - top,
        left, right, bottom. and has methods width() and height().
        See win32structures.RECT for more information.
        """
        return self.element_info.rectangle

    #------------------------------------------------------------
    def client_to_screen(self, client_point):
        """Maps point from client to screen coordinates"""
        # Use a direct call to element_info.rectangle instead of self.rectangle
        # because the latter can be overriden in one of derived wrappers
        # (see _treeview_element.rectangle or _listview_item.rectangle)
        rect = self.element_info.rectangle
        if isinstance(client_point, win32structures.POINT):
            return (client_point.x + rect.left, client_point.y + rect.top)
        else:
            return (client_point[0] + rect.left, client_point[1] + rect.top)

    #-----------------------------------------------------------
    def process_id(self):
        "Return the ID of process that owns this window"
        return self.element_info.process_id

    #-----------------------------------------------------------
    def is_dialog(self):
        "Return true if the control is a top level window"
        if self.parent():
            return self == self.top_level_parent()
        else:
            return False

    #-----------------------------------------------------------
    def parent(self):
        """
        Return the parent of this element

        Note that the parent of a control is not necesarily a dialog or
        other main window. A group box may be the parent of some radio
        buttons for example.

        To get the main (or top level) window then use
        BaseWrapper.top_level_parent().
        """
        parent_elem = self.element_info.parent

        if parent_elem:
            return self.backend.generic_wrapper_class(parent_elem)
        else:
            return None

    #-----------------------------------------------------------
    def root(self):
        "Return wrapper for root element (desktop)"
        return self.backend.generic_wrapper_class(self.backend.element_info_class())

    #-----------------------------------------------------------
    def top_level_parent(self):
        """
        Return the top level window of this control

        The TopLevel parent is different from the parent in that the parent
        is the element that owns this element - but it may not be a dialog/main
        window. For example most Comboboxes have an Edit. The ComboBox is the
        parent of the Edit control.

        This will always return a valid window element (if the control has
        no top level parent then the control itself is returned - as it is
        a top level window already!)
        """
        if not ("top_level_parent" in self._cache.keys()):
            parent = self.parent()

            if parent:
                if self.parent() == self.root():
                    self._cache["top_level_parent"] = self
                else:
                    return self.parent().top_level_parent()
            else:
                self._cache["top_level_parent"] = self

        return self._cache["top_level_parent"]

    #-----------------------------------------------------------
    def texts(self):
        """
        Return the text for each item of this control

        It is a list of strings for the control. It is frequently overridden
        to extract all strings from a control with multiple items.

        It is always a list with one or more strings:

          * The first element is the window text of the control
          * Subsequent elements contain the text of any items of the
            control (e.g. items in a listbox/combobox, tabs in a tabcontrol)
        """
        texts_list = [self.window_text(), ]
        return texts_list

    #-----------------------------------------------------------
    def children(self, **kwargs):
        """
        Return the children of this element as a list

        It returns a list of BaseWrapper (or subclass) instances.
        An empty list is returned if there are no children.
        """
        child_elements = self.element_info.children(**kwargs)
        return [self.backend.generic_wrapper_class(element_info) for element_info in child_elements]

    #-----------------------------------------------------------
    def iter_children(self, **kwargs):
        """
        Iterate over the children of this element

        It returns a generator of BaseWrapper (or subclass) instances.
        """
        child_elements = self.element_info.iter_children(**kwargs)
        for element_info in child_elements:
            yield self.backend.generic_wrapper_class(element_info)

    #-----------------------------------------------------------
    def descendants(self, **kwargs):
        """
        Return the descendants of this element as a list

        It returns a list of BaseWrapper (or subclass) instances.
        An empty list is returned if there are no descendants.
        """
        desc_elements = self.element_info.descendants(**kwargs)
        return [self.backend.generic_wrapper_class(element_info) for element_info in desc_elements]

    #-----------------------------------------------------------
    def iter_descendants(self, **kwargs):
        """
        Iterate over the descendants of this element

        It returns a generator of BaseWrapper (or subclass) instances.
        """
        desc_elements = self.element_info.iter_descendants(**kwargs)
        for element_info in desc_elements:
            yield self.backend.generic_wrapper_class(element_info)

    #-----------------------------------------------------------
    def control_count(self):
        "Return the number of children of this control"
        return len(self.element_info.children(process=self.process_id()))

    #-----------------------------------------------------------
    def capture_as_image(self, rect=None):
        """
        Return a PIL image of the control.

        See PIL documentation to know what you can do with the resulting
        image.
        """

        control_rectangle = self.rectangle()
        if not (control_rectangle.width() and control_rectangle.height()):
            return None

        # PIL is optional so check first
        if not ImageGrab:
            print("PIL does not seem to be installed. "
                  "PIL is required for capture_as_image")
            self.actions.log("PIL does not seem to be installed. "
                             "PIL is required for capture_as_image")
            return None

        # get the control rectangle in a way that PIL likes it
        if rect:
            box = (rect.left, rect.top, rect.right, rect.bottom)
        else:
            box = (control_rectangle.left,
                   control_rectangle.top,
                   control_rectangle.right,
                   control_rectangle.bottom)

        # grab the image and get raw data as a string
        return ImageGrab.grab(box)

    #-----------------------------------------------------------
    def get_properties(self):
        """Return the properties of the control as a dictionary."""
        props = {}

        # for each of the properties that can be written out
        for propname in self.writable_props:
            # set the item in the props dictionary keyed on the propname
            props[propname] = getattr(self, propname)()

        if self._needs_image_prop:
            props["image"] = self.capture_as_image()

        return props

    #-----------------------------------------------------------
    def draw_outline(
        self,
        colour='green',
        thickness=2,
        fill=win32defines.BS_NULL,
        rect=None):
        """
        Draw an outline around the window.

        * **colour** can be either an integer or one of 'red', 'green', 'blue'
          (default 'green')
        * **thickness** thickness of rectangle (default 2)
        * **fill** how to fill in the rectangle (default BS_NULL)
        * **rect** the coordinates of the rectangle to draw (defaults to
          the rectangle of the control)
        """
        # don't draw if dialog is not visible
        if not self.is_visible():
            return

        colours = {
            "green": 0x00ff00,
            "blue": 0xff0000,
            "red": 0x0000ff,
        }

        # if it's a known colour
        if colour in colours:
            colour = colours[colour]

        if rect is None:
            rect = self.rectangle()

        # create the pen(outline)
        pen_handle = win32functions.CreatePen(
                win32defines.PS_SOLID, thickness, colour)

        # create the brush (inside)
        brush = win32structures.LOGBRUSH()
        brush.lbStyle = fill
        brush.lbHatch = win32defines.HS_DIAGCROSS
        brush_handle = win32functions.CreateBrushIndirect(ctypes.byref(brush))

        # get the Device Context
        dc = win32functions.CreateDC("DISPLAY", None, None, None )

        # push our objects into it
        win32functions.SelectObject(dc, brush_handle)
        win32functions.SelectObject(dc, pen_handle)

        # draw the rectangle to the DC
        win32functions.Rectangle(
            dc, rect.left, rect.top, rect.right, rect.bottom)

        # Delete the brush and pen we created
        win32functions.DeleteObject(brush_handle)
        win32functions.DeleteObject(pen_handle)

        # delete the Display context that we created
        win32functions.DeleteDC(dc)

    #-----------------------------------------------------------
    def is_child(self, parent):
        """
        Return True if this element is a child of 'parent'.

        An element is a child of another element when it is a direct of the
        other element. An element is a direct descendant of a given
        element if the parent element is the the chain of parent elements
        for the child element.
        """
        return self in parent.children(class_name = self.class_name())

    #-----------------------------------------------------------
    def __eq__(self, other):
        "Returns true if 2 BaseWrapper's describe 1 actual element"
        if hasattr(other, "element_info"):
            return self.element_info == other.element_info
        else:
            return self.element_info == other

    #-----------------------------------------------------------
    def __ne__(self, other):
        "Returns False if the elements described by 2 BaseWrapper's are different"
        return not self == other

    #-----------------------------------------------------------
    def verify_actionable(self):
        """
        Verify that the element is both visible and enabled

        Raise either ElementNotEnalbed or ElementNotVisible if not
        enabled or visible respectively.
        """
        self.wait_for_idle()
        self.verify_visible()
        self.verify_enabled()

    #-----------------------------------------------------------
    def verify_enabled(self):
        """
        Verify that the element is enabled

        Check first if the element's parent is enabled (skip if no parent),
        then check if element itself is enabled.
        """
        if not self.is_enabled():
            raise ElementNotEnabled()

    #-----------------------------------------------------------
    def verify_visible(self):
        """
        Verify that the element is visible

        Check first if the element's parent is visible. (skip if no parent),
        then check if element itself is visible.
        """
        if not self.is_visible():
            raise ElementNotVisible()

    #-----------------------------------------------------------
    def click_input(
        self,
        button = "left",
        coords = (None, None),
        button_down = True,
        button_up = True,
        double = False,
        wheel_dist = 0,
        use_log = True,
        pressed = "",
        absolute = False,
        key_down = True,
        key_up = True):
        """Click at the specified coordinates

        * **button** The mouse button to click. One of 'left', 'right',
          'middle' or 'x' (Default: 'left', 'move' is a special case)
        * **coords** The coordinates to click at.(Default: the center of the control)
        * **double** Whether to perform a double click or not (Default: False)
        * **wheel_dist** The distance to move the mouse wheel (default: 0)

        NOTES:
           This is different from click method in that it requires the control
           to be visible on the screen but performs a more realistic 'click'
           simulation.

           This method is also vulnerable if the mouse is moved by the user
           as that could easily move the mouse off the control before the
           click_input has finished.
        """
        if self.is_dialog():
            self.set_focus()
        if self.backend.name == "win32":
            self._ensure_enough_privileges('win32api.SetCursorPos(x, y)')
        # TODO: check it in more general way for both backends

        if isinstance(coords, win32structures.RECT):
            coords = coords.mid_point()
        # allow points objects to be passed as the coords
        elif isinstance(coords, win32structures.POINT):
            coords = [coords.x, coords.y]
        else:
            coords = list(coords)

        # set the default coordinates
        if coords[0] is None:
            coords[0] = int(self.rectangle().width() / 2)
        if coords[1] is None:
            coords[1] = int(self.rectangle().height() / 2)

        if not absolute:
            coords = self.client_to_screen(coords)

        message = None
        if use_log:
            ctrl_text = self.window_text()
            if ctrl_text is None:
                ctrl_text = six.text_type(ctrl_text)
            if button.lower() == 'move':
                message = 'Moved mouse over {} "{}" to screen point ('.format(
                    self.friendly_class_name(), ctrl_text)
            else:
                message = 'Clicked {} "{}" by {} button mouse click at '.format(
                    self.friendly_class_name(), ctrl_text, button)
                if double:
                    message = 'Double-c' + message[1:]
            message += str(tuple(coords))

        _perform_click_input(button, coords, double, button_down, button_up,
                             wheel_dist=wheel_dist, pressed=pressed,
                             key_down=key_down, key_up=key_up)

        if message:
            self.actions.log(message)

    #-----------------------------------------------------------
    def double_click_input(self, button ="left", coords = (None, None)):
        """Double click at the specified coordinates"""
        self.click_input(button, coords, double=True)

    #-----------------------------------------------------------
    def right_click_input(self, coords = (None, None)):
        """Right click at the specified coords"""
        self.click_input(button='right', coords=coords)

    #-----------------------------------------------------------
    def press_mouse_input(
            self,
            button = "left",
            coords = (None, None),
            pressed = "",
            absolute = True,
            key_down = True,
            key_up = True
    ):
        """Press a mouse button using SendInput"""
        self.click_input(
            button=button,
            coords=coords,
            button_down=True,
            button_up=False,
            pressed=pressed,
            absolute=absolute,
            key_down=key_down,
            key_up=key_up
        )

    #-----------------------------------------------------------
    def release_mouse_input(
            self,
            button = "left",
            coords = (None, None),
            pressed = "",
            absolute = True,
            key_down = True,
            key_up = True
    ):
        """Release the mouse button"""
        self.click_input(
            button,
            coords,
            button_down=False,
            button_up=True,
            pressed=pressed,
            absolute=absolute,
            key_down=key_down,
            key_up=key_up
        )

    #-----------------------------------------------------------
    def move_mouse_input(self, coords=(0, 0), pressed="", absolute=True):
        """Move the mouse"""
        if not absolute:
            self.actions.log('Moving mouse to relative (client) coordinates ' + str(coords).replace('\n', ', '))

        self.click_input(button='move', coords=coords, absolute=absolute, pressed=pressed)

        self.wait_for_idle()
        return self

    # -----------------------------------------------------------
    def _calc_click_coords(self):
        """A helper that tries to get click coordinates of the control

        The calculated coordinates are absolute and returned as
        a tuple with x and y values.
        """
        coords = self.rectangle().mid_point()
        return (coords.x, coords.y)

    # -----------------------------------------------------------
    def drag_mouse_input(self,
                         dst=(0, 0),
                         src=None,
                         button="left",
                         pressed="",
                         absolute=True):
        """Click on **src**, drag it and drop on **dst**

        * **dst** is a destination wrapper object or just coordinates.
        * **src** is a source wrapper object or coordinates.
          If **src** is None the self is used as a source object.
        * **button** is a mouse button to hold during the drag.
          It can be "left", "right", "middle" or "x"
        * **pressed** is a key on the keyboard to press during the drag.
        * **absolute** specifies whether to use absolute coordinates
          for the mouse pointer locations
        """
        if not src:
            src = self

        if dst is src:
            raise AttributeError("Can't drag-n-drop on itself")

        if isinstance(src, BaseWrapper):
            press_coords = src._calc_click_coords()
        elif isinstance(src, win32structures.POINT):
            press_coords = (src.x, src.y)
        else:
            press_coords = src

        if isinstance(dst, BaseWrapper):
            release_coords = dst._calc_click_coords()
        elif isinstance(dst, win32structures.POINT):
            release_coords = (dst.x, dst.y)
        else:
            release_coords = dst
        self.actions.log('Drag mouse from coordinates {0} to {1}'.format(press_coords, release_coords))

        self.press_mouse_input(button, press_coords, pressed, absolute=absolute)
        time.sleep(Timings.before_drag_wait)
        for i in range(5):
            self.move_mouse_input((press_coords[0] + i, press_coords[1]), pressed=pressed, absolute=absolute) # "left"
            time.sleep(Timings.drag_n_drop_move_mouse_wait)
        self.move_mouse_input(release_coords, pressed=pressed, absolute=absolute) # "left"
        time.sleep(Timings.before_drop_wait)
        self.release_mouse_input(button, release_coords, pressed, absolute=absolute)
        time.sleep(Timings.after_drag_n_drop_wait)
        return self

    #-----------------------------------------------------------
    def wheel_mouse_input(self, coords = (None, None), wheel_dist = 1, pressed =""):
        """Do mouse wheel"""
        self.click_input(button='wheel', coords=coords, wheel_dist=wheel_dist, pressed=pressed)
        return self

    #-----------------------------------------------------------
    def wait_for_idle(self):
        """Backend specific function to wait for idle state of a thread or a window"""
        pass # do nothing by deafault
        # TODO: implement wait_for_idle for backend="uia"

    #-----------------------------------------------------------
    def type_keys(
        self,
        keys,
        pause = None,
        with_spaces = False,
        with_tabs = False,
        with_newlines = False,
        turn_off_numlock = True,
        set_foreground = True):
        """
        Type keys to the element using keyboard.SendKeys

        This uses the re-written keyboard_ python module where you can
        find documentation on what to use for the **keys**.

        .. _keyboard: pywinauto.keyboard.html
        """
        self.verify_actionable()
        friendly_class_name = self.friendly_class_name()

        if pause is None:
            pause = Timings.after_sendkeys_key_wait

        if set_foreground:
            self.set_focus()

        # attach the Python process with the process that self is in
        if self.element_info.handle:
            window_thread_id, _ = win32process.GetWindowThreadProcessId(int(self.handle))
            win32functions.AttachThreadInput(win32functions.GetCurrentThreadId(), window_thread_id, win32defines.TRUE)
            # TODO: check return value of AttachThreadInput properly
        else:
            # TODO: UIA stuff maybe
            pass

        if isinstance(keys, six.text_type):
            aligned_keys = keys
        elif isinstance(keys, six.binary_type):
            aligned_keys = keys.decode(locale.getpreferredencoding())
        else:
            # convert a non-string input
            aligned_keys = six.text_type(keys)

        # Play the keys to the active window
        keyboard.SendKeys(
            aligned_keys,
            pause,
            with_spaces,
            with_tabs,
            with_newlines,
            turn_off_numlock)

        # detach the python process from the window's process
        if self.element_info.handle:
            win32functions.AttachThreadInput(win32functions.GetCurrentThreadId(), window_thread_id, win32defines.FALSE)
            # TODO: check return value of AttachThreadInput properly
        else:
            # TODO: UIA stuff
            pass

        self.wait_for_idle()

        self.actions.log('Typed text to the ' + friendly_class_name + ': ' + aligned_keys)
        return self

    #-----------------------------------------------------------
    def set_focus(self):
        """Set the focus to this element"""
        pass

#====================================================================