/**
 *
 * Copyright (c) 2006-2017, 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;

import com.speedment.common.injector.Injector;
import com.speedment.common.injector.InjectorBuilder;
import com.speedment.common.injector.State;
import com.speedment.common.injector.annotation.Inject;
import com.speedment.common.injector.annotation.WithState;
import com.speedment.common.injector.dependency.DependencyGraph;
import com.speedment.common.injector.dependency.DependencyNode;
import com.speedment.common.injector.exception.NotInjectableException;
import com.speedment.common.injector.execution.Execution;
import com.speedment.common.injector.execution.Execution.ClassMapper;
import com.speedment.common.injector.internal.util.InjectorUtil;
import static com.speedment.common.injector.internal.util.InjectorUtil.findIn;
import static com.speedment.common.injector.internal.util.PrintUtil.horizontalLine;
import static com.speedment.common.injector.internal.util.PrintUtil.limit;
import static com.speedment.common.injector.internal.util.PropertiesUtil.configureParams;
import static com.speedment.common.injector.internal.util.ReflectionUtil.traverseFields;
import com.speedment.common.logger.Level;
import com.speedment.common.logger.Logger;
import com.speedment.common.logger.LoggerManager;
import java.lang.reflect.InvocationTargetException;
import java.util.*;
import static java.util.Objects.requireNonNull;
import java.util.concurrent.atomic.AtomicBoolean;
import static java.util.stream.Collectors.toSet;
import java.util.stream.Stream;

/**
 * The default implementation of the {@link Injector} interface.
 *
 * @author Emil Forslund
 * @since  3.0.0
 */
public final class InjectorImpl implements Injector {
    
    /**
     * Create a new {@link InjectorBuilder} using the default implementation and 
     * default {@code ClassLoader}.
     * 
     * @return  the injector builder
     */
    public static InjectorBuilder builder() {
        return new InjectorBuilderImpl();
    }
    
    /**
     * Create a new {@link InjectorBuilder} using the default implementation but 
     * with a specific {@code ClassLoader}.
     * 
     * @param classLoader  the class loader to use
     * @return             the injector builder
     */
    public static InjectorBuilder builder(ClassLoader classLoader) {
        return new InjectorBuilderImpl(classLoader);
    }

    public final static Logger LOGGER = 
        LoggerManager.getLogger(InjectorImpl.class);

    private final Set<Class<?>> injectables;
    private final List<Object> instances;
    private final Properties properties;
    private final ClassLoader classLoader;
    private final DependencyGraph graph;
    private final InjectorBuilder builder;

    InjectorImpl(
            Set<Class<?>> injectables, 
            List<Object> instances,
            Properties properties,
            ClassLoader classLoader,
            DependencyGraph graph,
            InjectorBuilder builder) {
        
        this.injectables = requireNonNull(injectables);
        this.instances   = requireNonNull(instances);
        this.properties  = requireNonNull(properties);
        this.classLoader = requireNonNull(classLoader);
        this.graph       = requireNonNull(graph);
        this.builder     = requireNonNull(builder);
    }

    @Override
    public <T> Stream<T> stream(Class<T> type) {
        return findAll(type);
    }

    @Override
    public <T> T getOrThrow(Class<T> type) throws IllegalArgumentException {
        return find(type, true);
    }

    @Override
    public <T> Optional<T> get(Class<T> type) {
        return Optional.ofNullable(find(type, false));
    }

    @Override
    public Stream<Class<?>> injectables() {
        return injectables.stream();
    }

    @Override
    public <T> T inject(T instance) {
        injectFields(instance);
        configureParams(instance, properties);
        return instance;
    }

    @Override
    public ClassLoader classLoader() {
        return classLoader;
    }

