# coding=utf-8
from __future__ import unicode_literals

import copy
import six
import time
from functools import wraps

from poco.exceptions import PocoTargetTimeout, InvalidOperationException, PocoNoSuchNodeException, PocoTargetRemovedException
from poco.sdk.exceptions import UnableToSetAttributeException
from poco.utils.query_util import query_expr, build_query

__author__ = 'lxn3032'
__all__ = ['UIObjectProxy']


def wait(func):
    @wraps(func)
    def wrapped(proxy, *args, **kwargs):
        try:
            return func(proxy, *args, **kwargs)
        except PocoNoSuchNodeException as e:
            try:
                proxy.wait_for_appearance(timeout=proxy.poco._pre_action_wait_for_appearance)
                return func(proxy, *args, **kwargs)
            except PocoTargetTimeout:
                raise e

    return wrapped


def refresh_when(err_type):
    def wrapper(func):
        @wraps(func)
        def wrapped(self, *args, **kwargs):
            try:
                return func(self, *args, **kwargs)
            except err_type:
                self._do_query(multiple=False, refresh=True)
                return func(self, *args, **kwargs)
        return wrapped
    return wrapper


class UIObjectProxy(object):
    """
    UI Proxy class that represents the UI element on target device.

    Any action performing on this instance is handled by Poco. It is not necessary to initialize this object manually.
    See ``QueryCondition`` for more details about how to select the UI elements.

    Args:
        poco: the poco instance
        name: query condition of "name" attribute, i.e. the UI element(s) with ``name`` name will be selected
        attrs: other query expressions except for the ``name``

    See Also:
        :py:meth:`select UI element(s) by poco <poco.pocofw.Poco.__call__>`
    """

    def __init__(self, poco, name=None, **attrs):
        # query object in tuple
        self.query = build_query(name, **attrs)
        self.poco = poco

        # this flag is introduced to improve the performance, it is set if multiple UI elements are selected and
        # it does not affect the selection result
        # 上一次选择是否是多选，如果不是多选但需要访问所有UI elements时会进行重新选择。
        self._query_multiple = False

        # true or false whether the corresponding UI elements of this UI proxy (self) have been selected
        # 此UI proxy是否已经查找到对应的UI elements了
        self._evaluated = False

        # the proxy object of UI elements, migh be `node` or `[nodes]`, the proxy type is specified by
        # `self._nodes_proxy_is_list`
        # 可能是远程node代理，也可能是远程[node]代理, 由`self._nodes_proxy_is_list`指定是何种proxy类型
        self._nodes = None
        self._nodes_proxy_is_list = True

        # use only for caching some proxies of sorted nodes in `self.__getitem__`
        # 仅用于__getitem__时保存好已排序的child代理对象
        self._sorted_children = None

        # focus point of the UI element, see `CoordinateSystem` for more details
        # 相对于包围盒的focus point定义，用于touch/swipe/drag操作的局部相对定位
        self._focus = None

    def child(self, name=None, **attrs):
        """
        Select the direct child(ren) from the UI element(s) given by the query expression, see ``QueryCondition`` for
        more details about the selectors.

        Args:
            name: query expression of attribute "name", i.e. the UI elements with ``name`` name will be selected
            attrs: other query expression except for the ``name``

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object representing the child(ren) of
            current UI element(s)
        """

        sub_query = build_query(name, **attrs)
        query = ('/', (self.query, sub_query))
        obj = UIObjectProxy(self.poco)
        obj.query = query
        return obj

    def children(self):
        """
        The same as :py:meth:`.child() <poco.proxy.UIObjectProxy.child>` but it selects all children from the UI
        element(s).

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object
        """

        return self.child()

    def offspring(self, name=None, **attrs):
        """
        Select the offsprings including the direct child(ren) from the UI element(s) given by the query expression,
        see ``QueryCondition`` for more details about selectors.

        Args:
            name: query expression of attribute "name", i.e. the UI elements with ``name`` name will be selected
            attrs: other query expression except for the ``name``

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object representing the child(ren) of
            current UI element(s)
        """

        sub_query = build_query(name, **attrs)
        query = ('>', (self.query, sub_query))
        obj = UIObjectProxy(self.poco)
        obj.query = query
        return obj

    def sibling(self, name=None, **attrs):
        """
        Select the sibling(s) from the UI element(s) given by the query expression, see ``QueryCondition`` for more
        details about the selectors.

        Args:
            name: query expression of attribute "name", i.e. the UI elements with ``name`` name will be selected
            attrs: other query expression except for the ``name``

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object representing the child(ren) of
            current UI element(s)
        """

        sub_query = build_query(name, **attrs)
        query = ('-', (self.query, sub_query))
        obj = UIObjectProxy(self.poco)
        obj.query = query
        return obj

    def __getitem__(self, item):
        """
        Select the specific UI element by index. If this UI proxy represents a set of UI elements, then use this method
        to access the specific UI element. The new UI element will be wrapped by UIObjectProxy instance and therefore
        the returned value is also the UI proxy object.

        The order of UI elements are determined by their position on the screen and not by the selection sequence. This
        rule is called  "L2R U2D" (one by one from left to right, line by line from up to down), i.e. the most top left
        UI element is always the first one. See ``IterationOverUI`` for more details.

        Warnings:
            This method may cause some performance issues depending on implementation of PocoAgent.

        Args:
            item (:obj:`int`): the index.

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object representing the n-th of the
            current UI elements.
        """

        if not self._query_multiple:
            nodes = self._do_query(multiple=True, refresh=True)
        else:
            nodes = self._nodes
        length = len(nodes)
        if not self._sorted_children:
            self._sorted_children = []
            for i in range(length):
                uiobj = UIObjectProxy(self.poco)
                uiobj.query = ('index', (self.query, i))
                uiobj._evaluated = True
                uiobj._query_multiple = True
                uiobj._nodes = nodes[i]
                uiobj._nodes_proxy_is_list = False
                pos = uiobj.get_position()
                self._sorted_children.append((uiobj, pos))
        self._sorted_children.sort(lambda a, b: cmp(list(reversed(a)), list(reversed(b))), key=lambda v: v[1])
        return self._sorted_children[item][0]

    def __len__(self):
        """
        Return the number of selected UI elements.

        Returns:
            :obj:`int`: returns 0 if none of the UI element matches the query expression otherwise returns the number
            of selected UI elements
        """

        if not self._nodes_proxy_is_list:
            return 1

        # 获取长度时总是multiple的
        if not self._query_multiple:
            try:
                nodes = self._do_query(multiple=True, refresh=True)
            except PocoNoSuchNodeException:
                nodes = []
        else:
            nodes = self._nodes
        return len(nodes)

    def __iter__(self):
        """
        Similar method to :py:meth:`.__getitem__() <poco.proxy.UIObjectProxy.__getitem__>` with the difference that this
        method iterates over all UI elements. The order rules of UI elements is same as for :py:meth:`.__getitem__()
        <poco.proxy.UIObjectProxy.__getitem__>`. See ``IterationOverUI`` for more details.

        Yields:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a generator yielding new UI proxy represents the
            specific UI element iterated over

        Raises:
            PocoTargetRemovedException: when hierarchy structure has changed and it is attempted to access to the
             nonexistent UI element over the iteration
        """

        # 节点数量太多时，就不按照控件顺序排序了
        if not self._query_multiple:
            nodes = self._do_query(multiple=True, refresh=True)
        else:
            nodes = self._nodes
        length = len(nodes)
        sorted_nodes = []
        for i in range(length):
            uiobj = UIObjectProxy(self.poco)
            uiobj.query = ('index', (self.query, i))
            uiobj._evaluated = True
            uiobj._query_multiple = True
            uiobj._nodes = nodes[i]
            uiobj._nodes_proxy_is_list = False
            pos = uiobj.get_position()
            sorted_nodes.append((uiobj, pos))
        sorted_nodes.sort(lambda a, b: cmp(list(reversed(a)), list(reversed(b))), key=lambda v: v[1])

        for obj, _ in sorted_nodes:
            yield obj

    @wait
    def click(self, focus=None, sleep_interval=None):
        """
        Perform the click action on the UI element(s) represented by the UI proxy. If this UI proxy represents a set of
        UI elements, the first one in the set is clicked and the anchor point of the UI element is used as the default
        one. It is also possible to click another point offset by providing ``focus`` argument.

        See ``CoordinateSystem`` for more details.

        Args:
            focus (2-:obj:`tuple`/2-:obj:`list`/:obj:`str`): an offset point (x, y) from the top left corner of the UI
             element(s), values must be in range of 0~1. This argument can be also specified by 'anchor' or 'center'.
             'Center' means to click the center of bounding box of UI element. 
            sleep_interval: number of seconds to wait after this action. Default is None which is the default sleep
             interval. This value can be configured by Poco initialization. See configuration at poco
             :py:class:`initialization <poco.pocofw.Poco>` for more details.

        Raises:
            PocoNoSuchNodeException: raised when the UI element does not exist
        """

        focus = focus or self._focus or 'anchor'
        pos_in_percentage = self.get_position(focus)
        self.poco.pre_action('click', self, pos_in_percentage)
        ret = self.poco.click(pos_in_percentage)
        if sleep_interval:
            time.sleep(sleep_interval)
        else:
            self.poco.wait_stable()
        self.poco.post_action('click', self, pos_in_percentage)
        return ret

    @wait
    def long_click(self, duration=2.0):
        """
        Perform the long click action on the UI element(s) represented by the UI proxy. If this UI proxy represents a 
        set of UI elements, the first one in the set is clicked and the anchor point of the UI element is used as the 
        default one. Similar to click but press the screen for the given time interval and then release.
    
        Args:
            duration (:py:obj:`float`): whole action duration.
        
        Return:
            the same as :py:meth:`poco.pocofw.Poco.long_click`, depending on poco agent implementation.
        """

        try:
            duration = float(duration)
        except ValueError:
            raise ValueError('Argument `duration` should be <float>. Got {}'.format(repr(duration)))

        pos_in_percentage = self.get_position(self._focus or 'anchor')
        self.poco.pre_action('long_click', self, pos_in_percentage)
        ret = self.poco.long_click(pos_in_percentage, duration)
        self.poco.post_action('long_click', self, pos_in_percentage)
        return ret

    @wait
    def swipe(self, dir, focus=None, duration=0.5):
        """
        Perform a swipe action given by the direction from this UI element. For notices and limitations see
        :py:meth:`.click() <poco.proxy.UIObjectProxy.click>`. 

        Args:
            dir (2-:obj:`tuple`/2-:obj:`list`/:obj:`str`):  coordinates (x, y) in NormalizedCoordinate system, it can
             be also specified as  'up', 'down', 'left', 'right'. Swipe 'up' is equivalent to [0, -0.1], swipe 'down' is
             equivalent to [0, 0.1], swipe 'left' is equivalent to [-0.1, 0] and swipe 'right' is equivalent to [0, 0.1]
            focus (2-:obj:`tuple`/2-:obj:`list`/:obj:`str`): see :py:meth:`.click() <poco.proxy.UIObjectProxy.click>`
             for more details
            duration (:py:obj:`float`): time interval in which the action is performed

        Raises:
            PocoNoSuchNodeException: raised when the UI element does not exist
        """

        try:
            duration = float(duration)
        except ValueError:
            raise ValueError('Argument `duration` should be <float>. Got {}'.format(repr(duration)))

        focus = focus or self._focus or 'anchor'
        dir_vec = self._direction_vector_of(dir)
        origin = self.get_position(focus)
        self.poco.pre_action('swipe', self, (origin, dir_vec))
        ret = self.poco.swipe(origin, direction=dir_vec, duration=duration)
        self.poco.post_action('swipe', self, (origin, dir_vec))
        return ret

    def drag_to(self, target, duration=2.0):
        """
        Similar to swipe action, but the end point is provide by a UI proxy or by fixed coordinates.

        Args:
            target (:py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`): a UI proxy or 2-list/2-tuple coordinates
             (x, y) in NormalizedCoordinate system
            duration (:py:obj:`float`): time interval in which the action is performed

        Raises:
            PocoNoSuchNodeException: raised when the UI element does not exist
        """

        try:
            duration = float(duration)
        except ValueError:
            raise ValueError('Argument `duration` should be <float>. Got {}'.format(repr(duration)))

        if type(target) in (list, tuple):
            target_pos = target
        else:
            target_pos = target.get_position()
        origin_pos = self.get_position()
        dir = [target_pos[0] - origin_pos[0], target_pos[1] - origin_pos[1]]
        return self.swipe(dir, duration=duration)

    def focus(self, f):
        """
        Get a new UI proxy copy with the given focus. Return a new UI proxy object as the UI proxy is immutable.

        Args:
            f (2-:obj:`tuple`/2-:obj:`list`/:obj:`str`): the focus point, it can be specified as 2-list/2-tuple
             coordinates (x, y) in NormalizedCoordinate system or as 'center' or 'anchor'.

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: a new UI proxy object (copy)
        """

        ret = copy.copy(self)
        ret._focus = f
        return ret

    def get_position(self, focus=None):
        """
        Get the position of the UI elements.

        Args:
            focus: focus point of UI proxy,  see :py:meth:`.focus() <poco.proxy.UIObjectProxy.focus>` for more details

        Returns:
            2-list/2-tuple: coordinates (x, y) in NormalizedCoordinate system

        Raises:
            TypeError: raised when unsupported focus type is specified
        """

        focus = focus or self._focus or 'anchor'
        if focus == 'anchor':
            pos = self.attr('pos')
        elif focus == 'center':
            x, y = self.attr('pos')
            w, h = self.get_size()
            ap_x, ap_y = self.attr("anchorPoint")
            fx, fy = 0.5, 0.5
            pos = [x + w * (fx - ap_x), y + h * (fy - ap_y)]
        elif type(focus) in (list, tuple):
            x, y = self.attr('pos')
            w, h = self.get_size()
            ap_x, ap_y = self.attr("anchorPoint")
            fx, fy = focus
            pos = [x + w * (fx - ap_x), y + h * (fy - ap_y)]
        else:
            raise TypeError('Unsupported focus type {}. '
                            'Only "anchor/center" or 2-list/2-tuple available.'.format(type(focus)))
        return pos

    def _direction_vector_of(self, dir):
        if dir == 'up':
            dir_vec = [0, -0.1]
        elif dir == 'down':
            dir_vec = [0, 0.1]
        elif dir == 'left':
            dir_vec = [-0.1, 0]
        elif dir == 'right':
            dir_vec = [0.1, 0]
        elif type(dir) in (list, tuple):
            dir_vec = dir
        else:
            raise TypeError('Unsupported direction type {}. '
                            'Only "up/down/left/right" or 2-list/2-tuple available.'.format(type(dir)))
        return dir_vec

    def wait(self, timeout=3):
        """
        Block and wait for max given time before the UI element appears.

        Args:
            timeout: maximum waiting time in seconds

        Returns:
            :py:class:`UIObjectProxy <poco.proxy.UIObjectProxy>`: self
        """

        start = time.time()
        while not self.exists():
            self.poco.sleep_for_polling_interval()
            if time.time() - start > timeout:
                break
        return self

    def wait_for_appearance(self, timeout=120):
        """
        Block and wait until the UI element **appears** within the given timeout. When timeout, the
        :py:class:`PocoTargetTimeout <poco.exceptions.PocoTargetTimeout>` is raised.

        Args:
            timeout: maximum waiting time in seconds

        Raises:
            PocoTargetTimeout: when timeout
        """

        start = time.time()
        while not self.exists():
            self.poco.sleep_for_polling_interval()
            if time.time() - start > timeout:
                raise PocoTargetTimeout('appearance', self)

    def wait_for_disappearance(self, timeout=120):
        """
        Block and wait until the UI element **disappears** within the given timeout.

        Args:
            timeout: maximum waiting time in seconds

        Raises:
            PocoTargetTimeout: when timeout
        """

        start = time.time()
        while self.exists():
            self.poco.sleep_for_polling_interval()
            if time.time() - start > timeout:
                raise PocoTargetTimeout('disappearance', self)

    @refresh_when(PocoTargetRemovedException)
    def attr(self, name):
        """
        Retrieve the attribute of UI element by given attribute name. Return None if attribute does not exist.
        If attribute type is :obj:`str`, it is encoded to utf-8 as :obj:`str` in Python2.7.

        Args:
            name: 
                attribute name, it can be one of the following or any other customized type implemented by SDK

                - visible: whether or not it is visible to user
                - text: string value of the UI element
                - type: the type name of UI element from remote runtime
                - pos: the position of the UI element
                - size: the percentage size [width, height] in range of 0~1 according to the screen
                - name: the name of UI element
                - ...: other sdk implemented attributes

        Returns:
            None if no such attribute or its value is None/null/nil/etc. Otherwise the attribute value is returned. The
            returned value type is json serializable.

        Raises:
            PocoNoSuchNodeException: when the UI element does not exists

        .. note:: Exception :py:class:`NodeHasBeenRemovedException` is caught automatically.
        
        See Also:
            :py:meth:`UI element attributes in poco sdk definition <poco.sdk.AbstractNode.AbstractNode.getAttr>`.
        """

        # to optimize performance speed, retrieve only the first matched element.
        # 优化速度，只选择第一个匹配到的节点
        nodes = self._do_query(multiple=False)
        val = self.poco.agent.hierarchy.getAttr(nodes, name)
        if six.PY2 and isinstance(val, unicode):
            val = val.encode('utf-8')
        return val

    @refresh_when(PocoTargetRemovedException)
    def setattr(self, name, val):
        """
        Change the attribute value of the UI element. Not all attributes can be casted to text. If changing the
        immutable attributes or attributes which do not exist, the InvalidOperationException exception is raised.

        Args:
            name: attribute name
            val: new attribute value to cast

        Raises:
            InvalidOperationException: when it fails to set the attribute on UI element
        """

        nodes = self._do_query(multiple=False)
        try:
            return self.poco.agent.hierarchy.setAttr(nodes, name, val)
        except UnableToSetAttributeException as e:
            raise InvalidOperationException('"{}" of "{}"'.format(e.message, self))

    def exists(self):
        """
        Test whether the UI element is in the hierarchy. Similar to :py:meth:`.attr('visible')
        <poco.proxy.UIObjectProxy.attr>`.

        Returns:
            bool: True if exists otherwise False
        """

        try:
            return self.attr('visible')
        except (PocoTargetRemovedException, PocoNoSuchNodeException):
            return False

    def get_text(self):
        """
        Get the text attribute of the UI element. Return None if no such attribute. Similar to :py:meth:`.attr('text')
        <poco.proxy.UIObjectProxy.attr>`.

        Returns:
            :obj:`str`: None if the UI element does not have the text element, otherwise the utf-8 encoded text value
        """

        text = self.attr('text')
        if six.PY2 and type(text) is unicode:
            text = text.encode('utf-8')
        return text

    def set_text(self, text):
        """
        Set the text attribute of the UI element. If the UI element does not support mutation, an exception is raised

        Args:
            text: the text value to be set

        Raises:
            InvalidOperationException: when unable to mutate text value of the UI element
        """

        return self.setattr('text', text)

    def get_name(self):
        """
        Get the UI element name attribute

        Returns:
            :obj:`str`: UI element name attribute
        """

        return self.attr('name')

    def get_size(self):
        """
        Get the UI element size in ``NormalizedCoordinate`` system.

        Returns:
            2-:obj:`list`: size [width, height] in range of 0 ~ 1.
        """

        return self.attr('size')

    def get_bounds(self):
        """
        Get the parameters of bounding box of the UI element.

        Returns:
            :obj:`list` <:obj:`float`>: 4-list (top, right, bottom, left) coordinates related to the edge of screen in
            NormalizedCoordinate system
        """

        size = self.get_size()
        top_left = self.get_position([0, 0])

        # t, r, b, l
        bounds = [top_left[1], top_left[0] + size[0], top_left[1] + size[1], top_left[0]]
        return bounds

    def __str__(self):
        return unicode(self).encode("utf-8")

    def __unicode__(self):
        return 'UIObjectProxy of "{}"'.format(query_expr(self.query))

    __repr__ = __str__

    @property
    def nodes(self):
        """
        Readonly property accessing the UI element(s) in the remote runtime.
        """

        return self._do_query()

    def invalidate(self):
        """
        Clear the flag to indicate to re-query or re-select the UI element(s) from hierarchy.
        """

        self._evaluated = False
        self._nodes = None

    def _do_query(self, multiple=True, refresh=False):
        if not self._evaluated or refresh:
            self._nodes = self.poco.agent.hierarchy.select(self.query, multiple)
            if len(self._nodes) == 0:
                raise PocoNoSuchNodeException(self)
            self._evaluated = True
            self._query_multiple = multiple
        return self._nodes
