package io.github.sukgu;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.lang.reflect.Method;
import java.util.List;

import org.openqa.selenium.ElementNotVisibleException;
import org.openqa.selenium.JavascriptException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.SearchContext;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.firefox.FirefoxDriver;
import org.openqa.selenium.ie.InternetExplorerDriver;
import org.openqa.selenium.remote.CommandExecutor;
import org.openqa.selenium.remote.RemoteWebDriver;
import org.openqa.selenium.remote.RemoteWebElement;
import org.openqa.selenium.remote.SessionId;
import org.openqa.selenium.support.ui.ExpectedCondition;
import org.openqa.selenium.support.ui.WebDriverWait;

public class Shadow {
	
	WebDriver driver;
	WebDriverException exception;
	WebDriverWait wait;
	CommandExecutor executer;
	SessionId sessionId;
	ChromeDriver chromeDriver;
	FirefoxDriver firfoxDriver;
	InternetExplorerDriver ieDriver;
	RemoteWebDriver remoteWebDriver;
	private int implicitWait = 0;
	private int explicitWait = 0;
	private int pollingTime = 0;
	
	public Shadow(WebDriver driver) {
		
		if (driver instanceof ChromeDriver) {
			sessionId  = ((ChromeDriver)driver).getSessionId();
			chromeDriver = (ChromeDriver)driver;
		} else if (driver instanceof FirefoxDriver) {
			sessionId  = ((FirefoxDriver)driver).getSessionId();
			firfoxDriver = (FirefoxDriver)driver;
		} else if (driver instanceof InternetExplorerDriver) {
			sessionId  = ((InternetExplorerDriver)driver).getSessionId();
			ieDriver = (InternetExplorerDriver)driver;
		} else if (driver instanceof RemoteWebDriver) {
			sessionId  = ((RemoteWebDriver)driver).getSessionId();
			remoteWebDriver = (RemoteWebDriver)driver;
		} 
		this.driver = driver;
	}
	
	private Object injectShadowExecuter(String javascript) {
		if(chromeDriver!=null) {
			JavascriptExecutor js = (JavascriptExecutor)chromeDriver;
			waitForPageLoaded();
			return js.executeScript(javascript);
		} else if (firfoxDriver!=null) {
			waitForPageLoaded();
			return firfoxDriver.executeScript(javascript);
		} else if (ieDriver!=null) {
			waitForPageLoaded();
			return ieDriver.executeScript(javascript);
		} else if (remoteWebDriver!=null) {
			JavascriptExecutor js = (JavascriptExecutor)remoteWebDriver;
			waitForPageLoaded();
			return js.executeScript(javascript);
		} else {
			return null;
		}
	}
	
	private Object injectShadowExecuter(String javascript, WebElement element) {
		if(chromeDriver!=null) {
			JavascriptExecutor js = (JavascriptExecutor)chromeDriver;
			waitForPageLoaded();
			return js.executeScript(javascript, element);
		} else if (firfoxDriver!=null) {
			waitForPageLoaded();
			return firfoxDriver.executeScript(javascript, element);
		} else if (ieDriver!=null) {
			waitForPageLoaded();
			return ieDriver.executeScript(javascript, element);
		} else if (remoteWebDriver!=null) {
			JavascriptExecutor js = (JavascriptExecutor)remoteWebDriver;
			waitForPageLoaded();
			return js.executeScript(javascript, element);
		} else {
			return null;
		}
	}
	
	private Object executerGetObject(String script) {
		String javascript = convertJStoText().toString();
		javascript += script;
		return injectShadowExecuter(javascript);
	}
	
	private Object executerGetObject(String script, WebElement element) {
		String javascript = convertJStoText().toString();
		javascript += script;
		return injectShadowExecuter(javascript, element);
	}
	
