/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements. See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership. The ASF licenses this file
 * to you 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
 *
 * http://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.
 */
package org.apache.deltaspike.core.impl.future;

import org.apache.deltaspike.core.api.future.Futureable;
import org.apache.deltaspike.core.impl.util.AnnotatedMethods;
import org.apache.deltaspike.core.spi.future.FutureableStrategy;
import org.apache.deltaspike.core.util.ExceptionUtils;

import javax.enterprise.context.Dependent;
import javax.enterprise.inject.spi.AnnotatedMethod;
import javax.enterprise.inject.spi.AnnotatedType;
import javax.enterprise.inject.spi.BeanManager;
import javax.inject.Inject;
import javax.interceptor.InvocationContext;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;

@Dependent
public class DefaultFutureableStrategy implements FutureableStrategy
{
    private static final Class<?> COMPLETION_STAGE;
    private static final Class<?> COMPLETABLE_FUTURE;
    private static final Method COMPLETABLE_STAGE_TO_FUTURE;

    // only for weld1
    private static final boolean IS_WELD1;
    private static final ThreadLocal<LinkedList<CallKey>> STACK = new ThreadLocal<LinkedList<CallKey>>()
    {
        @Override
        protected LinkedList<CallKey> initialValue()
        {
            return new LinkedList<CallKey>();
        }
    };

    static
    {
        Class<?> completionStageClass = null;
        Class<?> completableFutureClass = null;
        Method completionStageClassToCompletableFuture = null;
        try
        {
            final ClassLoader classLoader = ClassLoader.getSystemClassLoader();
            completionStageClass = classLoader.loadClass("java.util.concurrent.CompletionStage");
            completionStageClassToCompletableFuture = completionStageClass.getMethod("toCompletableFuture");
            completableFutureClass = classLoader.loadClass("java.util.concurrent.CompletableFuture");
        }
        catch (final Exception e)
        {
            // not on java 8
        }
        COMPLETION_STAGE = completionStageClass;
        COMPLETABLE_FUTURE = completableFutureClass;
        COMPLETABLE_STAGE_TO_FUTURE = completionStageClassToCompletableFuture;

        { // workaround for weld -> use a thread local to track the invocations
            boolean weld1 = false;
            try
            {
                final Class<?> impl = Thread.currentThread().getContextClassLoader()
                        .loadClass("org.jboss.weld.manager.BeanManagerImpl");
                final Package pck = impl.getPackage();
                weld1 = "Weld Implementation".equals(pck.getImplementationTitle())
                        && pck.getSpecificationVersion() != null && pck.getSpecificationVersion().startsWith("1.1.");
            }
            catch (final Throwable cnfe)
            {
                // no-op
            }
            IS_WELD1 = weld1;
        }
    }

    @Inject
    private ThreadPoolManager manager;

    @Inject
    private BeanManager beanManager;

    private transient ConcurrentMap<Method, ExecutorService> configByMethod =
        new ConcurrentHashMap<Method, ExecutorService>();


