package ru.yandex.autotests.direct.web.util;

import java.lang.reflect.Method;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.stream.Collectors;

import org.hamcrest.Matcher;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriverException;
import org.openqa.selenium.WebElement;

import ru.yandex.autotests.direct.web.data.RadioInputMethod;
import ru.yandex.autotests.direct.web.data.SelectInputMethod;
import ru.yandex.autotests.direct.web.util.matchers.IsDisplayedMatcher;
import ru.yandex.autotests.direct.web.webelements.NumericInput;
import ru.yandex.autotests.direct.web.webelements.bem.TextAreaBEM;
import ru.yandex.qatools.allure.webdriver.rules.WebDriverConfiguration;
import ru.yandex.qatools.htmlelements.element.Button;
import ru.yandex.qatools.htmlelements.element.CheckBox;
import ru.yandex.qatools.htmlelements.element.Radio;
import ru.yandex.qatools.htmlelements.element.Select;
import ru.yandex.qatools.htmlelements.element.TextInput;
import ru.yandex.qatools.htmlelements.element.TypifiedElement;
import ru.yandex.qatools.htmlelements.matchers.MatcherDecorators;

import static org.hamcrest.Matchers.hasItem;
import static ru.yandex.autotests.irt.testutils.allure.TestStepsEn.assumeThat;
import static ru.yandex.qatools.htmlelements.matchers.MatcherDecorators.should;
import static ru.yandex.qatools.htmlelements.matchers.MatcherDecorators.timeoutHasExpired;

/**
 * @author Roman Kuhta (kuhtich@yandex-team.ru)
 */
public class WebElementsActions {
    public WebElementsActions(WebDriverConfiguration config) {
        this.config = config;
    }

    private WebDriverConfiguration config;

    private JavaScriptActions getJavaScriptActions() {
        return new JavaScriptActions(config);
    }

    /**
     * Returns first visible {@link org.openqa.selenium.WebElement} from a collection
     *
     * @param elements List of elements implements isDisplayed method of {@link org.openqa.selenium.WebElement}
     * @param <T>      type should implement {@link org.openqa.selenium.WebElement} interface
     * @return first visible element
     */
    public static <T> T getVisibleElement(List<T> elements) {
        return getVisibleElement(elements, 5000);
    }

    /**
     * Returns first visible {@link org.openqa.selenium.WebElement} from a collection
     *
     * @param elements List of elements implements isDisplayed method of {@link org.openqa.selenium.WebElement}
     * @param <T>      type should implement {@link org.openqa.selenium.WebElement} interface
     * @return first visible element
     */
    public static <T> T getVisibleElement(List<T> elements, int milliseconds) {
        return getVisibleElements(elements, milliseconds).stream()
                .findFirst()
                .orElseThrow(() -> new DirectWebError("Не найден видимый элемент из " + elements.toString()));
    }

    public static <T> boolean isDisplayed(T element) {
        try {
            Method method = element.getClass().getMethod("isDisplayed");
            method.setAccessible(true);
            return (Boolean) method.invoke(element);
        } catch (Exception e) {
            return false;
        }
    }

    public static <T extends WebElement> boolean isExists(T element) {
        boolean state;
        try {
            state = element.findElements(By.xpath("self::*")).size() > 0;
        } catch (WebDriverException ex) {
            state = false;
        }
        return state;
    }

    public static <T extends TypifiedElement> boolean isExists(T element) {
        return isExists(element.getWrappedElement());
    }


    /**
     * Returns all visible {@link WebElement} elements from a collection
     *
     * @param elements List of {@link WebElement}
     * @param <T>      type should implement {@link WebElement} interface
     * @return all visible elements
     */
    public static <T> List<T> getVisibleElements(List<T> elements) {
        return getVisibleElements(elements, 200);
    }

