package com.codeborne.selenide.selector;

import org.openqa.selenium.By;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.NoSuchElementException;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.WrapsDriver;
import org.openqa.selenium.internal.FindsByCssSelector;

import java.io.Serializable;
import java.util.Arrays;
import java.util.List;

public class ByShadow {

  /**
   * Find target elements inside shadow-root that attached to shadow-host.
   * <br/> Supports inner shadow-hosts.
   *
   * <br/> For example: shadow-host > inner-shadow-host > target-element
   * (each shadow-host must be specified explicitly).
   *
   * @param target           CSS expression of target element
   * @param shadowHost       CSS expression of the shadow-host with attached shadow-root
   * @param innerShadowHosts subsequent inner shadow-hosts
   * @return A By which locates elements by CSS inside shadow-root.
   */
  public static By cssSelector(String target, String shadowHost, String... innerShadowHosts) {
    return new ByShadowCss(target, shadowHost, innerShadowHosts);
  }

  public static class ByShadowCss extends By implements Serializable {
    private static final long serialVersionUID = -1230258723099459239L;

    private final String shadowHost;
    private final String[] innerShadowHosts;
    private final String target;

    ByShadowCss(String target, String shadowHost, String... innerShadowHosts) {
      if (shadowHost == null || target == null) {
        throw new IllegalArgumentException("Cannot find elements when the selector is null");
      }
      this.shadowHost = shadowHost;
      this.innerShadowHosts = innerShadowHosts;
      this.target = target;
    }

    @Override
    public WebElement findElement(SearchContext context) {
      WebElement host = ((FindsByCssSelector) context).findElementByCssSelector(shadowHost);
      for (String innerHost : innerShadowHosts) {
        host = getElementInsideShadowTree(host, innerHost);
      }

      return getElementInsideShadowTree(host, target);
    }

    @Override
    public List<WebElement> findElements(SearchContext context) {
      WebElement host = ((FindsByCssSelector) context).findElementByCssSelector(shadowHost);
      for (String innerHost : innerShadowHosts) {
        host = getElementInsideShadowTree(host, innerHost);
      }

      return getElementsInsideShadowTree(host, target);
    }

    private WebElement getElementInsideShadowTree(WebElement host, String target) {
      if (isShadowRootAttached(host)) {
        WebElement targetWebElement = (WebElement) getJavascriptExecutor(host)
          .executeScript("return arguments[0].shadowRoot.querySelector(arguments[1])", host, target);
        if (targetWebElement == null) {
          throw new NoSuchElementException("The element was not found: " + target);
        }
        return targetWebElement;
      } else {
        throw new NoSuchElementException("The element is not a shadow host or has 'closed' shadow-dom mode: " + host);
      }
    }

    private boolean isShadowRootAttached(WebElement host) {
      return getJavascriptExecutor(host).executeScript("return arguments[0].shadowRoot", host) != null;
    }

    @SuppressWarnings("unchecked")
    private List<WebElement> getElementsInsideShadowTree(WebElement host, String sh) {
      return (List<WebElement>) getJavascriptExecutor(host)
        .executeScript("return arguments[0].shadowRoot.querySelectorAll(arguments[1])", host, sh);
    }

    private JavascriptExecutor getJavascriptExecutor(SearchContext context) {
      JavascriptExecutor jsExecutor;
      if (context instanceof JavascriptExecutor) {
        jsExecutor = (JavascriptExecutor) context;
      } else {
        jsExecutor = (JavascriptExecutor) ((WrapsDriver) context).getWrappedDriver();
      }
      return jsExecutor;
    }

    @Override
    public String toString() {
      StringBuilder sb = new StringBuilder("By.cssSelector: ");
      sb.append(shadowHost).append(" ");
      if (innerShadowHosts.length > 0) {
        sb.append(Arrays.toString(innerShadowHosts)).append(" ");
      }
      sb.append(target);
      return sb.toString();
    }
  }
}

@@ -288,64 +288,8 @@
 ver;
-%0Aimport org.openqa.selenium.internal.FindsByCssSelector;
 %0A%0Aim
@@ -1808,38 +1808,16 @@
 st =
- ((FindsByCssSelector)
  context
 ).fi
@@ -1804,33 +1804,32 @@
 t host = context
-)
 .findElementByCs
@@ -1816,35 +1816,37 @@
 text.findElement
+(
 By
-C
+.c
 ssSelector(shado
@@ -1843,32 +1843,33 @@
 ctor(shadowHost)
+)
 ;%0A      for (Str
@@ -2142,38 +2142,16 @@
 st =
- ((FindsByCssSelector)
  context
 ).fi
@@ -2146,17 +2146,16 @@
  context
-)
 .findEle
@@ -2162,11 +2162,13 @@
 ment
+(
 By
-C
+.c
 ssSe
@@ -2181,24 +2181,25 @@
 (shadowHost)
+)
 ;%0A      for 

remove usages of deprecated interface FindsByCssSelector