	private StringBuilder convertJStoText() {
		InputStream in = getClass().getResourceAsStream("/querySelector.js"); 
		BufferedReader reader = null;
		//File jsFile = new File("querySelector.js");
		//BufferedReader reader = null;
		StringBuilder text = new StringBuilder();
		//reader = new BufferedReader(new FileReader(jsFile));
		reader = new BufferedReader(new InputStreamReader(in)); 
		if(reader!=null) {
			try {
				while(reader.ready()) {
					text.append(reader.readLine());
				}
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		if(reader!=null) {
			try {
				reader.close();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
		return text;
	}
	
	private void fixLocator(SearchContext context, String cssLocator, WebElement element) {
		if (element instanceof RemoteWebElement) {
			try {
			@SuppressWarnings("rawtypes")
			Class[] parameterTypes = new Class[] { SearchContext.class,
			        String.class, String.class };
			Method m = element.getClass().getDeclaredMethod(
			        "setFoundBy", parameterTypes);
			m.setAccessible(true);
			Object[] parameters = new Object[] { context, "cssSelector", cssLocator };
			m.invoke(element, parameters);
			} catch (Exception fail) {
				//fail("Something bad happened when fixing locator");
			}
		}
	}
	
	private void waitForPageLoaded() {
        ExpectedCondition<Boolean> expectation = new
                ExpectedCondition<Boolean>() {
                    public Boolean apply(WebDriver driver) {
                        return ((JavascriptExecutor) driver).executeScript("return document.readyState").toString().equals("complete");
                    }
                };
        try {
            Thread.sleep(1000);
            WebDriverWait wait = new WebDriverWait(driver, 30);
            wait.until(expectation);
        } catch (Throwable error) {
            //Assertions.fail("Timeout waiting for Page Load Request to complete.");
        }
    }
	
	// wait methods on shadow objects
	public void setImplicitWait(int seconds) {
        this.implicitWait = seconds;
    }
	
	public void setExplicitWait(int seconds, int pollingTime) throws Exception {
		if(pollingTime > seconds) {
			throw new Exception("pollingTime can't be greater than wait time");
		}
        this.explicitWait = seconds;
        this.pollingTime = pollingTime;
    }
	
	private boolean isPresent(WebElement element) {
		boolean present = false;
		try {
			present =  (Boolean) executerGetObject("return isVisible(arguments[0]);", element);
		} catch(JavascriptException ex) {
			
		}
		return present;
	}
	
	public WebElement findElement(String cssSelector) {
		WebElement element = null;
		boolean visible = false;
		
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\");");
			fixLocator(driver, cssSelector, element);
			visible = isPresent(element);
		}
		
		if(explicitWait>0) {
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\");");
			fixLocator(driver, cssSelector, element);
			visible = isPresent(element);
			
			for(int i=0;i<explicitWait && !visible;) {
				try {
					Thread.sleep(pollingTime*1000);
					element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\");");
					fixLocator(driver, cssSelector, element);
					visible = isPresent(element);
					i=i+pollingTime;
				} catch (InterruptedException e) {
					
				}
			}
		}
		
		if(explicitWait == 0 && implicitWait == 0) {
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\");");
			fixLocator(driver, cssSelector, element);
		}
		
		if(!isPresent(element)) {
			throw new ElementNotVisibleException("Element with CSS "+cssSelector+" is not present on screen");
		}
		