    @Override
    public void stop() {
        final AtomicBoolean hasAnythingChanged = new AtomicBoolean();
        
        // Create ClassMapper
        final ClassMapper classMapper = new ClassMapper() {
            @Override
            public <T> T apply(Class<T> type) throws NotInjectableException {
                return find(type, true);
            }
        };

        // Loop until all nodes have been started.
        Set<DependencyNode> unfinished;
        while (!(unfinished = graph.nodes()
            .filter(n -> n.getCurrentState() != State.STOPPED)
            .collect(toSet())).isEmpty()) {

            hasAnythingChanged.set(false);

            unfinished.forEach(n -> {

                // Check if all its dependencies have been satisfied.
                // TODO: Dependencies should be resolved in the opposite order 
                // when stopping.
                if (n.canBe(State.STOPPED)) {

                    LOGGER.debug(horizontalLine());

                    // Retreive the instance for that node
                    final Object inst = find(n.getRepresentedType(), true);

                    // Execute all the executions for the next step.
                    n.getExecutions().stream()
                        .filter(e -> e.getState() == State.STOPPED)
                        .map(exec -> {
                            @SuppressWarnings("unchecked")
                            final Execution<Object> casted = 
                                (Execution<Object>) exec;
                            return casted;
                        })
                        .forEach(exec -> {

                            // We might want to log exactly which steps we
                            // have completed.
                            if (LOGGER.getLevel()
                                .isEqualOrLowerThan(Level.DEBUG)) {

                                LOGGER.debug(
                                    "| -> %-76s |", 
                                    limit(exec.toString(), 76)
                                );
                            }

                            try {
                                exec.invoke(inst, classMapper);
                            } catch (final IllegalAccessException 
                                         | IllegalArgumentException 
                                         | InvocationTargetException ex) {

                                throw new RuntimeException(ex);
                            }
                        });

                    // Update its state to the new state.
                    n.setState(State.STOPPED);
                    hasAnythingChanged.set(true);

                    LOGGER.debug(
                        "| %-66s %12s |",
                        n.getRepresentedType().getSimpleName(),
                        State.STOPPED.name()
                    );
                }
            });

            if (!hasAnythingChanged.get()) {
                throw new IllegalStateException(
                    "Injector appears to be stuck in an infinite loop. The " + 
                    "following componenets have not been stopped: " + 
                    unfinished.stream()
                        .map(DependencyNode::getRepresentedType)
                        .map(Class::getSimpleName)
                        .collect(toSet())
                );
            }
        }
        
        LOGGER.debug(horizontalLine());
        LOGGER.debug(
            "| %-79s |",
            "All " + instances.size() + " components have been stopped!"
        );
        LOGGER.debug(horizontalLine());
    }

    @Override
    public InjectorBuilder newBuilder() {
        return builder;
    }
    
    private <T> Stream<T> findAll(Class<T> type) {
        return InjectorUtil.findAll(type, this, instances);
    }

    private <T> T find(Class<T> type, boolean required) {
        return findIn(type, this, instances, required);
    }
  
    private <T> void injectFields(T instance) {
        requireNonNull(instance);
        
        traverseFields(instance.getClass())
            .filter(f -> f.isAnnotationPresent(Inject.class))
            .distinct()
            .forEachOrdered(field -> {
                final Object value;

                if (Injector.class.isAssignableFrom(field.getType())) {
                    value = this;
                } else {
                    value = find(
                        field.getType(), 
                        field.getAnnotation(WithState.class) != null
                    );
                }

                field.setAccessible(true);

                try {
                    field.set(instance, value);
                } catch (final IllegalAccessException ex) {
                    final String err = "Could not access field '" + 
                        field.getName() +
                        "' in class '" + value.getClass().getName() +
                        "' of type '" + field.getType() + "'.";
                    LOGGER.error(ex, err);
                    throw new RuntimeException(err, ex);
                }
            });
    }
}

@@ -7752,17 +7752,16 @@
 componen
-e
 ts have 
@@ -7777,25 +7777,24 @@
 stopped: %22 +
- 
 %0A           

Fix typo