# 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.

"""Functions to retrieve properties from a window handle

These are implemented in a procedural way so as to to be
useful to other modules with the least conceptual overhead
"""

import ctypes
import warnings
import win32process
import win32api
import win32con
import win32gui

from . import win32functions
from . import win32defines
from . import win32structures
from .actionlogger import ActionLogger


#=========================================================================
def text(handle):
    """Return the text of the window"""
    class_name = classname(handle)
    if class_name == 'IME':
        return 'Default IME'
    if class_name == 'MSCTFIME UI':
        return 'M'
    #length = win32functions.SendMessage(handle, win32defines.WM_GETTEXTLENGTH, 0, 0)

    # XXX: there are some very rare cases when WM_GETTEXTLENGTH hangs!
    # WM_GETTEXTLENGTH may hang even for notepad.exe main window!
    c_length = win32structures.DWORD(0)
    result = win32functions.SendMessageTimeout(
        handle,
        win32defines.WM_GETTEXTLENGTH,
        0,
        0,
        win32defines.SMTO_ABORTIFHUNG,
        500,
        ctypes.byref(c_length)
    )
    if result == 0:
        ActionLogger().log('WARNING! Cannot retrieve text length for handle = ' + str(handle))
        return None
    else:
        length = c_length.value

    textval = ''
    # In some rare cases, the length returned by WM_GETTEXTLENGTH is <0.
    # Guard against this by checking it is >0 (==0 is not of interest):
    if length > 0:
        length += 1

        buffer_ = ctypes.create_unicode_buffer(length)

        ret = win32functions.SendMessage(
            handle, win32defines.WM_GETTEXT, length, ctypes.byref(buffer_))

        if ret:
            textval = buffer_.value

    return textval


#=========================================================================
def classname(handle):
    """Return the class name of the window"""
    class_name = (ctypes.c_wchar * 257)()
    win32functions.GetClassName(handle, ctypes.byref(class_name), 256)
    return class_name.value


#=========================================================================
def parent(handle):
    """Return the handle of the parent of the window"""
    return win32functions.GetParent(handle)


#=========================================================================
def style(handle):
    """Return the style of the window"""
    return win32functions.GetWindowLong(handle, win32defines.GWL_STYLE)


#=========================================================================
def exstyle(handle):
    """Return the extended style of the window"""
    return win32functions.GetWindowLong(handle, win32defines.GWL_EXSTYLE)


#=========================================================================
def controlid(handle):
    """Return the ID of the control"""
    return win32functions.GetWindowLong(handle, win32defines.GWL_ID)


#=========================================================================
def userdata(handle):
    """Return the value of any user data associated with the window"""
    return win32functions.GetWindowLong(handle, win32defines.GWL_USERDATA)


#=========================================================================
def contexthelpid(handle):
    """Return the context help id of the window"""
    return win32functions.GetWindowContextHelpId(handle)


#=========================================================================
def iswindow(handle):
    """Return True if the handle is a window"""
    return bool(win32functions.IsWindow(handle))


#=========================================================================
def isvisible(handle):
    """Return True if the window is visible"""
    return bool(win32functions.IsWindowVisible(handle))


#=========================================================================
def isunicode(handle):
    """Return True if the window is a Unicode window"""
    return bool(win32functions.IsWindowUnicode(handle))


#=========================================================================
def isenabled(handle):
    """Return True if the window is enabled"""
    return bool(win32functions.IsWindowEnabled(handle))


#=========================================================================
def is64bitprocess(process_id):
    """Return True if the specified process is a 64-bit process on x64

    Return False if it is only a 32-bit process running under Wow64.
    Always return False for x86.
    """
    from .sysinfo import is_x64_OS
    is32 = True
    if is_x64_OS():
        phndl = win32api.OpenProcess(win32con.MAXIMUM_ALLOWED, 0, process_id)
        if phndl:
            is32 = win32process.IsWow64Process(phndl)
            #print("is64bitprocess, is32: %d, procid: %d" % (is32, process_id))

    return (not is32)


#=========================================================================
def is64bitbinary(filename):
    """Check if the file is 64-bit binary"""
    import win32file
    try:
        binary_type = win32file.GetBinaryType(filename)
        return binary_type != win32file.SCS_32BIT_BINARY
    except Exception as exc:
        warnings.warn('Cannot get binary type for file "{}". Error: {}' \
            ''.format(filename, exc), RuntimeWarning, stacklevel=2)
        return None


#=========================================================================
def clientrect(handle):
    """Return the client rectangle of the control"""
    client_rect = win32structures.RECT()
    win32functions.GetClientRect(handle, ctypes.byref(client_rect))
    return client_rect


#=========================================================================
def rectangle(handle):
    """Return the rectangle of the window"""
    # GetWindowRect returns 4-tuple
    return win32structures.RECT(*win32gui.GetWindowRect(handle))


