/**
 *
 * Copyright (c) 2006-2019, Speedment, Inc. All Rights Reserved.
 *
 * 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:
 *
 * 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 com.speedment.common.injector.internal.dependency;

import com.speedment.common.injector.State;
import com.speedment.common.injector.annotation.Execute;
import com.speedment.common.injector.annotation.ExecuteBefore;
import com.speedment.common.injector.annotation.WithState;
import com.speedment.common.injector.dependency.Dependency;
import com.speedment.common.injector.dependency.DependencyGraph;
import com.speedment.common.injector.dependency.DependencyNode;
import com.speedment.common.injector.exception.CyclicReferenceException;
import com.speedment.common.injector.execution.Execution;
import com.speedment.common.injector.internal.InjectorImpl;
import com.speedment.common.injector.internal.execution.ReflectionExecutionImpl;

import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Stream;

import static com.speedment.common.injector.internal.util.ReflectionUtil.traverseMethods;
import static java.util.stream.Collectors.joining;

/**
 * The default implementation of the {@link DependencyGraph} interface.
 * 
 * @author Emil Forslund
 * @since  1.0.0
 */
public final class DependencyGraphImpl implements DependencyGraph {
    
    private final Map<Class<?>, DependencyNode> nodes;
    
    public static DependencyGraph create(Stream<Class<?>> injectables)
            throws CyclicReferenceException {
        
        final DependencyGraphImpl graph = new DependencyGraphImpl();
        injectables.forEach(graph::getOrCreate);
        graph.inject();
        return graph;
    }
    
    private DependencyGraphImpl() {
        nodes = new ConcurrentHashMap<>();
        nodes.put(InjectorImpl.class, new InjectorDependencyNode());
    }

    @Override
    public DependencyNode get(Class<?> clazz) throws IllegalArgumentException {
        for (final Class<?> impl : nodes.keySet()) {
            if (clazz.isAssignableFrom(impl)) {
                return nodes.get(impl);
            }
        }
        
        throw new IllegalArgumentException(
            "There is no implementation of '" + clazz + 
            "' in the injection dependency graph."
        );
    }

    @Override
    public DependencyNode getOrCreate(Class<?> clazz) {
        return nodes.computeIfAbsent(clazz, DependencyNodeImpl::new);
    }

    @Override
    public DependencyGraph inject() {
        nodes.forEach((clazz, node) -> {
            // Go through all the methods with the '@Execute'-annotation in the 
            // class and add them to the executors set.
            traverseMethods(clazz)
                .filter(m -> m.isAnnotationPresent(Execute.class))
                .forEach(m -> {
                    node.getExecutions().add(createExecution(m, State.STARTED));
                });

            // Go through all the methods with the '@ExecuteBefore'-annotation 
            // in the class  and add them to the executors set.
            traverseMethods(clazz)
                .filter(m -> m.isAnnotationPresent(ExecuteBefore.class))
                .forEach(m -> {
                    final ExecuteBefore execute =
                        m.getAnnotation(ExecuteBefore.class);
                    
                    node.getExecutions()
                        .add(createExecution(m, execute.value()));
                });
        });
        
        return this;
    }

    @Override
    public Stream<DependencyNode> nodes() {
        return nodes.values().stream();
    }
    
    private static String methodName(Method m) {
        return m.getDeclaringClass().getName() + "#" + 
            m.getName() + "(" + 
            Stream.of(m.getParameterTypes())
                .map(Class::getSimpleName)
                .collect(joining(", ")) + 
            ")";
    }
    
    private Execution<?> createExecution(Method m, State executeBefore) {
        final Set<Dependency> dependencies = new HashSet<>();
        try {
            for (int i = 0; i < m.getParameterCount(); i++) {
                final Parameter p   = m.getParameters()[i];
                final WithState ws  = p.getAnnotation(WithState.class);
                
                if (ws != null) {
                    final Class<?> type = p.getType();
                    final State state   = ws.value();

                    try {
                        dependencies.add(
                            new DependencyImpl(get(type), state)
                        );
                    } catch (final CyclicReferenceException ex) {
                        throw new CyclicReferenceException(
                            m.getDeclaringClass(), ex
                        );
                    } catch (final IllegalArgumentException iae) {
                        throw new IllegalStateException(
                            "Unable to resolve " + m.toString() + " (" + type.toString() + ") at state " + state.toString()
                            ,
                            iae
                        );
                    }
                }
            }
        } catch (final CyclicReferenceException ex) {
            throw new IllegalStateException(
                "Could not execute method " + methodName(m) + 
                " since one of its dependencies had not been injected.",
                ex
            );
        }

        return new ReflectionExecutionImpl<>(
            m.getDeclaringClass(), 
            executeBefore, dependencies, m
        );
    }
}
@@ -1482,24 +1482,51 @@
 a.util.Map;%0A
+import java.util.Optional;%0A
 import java.
@@ -5147,24 +5147,455 @@
 mpl(
-get(type), state
+%0A                                Optional.ofNullable(m.getAnnotation(ExecuteBefore.class))%0A                                    .filter(execBef -%3E !execBef.orThrow()) // If orThrow() == false, create the node if missing%0A                                    .isPresent()%0A                                        ? getOrCreate(type)%0A                                        : get(type),%0A                                state%0A                            
 )%0A  

Handle optional dependencies in dependency graph