"""
Copyright 2021 Daniel Afriyie

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    https://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""

import sys
import time
import warnings
import random as rd
from typing import Any, Optional, Callable, cast, overload

try:
    from selenium.webdriver.remote.webdriver import WebDriver, WebElement
    from selenium.webdriver.support import expected_conditions as ec
    from selenium.webdriver.common.action_chains import ActionChains
    from selenium.webdriver.support.wait import WebDriverWait
    from selenium.webdriver.common.by import By
    from selenium.webdriver.common.keys import Keys
    from selenium.common.exceptions import (
        TimeoutException,
        WebDriverException,
        ElementClickInterceptedException,
        StaleElementReferenceException,
    )
except ImportError:
    warnings.warn(
        "It seems you don't have selenium installed. "
        "Install it before using this module!\npip3 install selenium"
    )
    raise

try:
    import pyperclip
except ImportError:
    warnings.warn(
        "It seems you don't have pyperclip installed. "
        "Install it before using this module!\npip3 install pyperclip"
    )
    raise

type Number = int | float
type Action = Optional[str]
type Element = WebElement | list[WebElement]
type Condition[T] = Callable[[tuple[str, str]], T]


def window_scroll_to(driver: WebDriver, loc: Number) -> None:
    driver.execute_script(f"window.scrollTo(0, {loc});")


def scroll_into_view(driver: WebDriver, element: WebElement, offset: int = 200) -> None:
    window_scroll_to(driver, element.location["y"] - offset)


def scroll_into_view_js(driver: WebDriver, element: WebElement) -> None:
    driver.execute_script("arguments[0].scrollIntoView();", element)


def find_element(
        driver: WebDriver,
        locator: str,
        by: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        action: Action = None,
        *args: Any,
        **kwargs: Any,
) -> Element:
    wait: WebDriverWait = WebDriverWait(driver=driver, timeout=secs)
    element: Element = wait.until(condition((by, locator)))
    if action:
        if hasattr(element, action):
            action_func = getattr(element, action)
            action_func(*args, **kwargs)
    return element


def find_element_by_xpath(
        driver: WebDriver,
        xpath: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        action: Action = None,
        *args: Any,
        **kwargs: Any,
) -> Element:
    return find_element(
        driver, xpath, By.XPATH, secs, condition, action, *args, **kwargs
    )


def find_element_by_css(
        driver: WebDriver,
        selector: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        action: Action = None,
        *args: Any,
        **kwargs: Any,
) -> Element:
    return find_element(
        driver, selector, By.CSS_SELECTOR, secs, condition, action, *args, **kwargs
    )


def find_element_by_id(
        driver: WebDriver,
        id: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        action: Action = None,
        *args: Any,
        **kwargs: Any,
) -> Element:
    return find_element(driver, id, By.ID, secs, condition, action, *args, **kwargs)


def find_element_by_link_text(
        driver: WebDriver,
        text: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        action: Action = None,
        *args: Any,
        **kwargs: Any,
) -> Element:
    return find_element(
        driver, text, By.LINK_TEXT, secs, condition, action, *args, **kwargs
    )


def driver_or_js_click(
        driver: WebDriver,
        xpath: str,
        secs: Number = 5,
        condition: Condition = ec.element_to_be_clickable,
) -> None:
    elm: WebElement
    try:
        elm = cast(WebElement, find_element_by_xpath(driver, xpath, secs=secs, condition=condition))
        ActionChains(driver).move_to_element(elm).click().perform()
    except WebDriverException:
        elm = driver.find_element(By.XPATH, xpath)
        try:
            ActionChains(driver).move_to_element(elm).click().perform()
        except WebDriverException:
            driver.execute_script("arguments[0].click()", elm)


def manual_entry(
        driver: WebDriver,
        xpath: str,
        text: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        sleep_time: Number = 0.05,
        *args: Any,
) -> None:
    if (not isinstance(sleep_time, int)) and (not isinstance(sleep_time, float)):
        args += (sleep_time,)
        sleep_time = 0.05
    elm = cast(WebElement, find_element_by_xpath(driver, xpath, secs=secs, condition=condition))
    ActionChains(driver).move_to_element(elm).perform()
    elm.clear()
    text = f"{text}"
    for letter in text:
        elm.send_keys(letter)
        time.sleep(sleep_time)
    time.sleep(sleep_time)
    elm.send_keys(*args)


def enter(
        driver: WebDriver,
        xpath: str,
        text: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        *args: Any,
) -> None:
    elm: WebElement = cast(WebElement, find_element_by_xpath(driver, xpath, secs=secs, condition=condition))
    ActionChains(driver).move_to_element(elm).perform()
    elm.clear()
    text = f"{text}"
    elm.send_keys(text, *args)


def paste(
        driver: WebDriver,
        xpath: str,
        text: str,
        secs: Number = 10,
        condition: Condition = ec.element_to_be_clickable,
        paste_key: str = "v",
) -> None:
    elm = cast(WebElement, find_element_by_xpath(driver, xpath, secs=secs, condition=condition))
    pyperclip.copy(text)
    ctrl = Keys.COMMAND if sys.platform == "darwin" else Keys.CONTROL
    actions = ActionChains(driver)
    actions.move_to_element(elm)
    actions.click()
    actions.key_down(ctrl)
    actions.send_keys(paste_key)
    actions.key_up(ctrl)
    actions.perform()


def random_delay(a: int = 1, b: int = 3) -> None:
    delay: int = rd.randint(a, b)
    precision: float = delay / (a + b)
    sleep_time: float = delay + precision
    time.sleep(sleep_time)


click = driver_or_js_click
driver_wait = find_element_by_xpath