#=========================================================================
def font(handle):
    """Return the font as a LOGFONTW of the window"""
    # get the font handle
    font_handle = win32functions.SendMessage(
        handle, win32defines.WM_GETFONT, 0, 0)

    # if the fondUsed is 0 then the control is using the
    # system font (well probably not - even though that is what the docs say)
    # instead we switch to the default GUI font - which is more likely correct.
    if not font_handle:

        # So just get the default system font
        font_handle = win32functions.GetStockObject(win32defines.DEFAULT_GUI_FONT)

        # if we still don't have a font!
        # ----- ie, we're on an antiquated OS, like NT 3.51
        if not font_handle:

            # ----- On Asian platforms, ANSI font won't show.
            if win32functions.GetSystemMetrics(win32defines.SM_DBCSENABLED):
                # ----- was...(SYSTEM_FONT)
                font_handle = win32functions.GetStockObject(
                    win32defines.SYSTEM_FONT)
            else:
                # ----- was...(SYSTEM_FONT)
                font_handle = win32functions.GetStockObject(
                    win32defines.ANSI_VAR_FONT)

    else:
        fontval = win32structures.LOGFONTW()
        ret = win32functions.GetObject(
            font_handle, ctypes.sizeof(fontval), ctypes.byref(fontval))

    # Get the Logfont structure of the font of the control
    fontval = win32structures.LOGFONTW()
    ret = win32functions.GetObject(
        font_handle, ctypes.sizeof(fontval), ctypes.byref(fontval))

    # The function could not get the font - this is probably
    # because the control does not have associated Font/Text
    # So we should make sure the elements of the font are zeroed.
    if not ret:
        fontval = win32structures.LOGFONTW()

    # if it is a main window
    if is_toplevel_window(handle):

        if "MS Shell Dlg" in fontval.lfFaceName or \
           fontval.lfFaceName == "System":
            # these are not usually the fonts actaully used in for
            # title bars so we need to get the default title bar font

            # get the title font based on the system metrics rather
            # than the font of the control itself
            ncms = win32structures.NONCLIENTMETRICSW()
            ncms.cbSize = ctypes.sizeof(ncms)
            win32functions.SystemParametersInfo(
                win32defines.SPI_GETNONCLIENTMETRICS,
                ctypes.sizeof(ncms),
                ctypes.byref(ncms),
                0)

            # with either of the following 2 flags set the font of the
            # dialog isthe small one (but there is normally no difference!
            if has_style(handle, win32defines.WS_EX_TOOLWINDOW) or \
               has_style(handle, win32defines.WS_EX_PALETTEWINDOW):

                fontval = ncms.lfSmCaptionFont
            else:
                fontval = ncms.lfCaptionFont

    return fontval


#=========================================================================
def processid(handle):
    """Return the ID of process that controls this window"""
    _, process_id = win32process.GetWindowThreadProcessId(int(handle))
    return process_id


#=========================================================================
def has_enough_privileges(process_id):
    """Check if target process has enough rights to query GUI actions"""
    try:
        access_level = win32con.PROCESS_QUERY_INFORMATION | win32con.PROCESS_VM_READ
        process_handle = win32api.OpenProcess(access_level, 0, process_id)
        if process_handle:
            win32api.CloseHandle(process_handle)
            return True
        return False
    except win32gui.error:
        return False


#=========================================================================
def children(handle):
    """Return a list of handles to the children of this window"""
    # this will be filled in the callback function
    child_windows = []

    # callback function for EnumChildWindows
    def enum_child_proc(hwnd, lparam):
        """Called for each child - adds child hwnd to list"""
        # append it to our list
        child_windows.append(hwnd)

        # return true to keep going
        return True

    # define the child proc type
    enum_child_proc_t = ctypes.WINFUNCTYPE(
        ctypes.c_int,            # return type
        win32structures.HWND,    # the window handle
        win32structures.LPARAM)  # extra information

    # update the proc to the correct type
    proc = enum_child_proc_t(enum_child_proc)

    # loop over all the children (callback called for each)
    win32functions.EnumChildWindows(handle, proc, 0)

    return child_windows


#=========================================================================
def has_style(handle, tocheck):
    """Return True if the control has style tocheck"""
    hwnd_style = style(handle)
    return tocheck & hwnd_style == tocheck


#=========================================================================
def has_exstyle(handle, tocheck):
    """Return True if the control has extended style tocheck"""
    hwnd_exstyle = exstyle(handle)
    return tocheck & hwnd_exstyle == tocheck


#=========================================================================
def is_toplevel_window(handle):
    """Return whether the window is a top level window or not"""
    # only request the style once - this is an optimization over calling
    # (handle, style) for each style I wan to check!
    style_ = style(handle)

    if (style_ & win32defines.WS_OVERLAPPED == win32defines.WS_OVERLAPPED or
        style_ & win32defines.WS_CAPTION == win32defines.WS_CAPTION) and \
       not (style_ & win32defines.WS_CHILD == win32defines.WS_CHILD):
        return True
    else:
        return False


#=========================================================================
def dumpwindow(handle):
    """Dump a window to a set of properties"""
    props = {}

    for func in (text,
                 classname,
                 rectangle,
                 clientrect,
                 style,
                 exstyle,
                 contexthelpid,
                 controlid,
                 userdata,
                 font,
                 parent,
                 processid,
                 isenabled,
                 isunicode,
                 isvisible,
                 children,
                 ):

        props[func.__name__] = func(handle)

    return props
