from inspect import getmembers, isroutine, stack
from re import search, sub

import six
from selenium.common.exceptions import ElementNotInteractableException, \
    NoSuchWindowException, StaleElementReferenceException
from selenium.webdriver.common.action_chains import ActionChains

import nerodia
from nerodia.adjacent import Adjacent
from nerodia.browser import Browser
from nerodia.container import Container
from nerodia.elements.scroll import Scrolling
from nerodia.exception import Error, NoMatchingWindowFoundException, ObjectDisabledException, \
    ObjectReadOnlyException, UnknownFrameException, UnknownObjectException
from nerodia.js_execution import JSExecution
from nerodia.js_snippet import JSSnippet
from nerodia.locators.class_helpers import ClassHelpers
from nerodia.locators.element.selector_builder import SelectorBuilder
from nerodia.user_editable import UserEditable
from nerodia.wait.wait import TimeoutError, Waitable
from nerodia.window import Dimension, Point


class Element(ClassHelpers, JSExecution, Container, JSSnippet, Waitable, Adjacent, Scrolling):
    ATTRIBUTES = []
    CASE_INSENSITIVE_ATTRIBUTES = ['accept', 'accept_charset', 'align', 'alink', 'axis', 'bgcolor',
                                   'charset', 'checked', 'clear', 'codetype', 'color', 'compact',
                                   'declare', 'defer', 'dir', 'direction', 'disabled', 'enctype',
                                   'face', 'frame', 'hreflang', 'http_equiv', 'lang', 'language',
                                   'link', 'media', 'method', 'multiple', 'nohref', 'noresize',
                                   'noshade', 'nowrap', 'readonly', 'rel', 'rev', 'rules', 'scope',
                                   'scrolling', 'selected', 'shape', 'target', 'text', 'type',
                                   'valign', 'valuetype', 'vlink']
    keyword = None

    _content_editable = None
    _selector_builder = None
    _element_matcher = None
    _locator = None

    def __init__(self, query_scope, selector):
        self.query_scope = query_scope
        if not isinstance(selector, dict):
            raise TypeError('invalid argument: {!r}'.format(selector))

        self.el = selector.pop('element', None)
        if self.el and len(set(selector) - {'tag_name'}) > 0:
            nerodia.logger.deprecate("'element' locator to initialize a relocatable Element",
                                     '#cache=', ids=['element_cache'])
        self.selector = selector

        if self.el is None:
            self.build()

    @property
    def exists(self):
        """
        Returns True if element exists, False otherwise
        Checking for staleness is deprecated
        :rtype: bool
        """
        try:
            if self._located and self.stale:
                self.reset()
            elif self._located:
                return True
            self.assert_exists()
            return True
        except (UnknownObjectException, UnknownFrameException):
            return False

    exist = exists

    def __repr__(self):
        string = '#<{}: '.format(self.__class__.__name__)
        if self.keyword:
            string += 'keyword: {} '.format(self.keyword)
        string += 'located: {}; '.format(self._located)
        if not self.selector:
            string += '{element: (selenium element)}'
        else:
            string += self.selector_string
        string += '>'
        return string

    def __eq__(self, other):
        """
        Returns True if two elements are equal
        :param other: other element to compare
        :rtype: bool
        """
        return isinstance(other, self.__class__) and self.wd == other.wd

    eql = __eq__

    def __hash__(self):
        return self.el.__hash__() if self._located else super(Element, self).__hash__()

    @property
    def text(self):
        """
        Returns the text of the element
        :rtype: str
        """
        return self._element_call(lambda: self.el.text)

    @property
    def tag_name(self):
        """
        Returns the tag name of the element
        :rtype: str
        """
        return self._element_call(lambda: self.el.tag_name).lower()

    def click(self, *modifiers):
        """
        Clicks the element, optionally while pressing the given modifier keys.
        Note that support for holding a modifier key is currently experimental, and may not work
        at all.

        :param modifiers: modifier keys to press while clicking
        :Example: Click an element

        browser.element(name='new_user_button').click()

        :Example: Click an element with shift key pressed
        from selenium.webdriver.common.keys import Keys
        browser.element(name='new_user_button').click(Keys.SHIFT)

        :Example: Click an element with several modifier keys pressed
        from selenium.webdriver.common.keys import Keys
        browser.element(name='new_user_button').click(Keys.SHIFT, Keys.CONTROL)
        """

        def method():
            if modifiers:
                action = ActionChains(self.driver)
                for mod in modifiers:
                    action.key_down(mod)
                action.click(self.el)
                for mod in modifiers:
                    action.key_up(mod)
                action.perform()
            else:
                self.el.click()

        self._element_call(method, self.wait_for_enabled)
        self.browser.after_hooks.run()

    def js_click(self):
        """
        Simulates JavaScript click event on element.

        :Example: Click an element
        browser.element(name='new_user_button').js_click()
        """
        self.fire_event('click')
        self.browser.after_hooks.run()

    def double_click(self):
        """
        Double clicks the element.
        Note that browser support may vary.

        :Example: Double-click an element

        browser.element(name='new_user_button').double_click()
        """
        self._element_call(lambda: ActionChains(self.driver).double_click(self.el)
                           .perform(), self.wait_for_present)
        self.browser.after_hooks.run()

    def js_double_click(self):
        """
        Simulates JavaScript double click event on element.

        :Example: Click an element
        browser.element(name='new_user_button').js_double_click()
        """
        self.fire_event('dblclick')
        self.browser.after_hooks.run()

    def right_click(self, *modifiers):
        """
        Right clicks the element, optionally while pressing the given modifier keys.
        Note that support for holding a modifier key is currently experimental,
        and may not work at all. Also, the browser support may vary.

        :Example: Right click an element

        browser.element(name='new_user_button').right_click()

        :Example: Right click an element with shift key pressed
        browser.element(name='new_user_button').right_click(nerodia.Keys.SHIFT)

        :Example: Click an element with several modifier keys pressed
        browser.element(name='new_user_button').right_click(nerodia.Keys.SHIFT, nerodia.Keys.ALT)
        """

        def _right_click():
            action = ActionChains(self.driver)
            if len(modifiers) > 0:
                for mod in modifiers:
                    action.key_down(mod)
                action.context_click(self.el)
                for mod in modifiers:
                    action.key_up(mod)
                action.perform()
            else:
                action.context_click(self.el).perform()

        self._element_call(_right_click, self.wait_for_present)
        self.browser.after_hooks.run()

    def hover(self):
        """
        Moves the mouse to the middle of this element
        Note that browser support may vary

        :Example: Hover over an element

        browser.element(name='new_user_button').hover()
        """
        self._element_call(lambda: ActionChains(self.driver).move_to_element(self.el)
                           .perform(), self.wait_for_present)
        self.browser.after_hooks.run()

    def drag_and_drop_on(self, other):
        """
        Drag and drop this element on to another element instance
        Note that browser support may vary

        :param other: element to drop on

        :Example: Drag an element onto another

        a = browser.div(id='draggable')
        b = browser.div(id='droppable')
        a.drag_and_drop_on(b)