package cz.moneta.test.harness.support.mobile.ios;

import cz.moneta.test.harness.endpoints.MobileEndpoint;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.mobile.MobileLookup;
import cz.moneta.test.harness.support.mobile.share.AnnotationsBuilder;
import cz.moneta.test.harness.support.mobile.share.Direction;
import cz.moneta.test.harness.support.mobile.share.MobileFlow;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;

import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.Optional;
import java.util.stream.IntStream;
import java.util.stream.Stream;

import static cz.moneta.test.harness.support.reflect.ReflectionUtils.getInterfaces;

public class IosBuilders extends AnnotationsBuilder {

    private IosBuilders() {
    }

    private static void doIosWait(MobileEndpoint mobileEndpoint, Method method, Object[] args) {
        Arrays.stream(method.getAnnotationsByType(IosWait.class))
                .forEach(w -> doIosWait(w, mobileEndpoint, args));
    }

    private static void doIosWait(IosWait iosWait, MobileEndpoint mobileEndpoint) {
        doIosWait(iosWait, mobileEndpoint, null);
    }

    private static void doIosWait(IosWait iosWait, MobileEndpoint mobileEndpoint, Object[] args) {
        doWait(iosWait.value(), iosWait.waitSecondsForElement(), iosWait.by(), iosWait.until(), iosWait.explicitWaitSeconds(), iosWait.isStringDynamicXpath(), mobileEndpoint, args);
    }

    private static void doIosClick(MobileEndpoint endpoint, Method method, Object[] args) {
        Arrays.stream(method.getAnnotationsByType(IosClick.class))
                .filter(click -> StringUtils.isNotBlank(click.value()))
                .forEach(click -> {
                    endpoint.click(() -> constructDynamicElement(click.value(), click.isStringDynamicXpath(), args), click.by());
                    doIosWait(click.andWait(), endpoint, args);
                });
    }

    private static void doIosScroll(Method method, MobileEndpoint mobileEndpoint) {
        Arrays.stream(method.getAnnotationsByType(IosScroll.class))
                .forEach(scroll -> {
                    doScroll(scroll.direction(), scroll, scroll.untilElementPresent(), scroll.by(), mobileEndpoint);
                    if (scroll.andClick()) {
                        mobileEndpoint.click(scroll::untilElementPresent, scroll.by());
                    }
                });

    }

    private static void doIosCheckElementContent(MobileEndpoint endpoint, Method method, Object[] args) {
        IosCheckElementContent[] elementContents = method.getAnnotationsByType(IosCheckElementContent.class);
        Arrays.stream(elementContents)
                .forEach(el -> doCheckElementContent(elementContents, el.value(), el.by(), endpoint, method, args));
    }

    private static void doIosType(MobileEndpoint endpoint, Method method, Object[] args) {
        IosTypeInto[] annotations = method.getAnnotationsByType(IosTypeInto.class);
        checkAnnotationLength(annotations, method, args);
        IntStream.range(0, annotations.length)
                .mapToObj(i -> Pair.of(annotations[i], String.valueOf(args[i])))
                .filter(p -> StringUtils.isNotBlank(p.getLeft().value()))
                .forEach(p -> {
                    endpoint.type(p.getLeft()::value, p.getRight(), p.getLeft().clear(), p.getLeft().by());
                    if (p.getLeft().hideKeyboardAfterType()) {
                        endpoint.getIos().hideIosKeyboard(p.getLeft().hideButton());
                    }
                    doIosWait(p.getLeft().andWait(), endpoint, args);
                });
    }

    public static Optional<Object> doIosGetElementContent(MobileEndpoint mobileEndpoint, Method method) {
        return Arrays.stream(method.getAnnotationsByType(IosGetElementContent.class))
                .filter(type -> StringUtils.isNotBlank(type.value()))
                .findFirst()
                .map(type -> Optional.ofNullable(mobileEndpoint.getText(type.value(), type.by()))
                        .filter(StringUtils::isNotBlank)
                        .orElseThrow(() -> new HarnessException("Element '" + type.value() + "' content could not be read")));
    }

