/*******************************************************************************
 * Copyright (c) 2010, 2015 Sonatype, Inc.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *    Stuart McCulloch (Sonatype, Inc.) - initial API and implementation
 *******************************************************************************/
package org.eclipse.sisu.space;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import javax.inject.Qualifier;

import com.google.inject.Binder;
import com.google.inject.Key;
import com.google.inject.Module;
import com.google.inject.PrivateBinder;
import com.google.inject.spi.Element;
import com.google.inject.spi.Elements;
import com.google.inject.spi.MembersInjectorLookup;
import com.google.inject.spi.PrivateElements;
import com.google.inject.spi.ProviderLookup;

/**
 * Guice {@link Module} that automatically binds types annotated with {@link Qualifier} annotations.
 */
public final class SpaceModule
    implements Module
{
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final String NAMED_INDEX = AbstractSisuIndex.INDEX_FOLDER + AbstractSisuIndex.NAMED;

    public static final ClassFinder LOCAL_INDEX = new IndexedClassFinder( NAMED_INDEX, false );

    public static final ClassFinder GLOBAL_INDEX = new IndexedClassFinder( NAMED_INDEX, true );

    public static final ClassFinder LOCAL_SCAN = SpaceScanner.DEFAULT_FINDER;

    // ----------------------------------------------------------------------
    // Implementation fields
    // ----------------------------------------------------------------------

    private static final class RecordedElements
    {
        static final ConcurrentMap<String, List<Element>> cache = new ConcurrentHashMap<String, List<Element>>();
    }

    private final boolean caching;

    private final ClassSpace space;

    private final ClassFinder finder;

    private Strategy strategy = Strategy.DEFAULT;

    // ----------------------------------------------------------------------
    // Constructors
    // ----------------------------------------------------------------------

    public SpaceModule( final ClassSpace space )
    {
        this( space, BeanScanning.ON );
    }

    public SpaceModule( final ClassSpace space, final ClassFinder finder )
    {
        caching = false;

        this.space = space;
        this.finder = finder;
    }

    public SpaceModule( final ClassSpace space, final BeanScanning scanning )
    {
        caching = BeanScanning.CACHE == scanning;

        this.space = space;
        switch ( scanning )
        {
            case OFF:
                finder = null;
                break;
            case INDEX:
                finder = LOCAL_INDEX;
                break;
            case GLOBAL_INDEX:
                finder = GLOBAL_INDEX;
                break;
            default:
                finder = LOCAL_SCAN;
                break;
        }
    }

    // ----------------------------------------------------------------------
    // Public methods
    // ----------------------------------------------------------------------

    /**
     * Applies a new visitor {@link Strategy} to the current module.
     * 
     * @param _strategy The new strategy
     * @return Updated module
     */
    public Module with( final Strategy _strategy )
    {
        strategy = _strategy;
        return this;
    }

    public void configure( final Binder binder )
    {
        binder.bind( ClassSpace.class ).toInstance( space );

        if ( caching )
        {
            recordAndReplayElements( binder );
        }
        else if ( null != finder )
        {
            scanForElements( binder );
        }
    }

    // ----------------------------------------------------------------------
    // Public types
    // ----------------------------------------------------------------------

    /**
     * Visitor strategy.
     */
    public interface Strategy
    {
        /**
         * Selects the {@link SpaceVisitor} to be used for the given {@link Binder}.
         * 
         * @param binder The binder
         * @return Selected visitor
         */
        SpaceVisitor visitor( Binder binder );

        /**
         * Default visitor strategy; scan and bind implementations with {@link Qualifier}s.
         */
        Strategy DEFAULT = new Strategy()
        {
            public SpaceVisitor visitor( final Binder binder )
            {
                return new QualifiedTypeVisitor( new QualifiedTypeBinder( binder ) );
            }
        };
    }

    // ----------------------------------------------------------------------
    // Implementation methods
    // ----------------------------------------------------------------------

    void scanForElements( final Binder binder )
    {
        new SpaceScanner( space, finder ).accept( strategy.visitor( binder ) );
    }

    private void recordAndReplayElements( final Binder binder )
    {
        final String key = space.toString();
        List<Element> elements = RecordedElements.cache.get( key );
        if ( null == elements )
        {
            // record results of scanning plus any custom module bindings
            final List<Element> recording = Elements.getElements( new Module()
            {
                public void configure( final Binder recorder )
                {
                    scanForElements( recorder );
                }
            } );
            elements = RecordedElements.cache.putIfAbsent( key, recording );
            if ( null == elements )
            {
                // shortcut, no need to reset state first time round
                Elements.getModule( recording ).configure( binder );
                return;
            }
        }

        replayRecordedElements( binder, elements );
    }

    private static void replayRecordedElements( final Binder binder, final List<Element> elements )
    {
        for ( final Element e : elements )
        {
            // lookups have state so we replace them with duplicates when replaying...
            if ( e instanceof ProviderLookup<?> )
            {
                binder.getProvider( ( (ProviderLookup<?>) e ).getKey() );
            }
            else if ( e instanceof MembersInjectorLookup<?> )
            {
                binder.getMembersInjector( ( (MembersInjectorLookup<?>) e ).getType() );
            }
            else if ( e instanceof PrivateElements )
            {
                // Follows example set by Guice Modules when applying private elements:
                final PrivateElements privateElements = (PrivateElements) e;

                // 1. create new private binder, using the elements source token
                final PrivateBinder privateBinder = binder.withSource( e.getSource() ).newPrivateBinder();

                // 2. for all elements, apply each element to the private binder
                replayRecordedElements( privateBinder, privateElements.getElements() );

                // 3. re-expose any exposed keys using their exposed source token
                for ( final Key<?> k : privateElements.getExposedKeys() )
                {
                    privateBinder.withSource( privateElements.getExposedSource( k ) ).expose( k );
                }
            }
            else
            {
                e.applyTo( binder );
            }
        }
    }
}