    @Override
    public Object execute(final InvocationContext ic) throws Exception
    {
        final CallKey invocationKey;
        if (IS_WELD1)
        {
            invocationKey = new CallKey(ic);
            { // weld1 workaround
                final LinkedList<CallKey> stack = STACK.get();
                if (!stack.isEmpty() && stack.getLast().equals(invocationKey))
                {
                    try
                    {
                        return ic.proceed();
                    }
                    finally
                    {
                        if (stack.isEmpty())
                        {
                            STACK.remove();
                        }
                    }
                }
            }
        }
        else
        {
            invocationKey = null;
        }

        // validate usage
        final Class<?> returnType = ic.getMethod().getReturnType();
        if (!Future.class.isAssignableFrom(returnType) &&
                !void.class.isAssignableFrom(returnType) &&
                (COMPLETION_STAGE == null || !COMPLETION_STAGE.isAssignableFrom(returnType)))
        {
            throw new IllegalArgumentException("Return type should be a CompletableStage, Future or Void");
        }

        if (configByMethod == null)
        {
            synchronized (this)
            {
                if (configByMethod == null)
                {
                    configByMethod = new ConcurrentHashMap<Method, ExecutorService>();
                }
            }
        }

        // running < j8 we cant have cancellation
        //final AtomicReference<Callable<?>> cancelHook = new AtomicReference<Callable<?>>();
        final Callable<Object> invocation = new Callable<Object>()
        {
            @Override
            public Object call() throws Exception
            {
                final LinkedList<CallKey> callStack;
                if (IS_WELD1)
                {
                    callStack = STACK.get();
                    callStack.add(invocationKey);
                }
                else
                {
                    callStack = null;
                }
                try
                {
                    final Object proceed = ic.proceed();
                    final Future<?> future = COMPLETION_STAGE == null || !COMPLETION_STAGE.isInstance(proceed) ?
                            Future.class.cast(proceed) :
                            Future.class.cast(COMPLETABLE_STAGE_TO_FUTURE.invoke(proceed));
                    return future.get();
                }
                catch (final InvocationTargetException e)
                {
                    throw ExceptionUtils.throwAsRuntimeException(e.getCause());
                }
                catch (final Exception e)
                {
                    throw ExceptionUtils.throwAsRuntimeException(e);
                }
                finally
                {
                    if (IS_WELD1)
                    {
                        callStack.removeLast();
                        if (callStack.isEmpty())
                        {
                            STACK.remove();
                        }
                    }
                }
            }
        };

        final ExecutorService pool = getOrCreatePool(ic);
        
        if (void.class.isAssignableFrom(returnType))
        {
            pool.submit(invocation);
            return null;
        }
        
        if (COMPLETABLE_FUTURE == null)  // not on java 8 can only be a future
        {
            return pool.submit(invocation);
        }

        // java 8, use CompletableFuture, it impl CompletionStage and Future so everyone is happy
        final Object completableFuture = COMPLETABLE_FUTURE.newInstance();
        pool.submit(new J8PromiseCompanionTask(completableFuture, invocation));
        // TODO: handle cancel
        return completableFuture;
    }

    protected ExecutorService getOrCreatePool(final InvocationContext ic)
    {
        final Method method = ic.getMethod();
        ExecutorService executorService = configByMethod.get(method);
        if (executorService == null)
        {
            final AnnotatedType<?> annotatedType = beanManager.createAnnotatedType(method.getDeclaringClass());
            final AnnotatedMethod<?> annotatedMethod = AnnotatedMethods.findMethod(annotatedType, method);
            final Futureable methodConfig = annotatedMethod.getAnnotation(Futureable.class);
            final ExecutorService instance = manager.find(
                    (methodConfig == null ? annotatedType.getAnnotation(Futureable.class) : methodConfig).value());
            configByMethod.putIfAbsent(method, instance);
            executorService = instance;
        }
        return executorService;
    }

    private static final class CallKey
    {
        private final InvocationContext ic;
        private final int hash;

        private CallKey(final InvocationContext ic)
        {
            this.ic = ic;

            final Object[] parameters = ic.getParameters();
            this.hash = ic.getMethod().hashCode() + (parameters == null ? 0 : Arrays.hashCode(parameters));
        }

        @Override
        public boolean equals(final Object o)
        {
            return this == o || !(o == null || getClass() != o.getClass()) && equals(ic, CallKey.class.cast(o).ic);
        }

        @Override
        public int hashCode()
        {
            return hash;
        }

        private boolean equals(final InvocationContext ic1, final InvocationContext ic2)
        {
            final Object[] parameters1 = ic1.getParameters();
            final Object[] parameters2 = ic2.getParameters();
            return ic2.getMethod().equals(ic1.getMethod()) &&
                    (parameters1 == parameters2 ||
                    (parameters1 != null && parameters2 != null && Arrays.equals(parameters1, ic2.getParameters())));
        }
    }
}
