"""Utils for adb-based UI operations."""

import collections
import logging
import os
import re
import time

from xml.dom import minidom
from acts.controllers.android_lib.errors import AndroidDeviceError


class Point(collections.namedtuple('Point', ['x', 'y'])):

  def __repr__(self):
    return '{x},{y}'.format(x=self.x, y=self.y)


class Bounds(collections.namedtuple('Bounds', ['start', 'end'])):

  def __repr__(self):
    return '[{start}][{end}]'.format(start=str(self.start), end=str(self.end))

  def calculate_middle_point(self):
    return Point((self.start.x + self.end.x) // 2,
                 (self.start.y + self.end.y) // 2)


def get_key_value_pair_strings(kv_pairs):
  return ' '.join(['%s="%s"' % (k, v) for k, v in kv_pairs.items()])


def parse_bound(bounds_string):
  """Parse UI bound string.

  Args:
    bounds_string: string, In the format of the UI element bound.
                   e.g '[0,0][1080,2160]'

  Returns:
    Bounds, The bound of UI element.
  """
  bounds_pattern = re.compile(r'\[(\d+),(\d+)\]\[(\d+),(\d+)\]')
  points = bounds_pattern.match(bounds_string).groups()
  points = list(map(int, points))
  return Bounds(Point(*points[:2]), Point(*points[-2:]))


def _find_point_in_bounds(bounds_string):
  """Finds a point that resides within the given bounds.

  Args:
    bounds_string: string, In the format of the UI element bound.

  Returns:
    A tuple of integers, representing X and Y coordinates of a point within
    the given boundary.
  """
  return parse_bound(bounds_string).calculate_middle_point()


def get_screen_dump_xml(device):
  """Gets an XML dump of the current device screen.

  This only works when there is no instrumentation process running. A running
  instrumentation process will disrupt calls for `adb shell uiautomator dump`.

  Args:
    device: AndroidDevice object.

  Returns:
    XML Document of the screen dump.
  """
  os.makedirs(device.log_path, exist_ok=True)
  device.adb.shell('uiautomator dump')
  device.adb.pull('/sdcard/window_dump.xml %s' % device.log_path)
  return minidom.parse('%s/window_dump.xml' % device.log_path)


def match_node(node, **matcher):
  """Determine if a mode matches with the given matcher.

  Args:
    node: Is a XML node to be checked against matcher.
    **matcher: Is a dict representing mobly AdbUiDevice matchers.

  Returns:
    True if all matchers match the given node.
  """
  match_list = []
  for k, v in matcher.items():
    if k == 'class_name':
      key = k.replace('class_name', 'class')
    elif k == 'text_contains':
      key = k.replace('text_contains', 'text')
    else:
      key = k.replace('_', '-')
    try:
      if k == 'text_contains':
        match_list.append(v in node.attributes[key].value)
      else:
        match_list.append(node.attributes[key].value == v)
    except KeyError:
      match_list.append(False)
  return all(match_list)


def _find_node(screen_dump_xml, **kwargs):
  """Finds an XML node from an XML DOM.

  Args:
    screen_dump_xml: XML doc, parsed from adb ui automator dump.
    **kwargs: key/value pairs to match in an XML node's attributes. Value of
      each key has to be string type. Below lists keys which can be used:
        index
        text
        text_contains (matching a part of text attribute)
        resource_id
        class_name (representing "class" attribute)
        package
        content_desc
        checkable
        checked
        clickable
        enabled
        focusable
        focused
        scrollable
        long_clickable
        password
        selected
        A special key/value: matching_node key is used to identify If more than one nodes have the same key/value,
            the matching_node stands for which matching node should be fetched.

  Returns:
    XML node of the UI element or None if not found.
  """
  nodes = screen_dump_xml.getElementsByTagName('node')
  matching_node = kwargs.pop('matching_node', 1)
  count = 1
  for node in nodes:
    if match_node(node, **kwargs):
      if count == matching_node:
        logging.debug('Found a node matching conditions: %s',
                      get_key_value_pair_strings(kwargs))
        return node
      count += 1
  return None


def wait_and_get_xml_node(device, timeout, child=None, sibling=None, **kwargs):
  """Waits for a node to appear and return it.

  Args:
    device: AndroidDevice object.
    timeout: float, The number of seconds to wait for before giving up.
    child: dict, a dict contains child XML node's attributes. It is extra set of
      conditions to match an XML node that is under the XML node which is found
      by **kwargs.
    sibling: dict, a dict contains sibling XML node's attributes. It is extra
      set of conditions to match an XML node that is under parent of the XML
      node which is found by **kwargs.
    **kwargs: Key/value pairs to match in an XML node's attributes.

  Returns:
    The XML node of the UI element.

  Raises:
    AndroidDeviceError: if the UI element does not appear on screen within
    timeout or extra sets of conditions of child and sibling are used in a call.
  """
  if child and sibling:
    raise AndroidDeviceError(
        device, 'Only use one extra set of conditions: child or sibling.')
  start_time = time.time()
  threshold = start_time + timeout
  while time.time() < threshold:
    time.sleep(1)
    screen_dump_xml = get_screen_dump_xml(device)
    node = _find_node(screen_dump_xml, **kwargs)
    if node and child:
      node = _find_node(node, **child)
    if node and sibling:
      node = _find_node(node.parentNode, **sibling)
    if node:
      return node
  msg = ('Timed out after %ds waiting for UI node matching conditions: %s.'
         % (timeout, get_key_value_pair_strings(kwargs)))
  if child:
    msg = ('%s extra conditions: %s'
           % (msg, get_key_value_pair_strings(child)))
  if sibling:
    msg = ('%s extra conditions: %s'
           % (msg, get_key_value_pair_strings(sibling)))
  raise AndroidDeviceError(device, msg)


def has_element(device, **kwargs):
  """Checks a UI element whether appears or not in the current screen.

  Args:
    device: AndroidDevice object.
    **kwargs: Key/value pairs to match in an XML node's attributes.

  Returns:
    True if the UI element appears in the current screen else False.
  """
  timeout_sec = kwargs.pop('timeout', 30)
  try:
    wait_and_get_xml_node(device, timeout_sec, **kwargs)
    return True
  except AndroidDeviceError:
    return False


def get_element_attributes(device, **kwargs):
  """Gets a UI element's all attributes.

  Args:
    device: AndroidDevice object.
    **kwargs: Key/value pairs to match in an XML node's attributes.

  Returns:
    XML Node Attributes.
  """
  timeout_sec = kwargs.pop('timeout', 30)
  node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
  return node.attributes


def wait_and_click(device, duration_ms=None, **kwargs):
  """Wait for a UI element to appear and click on it.

  This function locates a UI element on the screen by matching attributes of
  nodes in XML DOM, calculates a point's coordinates within the boundary of the
  element, and clicks on the point marked by the coordinates.

  Args:
    device: AndroidDevice object.
    duration_ms: int, The number of milliseconds to long-click.
    **kwargs: A set of `key=value` parameters that identifies a UI element.
  """
  timeout_sec = kwargs.pop('timeout', 30)
  button_node = wait_and_get_xml_node(device, timeout_sec, **kwargs)
  x, y = _find_point_in_bounds(button_node.attributes['bounds'].value)
  args = []
  if duration_ms is None:
    args = 'input tap %s %s' % (str(x), str(y))
  else:
    # Long click.
    args = 'input swipe %s %s %s %s %s' % \
        (str(x), str(y), str(x), str(y), str(duration_ms))
  device.adb.shell(args)

def wait_and_input_text(device, input_text, duration_ms=None, **kwargs):
  """Wait for a UI element text field that can accept text entry.

  This function located a UI element using wait_and_click. Once the element is
  clicked, the text is input into the text field.

  Args:
    device: AndroidDevice, Mobly's Android controller object.
    input_text: Text string to be entered in to the text field.
    duration_ms: duration in milliseconds.
    **kwargs: A set of `key=value` parameters that identifies a UI element.
  """
  wait_and_click(device, duration_ms, **kwargs)
  # Replace special characters.
  # The command "input text <string>" requires special treatment for
  # characters ' ' and '&'.  They need to be escaped. for example:
  #    "hello world!!&" needs to transform to "hello\ world!!\&"
  special_chars = ' &'
  for c in special_chars:
    input_text = input_text.replace(c, '\\%s' % c)
  input_text = "'" + input_text + "'"
  args = 'input text %s' % input_text
  device.adb.shell(args)
