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

import cz.moneta.test.harness.context.StoreAccessor;
import cz.moneta.test.harness.endpoints.MobileEndpoint;
import cz.moneta.test.harness.endpoints.smartbanka.MobilePlatform;
import cz.moneta.test.harness.exception.HarnessException;
import cz.moneta.test.harness.support.mobile.android.AndroidBuilders;
import cz.moneta.test.harness.support.mobile.ios.IosBuilders;
import cz.moneta.test.harness.support.reflect.ReflectionUtils;
import cz.moneta.test.harness.support.web.Builder;
import org.openqa.selenium.WebDriverException;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

@SuppressWarnings("unchecked")
public class Builders {

    private Builders() {
    }

    public static <A extends MobileFlow<A>> A newMobileFlowBuilder(Class<A> pageFlow, MobileEndpoint endpoint, StoreAccessor harness) {
        A builder = (A) Proxy.newProxyInstance(
                Thread.currentThread().getContextClassLoader(),
                new Class[]{pageFlow},
                getInvocationHandler(endpoint, harness));
        processClassAnnotations(builder, harness, endpoint);
        return builder;
    }

    private static InvocationHandler getInvocationHandler(MobileEndpoint endpoint, StoreAccessor harness) {
        return (proxy, method, args) -> {
            if (StoreAccessor.class.equals(method.getDeclaringClass())) {
                return method.invoke(harness, args);
            }
            try {
                return doTerminalOperations(endpoint, proxy, harness, method, args)
                        .orElseGet(builderOperations(endpoint, harness, proxy, method, args));
            } catch (WebDriverException e) {
                throw new HarnessException(
                        String.format("Error invoking method '%s#%s'. Error message: %s",
                                method.getDeclaringClass().getSimpleName(),
                                method.getName(),
                                e.getMessage()), e);
            }
        };
    }

    private static Supplier<Object> builderOperations(MobileEndpoint endpoint, StoreAccessor harness, Object proxy, Method method, Object[] args) {
        return () -> {
            checkAnnotationType(endpoint, harness, proxy, method, args);
            return resolveView(endpoint, proxy, method, harness);
        };
    }

    private static void checkAnnotationType(MobileEndpoint endpoint, StoreAccessor harness, Object proxy, Method method, Object[] args) {
        if (MobilePlatform.isAndroid(harness)) {
            AndroidBuilders.androidBuilderOperations(endpoint, method, proxy, args);
        } else {
            IosBuilders.iosBuilderOperations(endpoint, proxy, method, args);
        }
    }

    private static <A extends MobileFlow<A>> void processClassAnnotations(A builder, StoreAccessor harness, MobileEndpoint endpoint) {
        if (MobilePlatform.isAndroid(harness)) {
            AndroidBuilders.processClassAnnotations(builder, endpoint);
        } else {
            IosBuilders.processClassAnnotations(builder, endpoint);
        }
    }

    private static Optional<Object> doTerminalOperations(MobileEndpoint endpoint, Object proxy, StoreAccessor store, Method method, Object[] args) {
        return Stream
                .<Supplier<Optional<Object>>>of(
                        () -> handleThen(proxy, method, args),
                        () -> doGetElementContent(endpoint, method, store))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst();
    }

    private static Optional<Object> doGetElementContent(MobileEndpoint endpoint, Method method, StoreAccessor store) {
        if (MobilePlatform.isAndroid(store)) {
            return AndroidBuilders.doAndroidGetElementContent(endpoint, method);
        }
        return IosBuilders.doIosGetElementContent(endpoint, method);
    }

    private static Optional<Object> handleThen(Object proxy, Method method, Object[] args) {
        return Arrays.stream(Builder.class.getMethods())
                .filter(m -> "then".equals(m.getName()))
                .filter(method::equals)
                .findFirst()
                .map(m -> args[0])
                .filter(Function.class::isInstance)
                .map(Function.class::cast)
                .map(f -> f.apply(proxy));
    }

    private static <A extends MobileFlow<A>> A resolveView(MobileEndpoint endpoint, Object proxy, Method method, StoreAccessor harness) {
        return Stream.<Supplier<Optional<A>>>of(
                () -> handleView(endpoint, method, harness),
                () -> handleReturnType(endpoint, proxy, method, harness),
                () -> Optional.of((A) proxy))
                .map(Supplier::get)
                .filter(Optional::isPresent)
                .map(Optional::get)
                .findFirst()
                .get();
    }

    protected static <A extends MobileFlow<A>> Optional<A> handleView(MobileEndpoint endpoint, Method method,
                                                                      StoreAccessor harness) {
        return Arrays.stream(method.getAnnotationsByType(View.class))
                .findFirst()
                .map(View::value)
                .map(view -> newMobileFlowBuilder((Class<A>) view, endpoint, harness));
    }

    protected static <A extends MobileFlow<A>> Optional<A> handleReturnType(MobileEndpoint endpoint, Object proxy,
                                                                            Method method, StoreAccessor harness) {
        return Optional.of(method.getReturnType())
                .filter(rt -> !rt.isInstance(proxy))
                .filter(MobileFlow.class::isAssignableFrom)
                .map(page -> newMobileFlowBuilder((Class<A>) page, endpoint, harness));
    }

    protected static <T> T invokeDefaultMethod(Object proxy, Method method, Object... args) {
        return ReflectionUtils.invokeDefaultMethod(
                proxy,
                method,
                msg -> String.format("Error invoking method '%s#%s'. Error message: %s",
                        method.getDeclaringClass().getSimpleName(),
                        method.getName(),
                        msg),
                args);
    }
}