/*
 * Copyright 2012 Netflix, Inc.
 *
 *    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.netflix.governator.guice;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.collect.Sets;
import com.google.inject.Binder;
import com.google.inject.ConfigurationException;
import com.google.inject.Module;
import com.google.inject.TypeLiteral;
import com.google.inject.matcher.Matchers;
import com.google.inject.spi.Dependency;
import com.google.inject.spi.InjectionListener;
import com.google.inject.spi.InjectionPoint;
import com.google.inject.spi.TypeEncounter;
import com.google.inject.spi.TypeListener;
import com.netflix.governator.annotations.WarmUp;
import com.netflix.governator.lifecycle.LifecycleListener;
import com.netflix.governator.lifecycle.LifecycleManager;
import com.netflix.governator.lifecycle.LifecycleMethods;
import com.netflix.governator.lifecycle.warmup.DAGManager;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicReference;

class InternalLifecycleModule implements Module
{
    private final Set<Dependency<?>> seen = new HashSet<Dependency<?>>();

    private final LoadingCache<Class<?>, LifecycleMethods> lifecycleMethods = CacheBuilder
        .newBuilder()
        .softValues()
        .build
            (
                new CacheLoader<Class<?>, LifecycleMethods>()
                {
                    @Override
                    public LifecycleMethods load(Class<?> key) throws Exception
                    {
                        return new LifecycleMethods(key);
                    }
                }
            );
    private final AtomicReference<LifecycleManager> lifecycleManager;

    InternalLifecycleModule(AtomicReference<LifecycleManager> lifecycleManager)
    {
        this.lifecycleManager = lifecycleManager;
    }

    @Override
    public void configure(final Binder binder)
    {
        binder.bindListener
        (
            Matchers.any(),
            new TypeListener()
            {
                @Override
                public <T> void hear(final TypeLiteral<T> type, TypeEncounter<T> encounter)
                {
                    encounter.register
                    (
                        new InjectionListener<T>()
                        {
                            @Override
                            public void afterInjection(T obj)
                            {
                                processInjectedObject(obj, type);
                            }
                        }
                    );
                }
            }
        );
    }

    private <T> void processInjectedObject(T obj, TypeLiteral<T> type)
    {
        LifecycleManager manager = lifecycleManager.get();
        if ( manager != null )
        {
            for ( LifecycleListener listener : manager.getListeners() )
            {
                listener.objectInjected(type, obj);
            }

            Class<?> clazz = obj.getClass();
            LifecycleMethods methods = getLifecycleMethods(clazz);

            if ( warmUpIsInDag(clazz, type) )
            {
                addDependencies(manager, obj, type, methods);
            }

            if ( methods.hasLifecycleAnnotations() )
            {
                try
                {
                    manager.add(obj, methods);
                }
                catch ( Exception e )
                {
                    throw new Error(e);
                }
            }
        }
    }

    private void addDependencies(LifecycleManager manager, Object obj, TypeLiteral<?> type, LifecycleMethods methods)
    {
        DAGManager              dagManager = manager.getDAGManager();
        dagManager.addObjectMapping(type, obj, methods);

        applyInjectionPoint(getConstructorInjectionPoint(type), dagManager, type);
        for ( InjectionPoint injectionPoint : getMethodInjectionPoints(type) )
        {
            applyInjectionPoint(injectionPoint, dagManager, type);
        }
    }

    private boolean warmUpIsInDag(Class<?> clazz, TypeLiteral<?> type)
    {
        LifecycleMethods methods = getLifecycleMethods(clazz);
        if ( methods.methodsFor(WarmUp.class).size() > 0 )
        {
            return true;
        }

        if ( warmUpIsInDag(getConstructorInjectionPoint(type)) )
        {
            return true;
        }

        for ( InjectionPoint injectionPoint : getMethodInjectionPoints(type) )
        {
            if ( warmUpIsInDag(injectionPoint) )
            {
                return true;
            }
        }

        return false;
    }

    private boolean warmUpIsInDag(InjectionPoint injectionPoint)
    {
        if ( injectionPoint == null )
        {
            return false;
        }

        List<Dependency<?>> dependencies = injectionPoint.getDependencies();
        for ( Dependency<?> dependency : dependencies )
        {
            if ( !seen.add(dependency) )
            {
                continue;
            }

            if ( warmUpIsInDag(dependency.getKey().getTypeLiteral().getRawType(), dependency.getKey().getTypeLiteral()) )
            {
                return true;
            }
        }
        return false;
    }

    private Set<InjectionPoint> getMethodInjectionPoints(TypeLiteral<?> type)
    {
        try
        {
            return InjectionPoint.forInstanceMethodsAndFields(type);
        }
        catch ( NullPointerException e )
        {
            // ignore - unfortunately this is happening inside of Guice
        }
        catch ( ConfigurationException e )
        {
            // ignore
        }
        return Sets.newHashSet();
    }

    private InjectionPoint getConstructorInjectionPoint(TypeLiteral<?> type)
    {
        try
        {
            return InjectionPoint.forConstructorOf(type);
        }
        catch ( ConfigurationException e )
        {
            // ignore
        }
        return null;
    }

    private void applyInjectionPoint(InjectionPoint injectionPoint, DAGManager dagManager, TypeLiteral<?> type)
    {
        if ( injectionPoint != null )
        {
            List<Dependency<?>> dependencies = injectionPoint.getDependencies();
            for ( Dependency<?> dependency : dependencies )
            {
                dagManager.addDependency(type, dependency.getKey().getTypeLiteral());
            }
        }
    }

    private LifecycleMethods getLifecycleMethods(Class<?> clazz)
    {
        try
        {
            return lifecycleMethods.get(clazz);
        }
        catch ( ExecutionException e )
        {
            throw new RuntimeException(e);
        }
    }
}
@@ -1553,34 +1553,8 @@
 til.
-HashSet;%0Aimport java.util.
 List
@@ -1575,16 +1575,63 @@
 util.Set
+;%0Aimport java.util.concurrent.ConcurrentHashMap
 ;%0Aimport
@@ -1783,25 +1783,165 @@
     
-private final Set
+// this really serves as a Set purpose.%0A    // put dummy boolean as Map value.%0A    // value is really not important here.%0A    private final ConcurrentHashMap
 %3CDep
@@ -1950,16 +1950,25 @@
 dency%3C?%3E
+, Boolean
 %3E seen =
@@ -1972,23 +1972,33 @@
 n = new 
+Concurrent
 Hash
-Set
+Map
 %3CDepende
@@ -2003,16 +2003,25 @@
 dency%3C?%3E
+, Boolean
 %3E();%0A%0A  
@@ -5807,34 +5807,96 @@
     
-if ( !seen.add(dependency)
+Boolean prev = seen.putIfAbsent(dependency, Boolean.TRUE);%0A            if ( prev != null
  )%0A 

change from HashSet to ConcurrentHashMap for thread safety