from functools import reduce
from xpath import dsl as x

import capybara
from capybara.compat import str_
from capybara.helpers import desc
from capybara.selector.filter_set import add_filter_set, remove_filter_set, filter_sets
from capybara.selector.selector import add_selector, remove_selector, selectors
from capybara.utils import isregex


__all__ = ["add_filter_set", "add_selector", "filter_sets", "remove_filter_set", "remove_selector",
           "selectors"]


with add_selector("css") as s:
    s.css = lambda css: css

with add_selector("xpath") as s:
    s.xpath = lambda xpath: xpath

with add_selector("id") as s:
    @s.xpath
    def xpath(id):
        return x.descendant()[x.attr("id") == id]

with add_filter_set("field") as fs:
    @fs.node_filter("checked", boolean=True)
    def checked(node, value):
        return not node.checked ^ value

    @fs.node_filter("disabled", boolean=True, default=False, skip_if="all")
    def disabled(node, value):
        return not node.disabled ^ value

    @fs.node_filter("id")
    def id(node, value):
        return node["id"] == value

    @fs.expression_filter("name")
    def name(expr, value):
        return expr[x.attr("name") == value]

    @fs.expression_filter("placeholder")
    def placeholder(expr, value):
        return expr[x.attr("placeholder") == value]

    @fs.node_filter("readonly", boolean=True)
    def readonly(node, value):
        return not node.readonly ^ value

    @fs.node_filter("unchecked", boolean=True)
    def unchecked(node, value):
        return node.checked ^ value

    @fs.describe
    def describe(options):
        description, states = "", []

        if options.get("checked") or options.get("unchecked") is False:
            states.append("checked")
        if options.get("unchecked") or options.get("checked") is False:
            states.append("not checked")
        if options.get("disabled") is True:
            states.append("disabled")

        if states:
            description += " that is {}".format(" and ".join(states))

        return description

with add_selector("button") as s:
    @s.xpath
    def xpath(locator):
        input_button_expr = x.descendant("input")[
            x.attr("type").one_of("submit", "reset", "image", "button")]
        button_expr = x.descendant("button")
        image_button_expr = x.descendant("input")[x.attr("type").equals("image")]

        if locator:
            attr_matchers = (
                x.attr("id").equals(locator) |
                x.attr("value").is_(locator) |
                x.attr("title").is_(locator))
            image_attr_matchers = x.attr("alt").is_(locator)

            if capybara.enable_aria_label:
                attr_matchers |= x.attr("aria-label").is_(locator)
                image_attr_matchers |= x.attr("aria-label").is_(locator)

            input_button_expr = input_button_expr[attr_matchers]
            button_expr = button_expr[
                attr_matchers |
                x.string.n.is_(locator) |
                x.descendant("img")[x.attr("alt").is_(locator)]]
            image_button_expr = image_button_expr[image_attr_matchers]

        return input_button_expr + button_expr + image_button_expr

    @s.node_filter("disabled", boolean=True, default=False, skip_if="all")
    def disabled(node, value):
        return not node.disabled ^ value

    @s.describe
    def describe(options):
        description = ""
        if options.get("disabled") is True:
            description += " that is disabled"
        return description

with add_selector("checkbox") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("input")[x.attr("type").equals("checkbox")]
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

with add_selector("field") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("input", "select", "textarea")[
            ~x.attr("type").one_of("hidden", "image", "submit")]
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

    @s.expression_filter("field_type")
    def field_type(expr, value):
        if value in ["select", "textarea"]:
            return expr.axis("self", value)
        else:
            return expr[x.attr("type").equals(value)]

    @s.node_filter("value")
    def value(node, value):
        if isregex(value):
            return bool(value.search(node.value))
        else:
            return node.value == value

    @s.describe
    def describe(options):
        description = ""
        if options.get("value"):
            description += " with value {}".format(desc(options["value"]))
        return description

with add_selector("fieldset") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("fieldset")
        if locator:
            expr = expr[
                x.attr("id").equals(locator) |
                x.child("legend")[x.string.n.is_(locator)]]
        return expr

with add_selector("file_field") as s:
    s.label = "file field"

    @s.xpath
    def xpath(locator):
        expr = x.descendant("input")[x.attr("type").equals("file")]
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

with add_selector("fillable_field") as s:
    s.label = "field"

    @s.xpath
    def xpath(locator):
        expr = x.descendant("input", "textarea")[
            ~x.attr("type").one_of("checkbox", "file", "hidden", "image", "radio", "submit")]
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

    @s.node_filter("value")
    def value(node, value):
        if isregex(value):
            return bool(value.search(node.value))
        else:
            return node.value == value

    @s.describe
    def describe(options):
        description = ""
        if options.get("value"):
            description += " with value {}".format(desc(options["value"]))
        return description