		return element;

	}
	
	public WebElement findElement(WebElement parent, String cssSelector) {
		WebElement element = null;
		boolean visible = false;
		
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\", arguments[0]);", parent);
			fixLocator(driver, cssSelector, element);
			visible = isPresent(element);
		}
		
		if(explicitWait>0) {
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\", arguments[0]);", parent);
			fixLocator(driver, cssSelector, element);
			visible = isPresent(element);
			
			for(int i=0;i<explicitWait && !visible;) {
				try {
					Thread.sleep(pollingTime*1000);
					element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\", arguments[0]);", parent);
					fixLocator(driver, cssSelector, element);
					visible = isPresent(element);
					i=i+pollingTime;
				} catch (InterruptedException e) {
					
				}
			}
			
		}
		
		if(explicitWait == 0 && implicitWait == 0) {
			element = (WebElement) executerGetObject("return getObject(\""+cssSelector+"\", arguments[0]);", parent);
			fixLocator(driver, cssSelector, element);
		}
		
		if(!isPresent(element)) {
			throw new ElementNotVisibleException("Element with CSS "+cssSelector+" is not present on screen");
		}
		
		return element;
	}
	
	@SuppressWarnings("unchecked")
	public List<WebElement> findElements(String cssSelector) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		List<WebElement> element = null;
		Object object = executerGetObject("return getAllObject(\""+cssSelector+"\");");
		if(object!=null && object instanceof List<?>) {
			element = (List<WebElement>) object;
		}
		for (WebElement webElement : element) {
			fixLocator(driver, cssSelector, webElement);
		}
		return element;
	}
	
	@SuppressWarnings("unchecked")
	public List<WebElement> findElements(WebElement parent, String cssSelector) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		List<WebElement> element = null;
		Object object = executerGetObject("return getAllObject(\""+cssSelector+"\", arguments[0]);", parent);
		if(object!=null && object instanceof List<?>) {
			element = (List<WebElement>) object;
		}
		for (WebElement webElement : element) {
			fixLocator(driver, cssSelector, webElement);
		}
		return element;
	}
	
	public WebElement getShadowElement(WebElement parent,String selector) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		WebElement element = null;
		element = (WebElement) executerGetObject("return getShadowElement(arguments[0],\""+selector+"\");", parent);
		fixLocator(driver, selector, element);
		return element;
	}
	
	@SuppressWarnings("unchecked")
	public List<WebElement> getAllShadowElement(WebElement parent,String selector) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		List<WebElement> elements = null;
		Object object = executerGetObject("return getAllShadowElement(arguments[0],\""+selector+"\");", parent);
		if(object!=null && object instanceof List<?>) {
			elements = (List<WebElement>) object;
		}
		for (WebElement element : elements) {
			fixLocator(driver, selector, element);
		}
		return elements;
	}

	public WebElement getParentElement(WebElement element) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		return (WebElement) executerGetObject("return getParentElement(arguments[0]);", element);
	}
	
	@SuppressWarnings("unchecked")
	public List<WebElement> getChildElements(WebElement parent) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		List<WebElement> elements = null;
		Object object = executerGetObject("return getChildElements(arguments[0]);", parent);
		if(object!=null && object instanceof List<?>) {
			elements = (List<WebElement>) object;
		}
		return elements;
	}
	
	@SuppressWarnings("unchecked")
	public List<WebElement> getSiblingElements(WebElement element) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		List<WebElement> elements = null;
		Object object = executerGetObject("return getSiblingElements(arguments[0]);", element);
		if(object!=null && object instanceof List<?>) {
			elements = (List<WebElement>) object;
		}
		return elements;
	}
	
	public WebElement getSiblingElement(WebElement element, String selector) {
		if(implicitWait>0) {
			try {
				Thread.sleep(implicitWait*1000);
			} catch (InterruptedException e) {
				
			}
		}
		return (WebElement)  executerGetObject("return getSiblingElement(arguments[0],\""+selector+"\");", element);
	}
	
	public WebElement getNextSiblingElement(WebElement element) {
		return (WebElement)  executerGetObject("return getNextSiblingElement(arguments[0]);", element);
	}
	
	public WebElement getPreviousSiblingElement(WebElement element) {
		return (WebElement)  executerGetObject("return getPreviousSiblingElement(arguments[0]);", element);
	}
	
	public boolean isVisible(WebElement element) {
		return (Boolean) executerGetObject("return isVisible(arguments[0]);", element);
	}
	
	public boolean isChecked(WebElement element) {
		return (Boolean) executerGetObject("return isChecked(arguments[0]);", element);
	}
	
	public boolean isDisabled(WebElement element) {
		return (Boolean) executerGetObject("return isDisabled(arguments[0]);", element);
	}
	
	public String getAttribute(WebElement element,String attribute) {
		return (String) executerGetObject("return getAttribute(arguments[0],\""+attribute+"\");", element);
	}
	
	public void selectCheckbox(WebElement parentElement,String label) {
		executerGetObject("return selectCheckbox(\""+label+"\",arguments[0]);", parentElement);
	}
	
	public void selectCheckbox(String label) {
		executerGetObject("return selectCheckbox(\""+label+"\");");
	}
	
	public void selectRadio(WebElement parentElement,String label) {
		executerGetObject("return selectRadio(\""+label+"\",arguments[0]);", parentElement);
	}
	
	public void selectRadio(String label) {
		executerGetObject("return selectRadio(\""+label+"\");");
	}
	
	public void selectDropdown(WebElement parentElement,String label) {
		executerGetObject("return selectDropdown(\""+label+"\",arguments[0]);", parentElement);
	}
	
	public void selectDropdown(String label) {
		executerGetObject("return selectDropdown(\""+label+"\");");
	}
	
	public void scrollTo(WebElement element) {
		executerGetObject("return scrollTo(arguments[0]);", element);
	}
	
	
}