package com.codeborne.selenide.impl;

import com.codeborne.selenide.SelenideElement;
import com.codeborne.selenide.ex.ElementNotFound;
import com.codeborne.selenide.ex.InvalidStateException;
import com.codeborne.selenide.ex.UIAssertionError;
import com.codeborne.selenide.commands.Commands;
import com.codeborne.selenide.logevents.SelenideLog;
import com.codeborne.selenide.logevents.SelenideLogger;
import org.openqa.selenium.InvalidElementStateException;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Set;

import static com.codeborne.selenide.Condition.exist;
import static com.codeborne.selenide.Configuration.AssertionMode.SOFT;
import static com.codeborne.selenide.Configuration.*;
import static com.codeborne.selenide.Selenide.sleep;
import static com.codeborne.selenide.logevents.LogEvent.EventStatus.PASSED;
import static java.lang.System.currentTimeMillis;
import static java.util.Arrays.asList;

class SelenideElementProxy implements InvocationHandler {
  private static final Set<String> methodsToSkipLogging = new HashSet<>(asList(
      "toWebElement",
      "toString"
  ));

  private static final Set<String> methodsForSoftAssertion = new HashSet<>(asList(
      "should",
      "shouldBe",
      "shouldHave",
      "shouldNot",
      "shouldNotHave",
      "shouldNotBe",
      "waitUntil",
      "waitWhile"
  ));

  private final WebElementSource webElementSource;
  
  protected SelenideElementProxy(WebElementSource webElementSource) {
    this.webElementSource = webElementSource;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object... args) throws Throwable {
    if (methodsToSkipLogging.contains(method.getName()))
      return dispatchSelenideMethod(proxy, method, args);

    SelenideLog log = SelenideLogger.beginStep(webElementSource.getSearchCriteria(), method.getName(), args);
    try {
      Object result = dispatchAndRetry(proxy, method, args);
      SelenideLogger.commitStep(log, PASSED);
      return result;
    }
    catch (Error error) {
      SelenideLogger.commitStep(log, error);
      if (assertionMode == SOFT && methodsForSoftAssertion.contains(method.getName()))
        return proxy;
      else
        throw UIAssertionError.wrap(error);
    }
    catch (RuntimeException error) {
      SelenideLogger.commitStep(log, error);
      throw error;
    }
  }

  protected Object dispatchAndRetry(Object proxy, Method method, Object[] args) throws Throwable {
    long timeoutMs = getTimeoutMs(method, args);

    final long startTime = currentTimeMillis();
    Throwable lastError;
    do {
      try {
        return SelenideElement.class.isAssignableFrom(method.getDeclaringClass()) ?
            dispatchSelenideMethod(proxy, method, args) :
            delegateSeleniumMethod(webElementSource.getWebElement(), method, args);
      }
      catch (Throwable e) {
        if (Cleanup.of.isInvalidSelectorError(e)) {
          throw Cleanup.of.wrap(e);
        }
        lastError = e;
        sleep(pollingInterval);
      }
    }
    while (currentTimeMillis() - startTime <= timeoutMs);

    if (lastError instanceof UIAssertionError) {
      UIAssertionError uiError = (UIAssertionError) lastError;
      uiError.timeoutMs = timeoutMs;
      throw uiError;
      
    }
    else if (lastError instanceof InvalidElementStateException) {
      InvalidStateException uiError = new InvalidStateException(lastError);
      uiError.timeoutMs = timeoutMs;
      throw uiError;
    }
    else if (lastError instanceof WebDriverException) {
      ElementNotFound uiError = webElementSource.createElementNotFoundError(exist, lastError);
      uiError.timeoutMs = timeoutMs;
      throw uiError;
    }
    throw lastError;
  }

  private long getTimeoutMs(Method method, Object[] args) {
    return "waitUntil".equals(method.getName()) || "waitWhile".equals(method.getName()) ?
        (Long) args[args.length - 1] : timeout;
  }

  protected Object dispatchSelenideMethod(Object proxy, Method method, Object[] args) throws Throwable {
    return Commands.collection.execute(proxy, webElementSource, method.getName(), args);
  }
  
  static Object delegateSeleniumMethod(WebElement delegate, Method method, Object[] args) throws Throwable {
    try {
      return method.invoke(delegate, args);
    } catch (InvocationTargetException e) {
      throw e.getTargetException();
    }
  }
}

@@ -78,16 +78,65 @@
 lement;%0A
+import com.codeborne.selenide.commands.Commands;%0A
 import c
@@ -177,16 +177,16 @@
 tFound;%0A
-
 import c
@@ -288,57 +288,8 @@
 or;%0A
-import com.codeborne.selenide.commands.Commands;%0A
 impo
@@ -500,47 +500,8 @@
 ion;
-%0Aimport org.openqa.selenium.WebElement;
 %0A%0Aim
@@ -546,60 +546,8 @@
 er;%0A
-import java.lang.reflect.InvocationTargetException;%0A
 impo
@@ -1798,44 +1798,77 @@
 urn 
-dispatchSelenideMethod(proxy, method
+Commands.collection.execute(proxy, webElementSource, method.getName()
 , ar
@@ -2731,23 +2731,20 @@
         
-return 
+if (
 Selenide
@@ -2801,18 +2801,19 @@
 Class())
- ?
+) %7B
 %0A       
@@ -2819,90 +2819,132 @@
     
-  dispatchSelenideMethod(proxy, method, args) :%0A            delegateSeleniumMethod
+return Commands.collection.execute(proxy, webElementSource, method.getName(), args);%0A        %7D%0A%0A        return method.invoke
 (web
@@ -2973,24 +2973,16 @@
 ement(),
- method,
  args);%0A
@@ -4023,16 +4023,16 @@
 me()) ?%0A
+
         
@@ -4078,462 +4078,7 @@
 %0A  %7D
-%0A%0A  protected Object dispatchSelenideMethod(Object proxy, Method method, Object%5B%5D args) throws Throwable %7B%0A    return Commands.collection.execute(proxy, webElementSource, method.getName(), args);%0A  %7D%0A  %0A  static Object delegateSeleniumMethod(WebElement delegate, Method method, Object%5B%5D args) throws Throwable %7B%0A    try %7B%0A      return method.invoke(delegate, args);%0A    %7D catch (InvocationTargetException e) %7B%0A      throw e.getTargetException();%0A    %7D%0A  %7D
 %0A%7D%0A

simplify class SelenideElementProxy - inline few methods