with add_selector("frame") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("frame") + x.descendant("iframe")
        if locator:
            expr = expr[x.attr("id").equals(locator) | x.attr("name").equals(locator)]
        return expr

    @s.expression_filter("name")
    def name(expr, value):
        return expr[x.attr("name").equals(value)]

with add_selector("label") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("label")
        if locator:
            expr = expr[x.string.n.is_(str_(locator)) | x.attr("id").equals(str_(locator))]
        return expr

    @s.node_filter("field")
    def field(node, field_or_value):
        from capybara.node.element import Element

        if isinstance(field_or_value, Element):
            if field_or_value["id"] and field_or_value["id"] == node["for"]:
                return True
            else:
                return node.base in field_or_value._find_xpath("./ancestor::label[1]")
        else:
            return node["for"] == str_(field_or_value)

    @s.describe
    def describe(options):
        description = ""
        if options.get("field"):
            description += " for {}".format(options["field"])
        return description

with add_selector("link") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("a")[x.attr("href")]

        if locator:
            attr_matchers = (
                x.attr("id").equals(locator) |
                x.attr("title").is_(locator) |
                x.string.n.is_(locator))

            if capybara.enable_aria_label:
                attr_matchers |= x.attr("aria-label").is_(locator)

            expr = expr[
                attr_matchers |
                x.descendant("img")[x.attr("alt").is_(locator)]]

        return expr

    @s.node_filter("href")
    def href(node, href):
        if isregex(href):
            return bool(href.search(node["href"]))
        else:
            # For href element attributes, Selenium returns the full URL that would
            # be visited rather than the raw value in the source. So we use XPath.
            query = x.axis("self")[x.attr("href") == str_(href)]
            return node.has_selector("xpath", query)

    @s.describe
    def describe(options):
        description = ""
        if options.get("href"):
            description += " with href {}".format(desc(options["href"]))
        return description

with add_selector("link_or_button") as s:
    s.label = "link or button"

    @s.xpath
    def xpath(locator):
        return selectors["link"](locator) + selectors["button"](locator)

    @s.node_filter("disabled", boolean=True, default=False, skip_if="all")
    def disabled(node, value):
        return (
            node.tag_name == "a" or
            not node.disabled ^ value)

    @s.describe
    def describe(options):
        description = ""
        if options.get("disabled") is True:
            description += " that is disabled"
        return description

with add_selector("option") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("option")
        if locator:
            expr = expr[x.string.n.is_(locator)]
        return expr

with add_selector("radio_button") as s:
    s.label = "radio button"

    @s.xpath
    def xpath(locator):
        expr = x.descendant("input")[x.attr("type").equals("radio")]
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

with add_selector("select") as s:
    s.label = "select box"

    @s.xpath
    def xpath(locator):
        expr = x.descendant("select")
        expr = _locate_field(expr, locator)
        return expr

    s.filter_set("field")

    @s.node_filter("multiple", boolean=True)
    def multiple(node, value):
        return not node.multiple ^ value

    @s.node_filter("options")
    def options(node, options):
        if node.visible:
            actual = [n.text for n in node.find_all("xpath", ".//option")]
        else:
            actual = [n.all_text for n in node.find_all("xpath", ".//option", visible=False)]

        return sorted(options) == sorted(actual)

    @s.node_filter("selected")
    def selected(node, selected):
        if not isinstance(selected, list):
            selected = [selected]

        actual = [
            n.all_text
            for n in node.find_all("xpath", ".//option", visible=False)
            if n.selected]

        return sorted(selected) == sorted(actual)

    @s.expression_filter("with_options")
    def with_options(expr, options):
        return reduce(lambda xpath, option: xpath[selectors["option"](option)], options, expr)

    @s.describe
    def describe(options):
        description = ""

        if options.get("multiple") is True:
            description += " with the multiple attribute"
        if options.get("multiple") is False:
            description += " without the multiple attribute"
        if options.get("options"):
            description += " with options {}".format(desc(options["options"]))
        if options.get("selected"):
            description += " with {} selected".format(desc(options["selected"]))
        if options.get("with_options"):
            description += " with at least options {}".format(desc(options["with_options"]))

        return description

with add_selector("table") as s:
    @s.xpath
    def xpath(locator):
        expr = x.descendant("table")
        if locator:
            expr = expr[
                x.attr("id").equals(locator) |
                x.descendant("caption").is_(locator)]
        return expr


def _locate_field(field_expr, locator):
    expr = field_expr

    if locator:
        attr_matchers = (
            x.attr("id").equals(locator) |
            x.attr("name").equals(locator) |
            x.attr("placeholder").equals(locator) |
            x.attr("id").equals(x.anywhere("label")[x.string.n.is_(locator)].attr("for")))

        if capybara.enable_aria_label:
            attr_matchers |= x.attr("aria-label").is_(locator)

        expr = expr[attr_matchers]
        expr += x.descendant("label")[x.string.n.is_(locator)].descendant(field_expr)

    return expr
