package nl.hsac.fitnesse.fixture.util.selenium.by;

import nl.hsac.fitnesse.fixture.util.selenium.JavascriptHelper;
import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;

import java.util.List;

/**
 * Decorator for a By statement, always returning only a single element.
 * When the nested By returns multiple the first element displayed, and 'on top' is returned.
 */
public class BestMatchBy<T extends WebElement> extends SingleElementOrNullBy<T> {
    private static final String TOP_ELEMENT_AT =
            "if (arguments[0].getBoundingClientRect) {\n" +
                    "  var rect = arguments[0].getBoundingClientRect();\n" +
                    "  var x = (rect.left + rect.right)/2;\n" +
                    "  var y = (rect.top + rect.bottom)/2;\n" +
                    "  return document.elementFromPoint(x,y);\n" +
                    "} else { return null; }";

    private final By by;

    public BestMatchBy(By nestedBy) {
        this.by = nestedBy;
    }

    @Override
    public T findElement(SearchContext context) {
        return findElement(by, context);
    }

    public static <T extends WebElement> T findElement(By by, SearchContext context) {
        WebElement element = null;
        List<WebElement> elements = context.findElements(by);
        if (elements.size() == 1) {
            element = elements.get(0);
        } else if (elements.size() > 1) {
            element = selectBestElement(context, elements);
        }
        return (T) element;
    }

    private static WebElement selectBestElement(SearchContext context, List<WebElement> elements) {
        JavascriptExecutor jse = JavascriptHelper.getJavascriptExecutor(context);
        // take the first displayed element without any elements on top of it,
        // if none: take first displayed
        // or if none are displayed: just take the first
        WebElement element = elements.get(0);
        WebElement firstDisplayed = null;
        WebElement firstOnTop = null;
        if (!element.isDisplayed() || !isOnTop(jse, element)) {
            for (int i = 1; i < elements.size(); i++) {
                WebElement otherElement = elements.get(i);
                if (otherElement.isDisplayed()) {
                    if (firstDisplayed == null) {
                        firstDisplayed = otherElement;
                    }
                    if (isOnTop(jse, otherElement)) {
                        firstOnTop = otherElement;
                        element = otherElement;
                        break;
                    }
                }
            }
            if (firstOnTop == null
                    && firstDisplayed != null
                    && !element.isDisplayed()) {
                // none displayed and on top
                // first was not displayed, but another was
                element = firstDisplayed;
            }
        }
        return element;
    }

    private static <T extends WebElement> boolean isOnTop(JavascriptExecutor executor, T element) {
        T e = (T) JavascriptHelper.executeScript(executor, TOP_ELEMENT_AT, element);
        return element.equals(e);
    }

    @Override
    public String toString() {
        return "BestMatchOf: " + by;
    }
}

@@ -291,16 +291,54 @@
 il.List;
+%0Aimport java.util.function.BiFunction;
 %0A%0A/**%0A *
@@ -1015,16 +1015,149 @@
 ull; %7D%22;
+%0A    private static BiFunction%3CSearchContext, List%3CWebElement%3E, ? extends WebElement%3E BEST_FUNCTION = BestMatchBy::selectBestElement;
 %0A%0A    pr
@@ -1684,33 +1684,35 @@
 ement = 
-selectBestElement
+BEST_FUNCTION.apply
 (context
@@ -1766,38 +1766,37 @@
 nt;%0A    %7D%0A%0A    p
-rivate
+ublic
  static WebEleme
@@ -3397,32 +3397,553 @@
 uals(e);%0A    %7D%0A%0A
+    /**%0A     * @return function used to select best element when multiple elements were found.%0A     */%0A    public static BiFunction%3CSearchContext, List%3CWebElement%3E, ? extends WebElement%3E getBestFunction() %7B%0A        return BEST_FUNCTION;%0A    %7D%0A%0A    /**%0A     * @param bestFunction function to use to select best element from list of elements found.%0A     */%0A    public static void setBestFunction(BiFunction%3CSearchContext, List%3CWebElement%3E, ? extends WebElement%3E bestFunction) %7B%0A        BEST_FUNCTION = bestFunction;%0A    %7D%0A%0A
     @Override%0A  

Allow function used to select best element from list to be configured