    /**
     * Returns first element from elements matches with matcher
     * waits 30 seconds for element match
     *
     * @param elements
     * @param matcher
     * @param <T>
     * @return null elements has not items matches with matcher
     */
    public static <T> T getFirstElementMatches(List<T> elements, Matcher<T> matcher, Long timeoutInSeconds) {
        if (!should(hasItem(matcher))
                .whileWaitingUntil(timeoutHasExpired(TimeUnit.SECONDS.toMillis(timeoutInSeconds))).matches(elements))
        {
            return null;
        }
        List<T> filtered = ListUtils.getFilteredList(matcher, elements);
        if (filtered.size() > 0) {
            return filtered.get(0);
        }
        return null;
    }

    public static <T> T getFirstElementMatches(List<T> elements, Matcher<T> matcher) {
        return getFirstElementMatches(elements, matcher, 0l);
    }

    /**
     * Returns all visible {@link WebElement} elements from a collection
     *
     * @param elements              List of {@link WebElement}
     * @param <T>                   type should implement {@link WebElement} interface
     * @param timeoutInMilliseconds time to wait one of the elements visible
     * @return all visible elements
     */
    public static <T> List<T> getVisibleElements(List<T> elements, int timeoutInMilliseconds) {
        List<T> result = new ArrayList<>();
        long intervalInMilliseconds = 150;
        long start = System.currentTimeMillis();
        long end = start + timeoutInMilliseconds;
        while (System.currentTimeMillis() < end) {
            result.addAll(
                    elements.stream()
                            .filter(elem -> isDisplayed(elem))
                            .collect(Collectors.toList()));
            if (result.size() != 0) {
                break;
            }
            try {
                Thread.sleep(intervalInMilliseconds);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        return result;
    }

    public <T extends NumericInput> void setNumericValue(T element, String value) {
        if (value == null || value.equals("null")) {
            return;
        }
        //element.click();
        element.setValue(value);
        getJavaScriptActions().fireChangeEvent(element.getWrappedElement());
    }

    public <T extends NumericInput> void setNumericValue(T element, Number value) {
        setNumericValue(element, String.valueOf(value));
    }

    public static void fillTextInput(TextInput input, String value) {
        if (value == null) {
            return;
        }
        if (value.isEmpty() && !input.getText().isEmpty()) {
            List<WebElement> clearButton = input.getWrappedElement()
                    .findElements(By.xpath("./../span[contains(@class, 'input__clear')]"));
            if (!clearButton.isEmpty()) {
                getVisibleElement(clearButton).click();
                return;
            }
        }
        input.clear();
        input.sendKeys(value);
    }

    public static void fillTextInput(TextAreaBEM input, String value) {
        if (value == null) {
            return;
        }

        //костыль, так как для TextArea нет кнопки ее очистки и не срабатывает событие изменений в попапе
        String inputText = (!value.isEmpty()) ? value : " ";
        fillTextValueByParts(input.getWrappedElement(), inputText);
    }

    private static void fillTextValueByParts(WebElement element, String value) {
        final int MAX_LENGTH = 1000;
        element.clear();
        int i = 0;
        for (int diff = value.length() / MAX_LENGTH; i < diff; i++) {
            element.sendKeys(value.substring(i * MAX_LENGTH, MAX_LENGTH * (i + 1)));
        }
        if (value.length() % MAX_LENGTH != 0) {
            element.sendKeys(value.substring(i * MAX_LENGTH));
        }
    }

    public static void fillHrefWithScheme(TextAreaBEM input, Select select, String link, boolean schemeFirst) {
        fillHrefWithScheme((text) -> fillTextInput(input, text), input, select, link, schemeFirst);
    }

    public static void fillHrefWithScheme(TextInput input, Select select, String link, boolean schemeFirst) {
        fillHrefWithScheme((text) -> fillTextInput(input, text), input, select, link, schemeFirst);
    }

    private static void fillHrefWithScheme(Consumer<String> filler, TypifiedElement element, Select select, String link, boolean schemeFirst) {
        //boolean schemeFirst=true - костыль для РМП, так как оно идет за данными о приложении после заполнения ссылки,
        // то на шаге выставления протокола упадет, поэтому сначала заполняем протокол

        //schemeFirst=false - Все не РМП, работает только в порядке сначала протокол, потом ссылка

        if (link == null) {
            return;
        }
        if (link.isEmpty()) {
            filler.accept(link);
            return;
        }
        try {
            URI uri = new URI(link);
            if (uri.getScheme() == null) {
                throw new DirectWebError("У ссылки отсутствсует протокол: " + link);
            }
            element.getWrappedElement().click();
            if (schemeFirst) {
                select.selectByValue(uri.getScheme() + "://");
                filler.accept(
                        uri.getSchemeSpecificPart().replaceFirst("//", ""));
            } else {
                filler.accept(
                        uri.getSchemeSpecificPart().replaceFirst("//", ""));
                select.selectByValue(uri.getScheme() + "://");
            }
        } catch (URISyntaxException e) {
            throw new DirectWebError("Неверный формат ссылки: " + link);
        }
    }

    public void fillTextInputWithClickAndEvents(TextInput input, String value) {
        if (value == null) {
            return;
        }
        fillTextValueByParts(input.getWrappedElement(), value);
        getJavaScriptActions().fireBlurEvent(input.getWrappedElement());
    }

    public void fireFocusEvent(Button button) {
        getJavaScriptActions().fireFocusEvent(button.getWrappedElement());
    }

    public static <T extends CheckBox> void setCheckboxState(T element, Boolean state) {
        if (state == null) {
            return;
        }
        if (state) {
            element.select();
        } else {
            element.deselect();
        }
    }

    public static <T extends Select> void fillSelect(T element, String value, SelectInputMethod inputMethod) {
        if (value == null) {
            return;
        }
        switch (inputMethod) {
            case BY_TEXT:
                element.selectByVisibleText(value);
                break;
            case BY_VALUE:
                element.selectByValue(value);
                break;
        }
    }

    public static <T extends Select> void fillSelectWithPatrialMatch(T element, String partialValue,
            SelectInputMethod inputMethod)
    {
        if (partialValue == null) {
            return;
        }

        String value;

        for (WebElement option : element.getOptions()) {
            value = getOptionValueWithPartialMatch(option, partialValue, inputMethod);
            if (value != null) {
                fillSelect(element, String.valueOf(value), inputMethod);
                return;
            }
        }
    }

    private static String getOptionValueWithPartialMatch(
            WebElement option, String partialValue, SelectInputMethod inputMethod)
    {
        String value = null;
        switch (inputMethod) {
            case BY_TEXT:
                if (option.getText().contains(partialValue)) {
                    value = option.getText();
                }
                break;
            case BY_VALUE:
                if (option.getAttribute("value").contains(partialValue)) {
                    value = option.getAttribute("value");
                }
                break;
        }
        return value;
    }

    public static <T extends Select> void fillSelect(T element, Integer value, SelectInputMethod inputMethod) {
        if (value == null) {
            return;
        }
        fillSelect(element, String.valueOf(value), inputMethod);
    }

    public <T extends Select> void fillSelectWithEvents(T element, String value, SelectInputMethod inputMethod) {
        fillSelect(element, value, inputMethod);
        getJavaScriptActions().fireBlurEvent(element.getWrappedElement());
        getJavaScriptActions().fireChangeEvent(element.getWrappedElement());
    }

    public static <T extends Radio> void fillRadio(T element, String value, RadioInputMethod inputMethod) {
        if (value == null) {
            return;
        }
        switch (inputMethod) {
            case BY_INDEX:
                element.selectByIndex(Integer.valueOf(value));
                break;
            case BY_VALUE:
                element.selectByValue(value);
                break;
        }
    }

    private Class<?>[] getClasses(Object[] parameters) {
        LinkedList<Class> result = new LinkedList<>();
        for (Object obj : parameters) {
            result.add(obj.getClass());
        }
        return result.toArray(new Class[result.size()]);
    }

    public static void waitVisibility(WebElement webElement) {
        assumeThat("не дождались элемента", webElement, MatcherDecorators.should(
                IsDisplayedMatcher.isDisplayed())
                .whileWaitingUntil(timeoutHasExpired()));
    }
}