    private static void doIosSelect(MobileEndpoint endpoint, Method method, Object... args) {
        IosSelect[] annotations = method.getAnnotationsByType(IosSelect.class);
        if (annotations.length == 0) {
            return;
        }
        checkAnnotationLength(annotations, method, args);
        String value = getSelectValue(args[0]);

        IntStream.range(0, annotations.length)
                .mapToObj(i -> Pair.of(annotations[i], args[i]))
                .filter(IosBuilders::hasSelectValue)
                .forEach(p -> {
                    endpoint.click(() -> p.getLeft().value(), p.getLeft().by());
                    iosScroll(endpoint, p.getLeft(), value);
                    endpoint.click(() -> value, MobileLookup.IOS_NAME);
                });
    }

    //TODO: the same with androidCheckElementPresent, move to AnnotationsBuilder
    private static void doIosCheckElementPresent(MobileEndpoint endpoint, Method method, Object[] args) {
        IosCheckElementPresent[] annotations = method.getAnnotationsByType(IosCheckElementPresent.class);
        Arrays.stream(annotations)
                .filter(check -> StringUtils.isNotBlank(check.value()))
                .forEach(check -> {
                    try {
                        if (check.isStringDynamicXpath()) {
                            IntStream.range(0, args.length)
                                    .mapToObj(i -> Pair.of(annotations[i], args[i]))
                                    .forEach(elementPresentObjectPair -> endpoint.checkElementPresent(constructDynamicElement(elementPresentObjectPair.getLeft().value(), elementPresentObjectPair.getLeft().isStringDynamicXpath(), new Object[]{elementPresentObjectPair.getRight()}), elementPresentObjectPair.getLeft().by()));
                        } else {
                            endpoint.checkElementPresent(constructDynamicElement(check.value(), check.isStringDynamicXpath(), args), check.by());
                        }
                    } catch (ArrayIndexOutOfBoundsException | NullPointerException e) {
                        throw new IllegalStateException(method.getDeclaringClass().getSimpleName() + "#" + method.getName() +
                                " The number of arguments passed to a IosCheckElementPresent-annotated method with isDynamicXpath = true parameter "
                                + "must match the annotation count!", e);
                    }
                });
    }

    private static void iosScroll(MobileEndpoint endpoint, IosSelect iosSelect, String value) {
        Stream.<Optional<Runnable>>of(
                Optional.of(iosSelect.scrollToValue())
                        .filter(Direction.DOWN::equals)
                        .map(direction -> () -> scrollDownWithJS(endpoint, iosSelect, value)),
                Optional.of(iosSelect.scrollToValue())
                        .filter(Direction.UP::equals)
                        .map(direction -> () -> scrollUpWithJS(endpoint, iosSelect, value)))
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .ifPresent(Runnable::run);
    }

    private static void scrollDownWithJS(MobileEndpoint endpoint, IosSelect iosSelect, String value) {
        if (iosSelect.jsScroll()) {
            endpoint.getIos().executeScrollScript(value, "up");
        } else {
            endpoint.getIos().scrollDownUntil(value, MobileLookup.IOS_NAME);
        }
    }

    private static void scrollUpWithJS(MobileEndpoint endpoint, IosSelect iosSelect, String value) {
        if (iosSelect.jsScroll()) {
            endpoint.getIos().executeScrollScript(value, "down");
        } else {
            endpoint.getIos().scrollUpUntil(value, MobileLookup.IOS_NAME);
        }
    }

    private static boolean hasSelectValue(Pair<IosSelect, Object> pair) {
        return StringUtils.isNotBlank(pair.getLeft().value()) && pair.getRight() != null;
    }

    public static void iosBuilderOperations(MobileEndpoint endpoint, Object proxy, Method method, Object[] args) {
        doIosWait(endpoint, method, args);
        doIosScroll(method, endpoint);
        doIosClick(endpoint, method, args);
        doIosType(endpoint, method, args);
        doIosCheckElementContent(endpoint, method, args);
        doIosCheckElementPresent(endpoint, method, args);
        doIosSelect(endpoint, method, args);
        doCustomAction(proxy, method, args);
    }

    public static <A extends MobileFlow<A>> void processClassAnnotations(A builder, MobileEndpoint endpoint) {
        getInterfaces(builder.getClass()).stream()
                .flatMap(i -> Arrays.stream(i.getAnnotationsByType(IosWait.class)))
                .forEach(w -> doIosWait(w, endpoint));
    }
}