/*******************************************************************************
 * 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.inject;

import java.lang.annotation.Annotation;

import javax.inject.Provider;

import org.eclipse.sisu.Description;
import org.eclipse.sisu.Priority;

import com.google.inject.Binding;
import com.google.inject.spi.BindingTargetVisitor;
import com.google.inject.spi.ConstructorBinding;
import com.google.inject.spi.DefaultBindingTargetVisitor;
import com.google.inject.spi.ExposedBinding;
import com.google.inject.spi.InstanceBinding;
import com.google.inject.spi.LinkedKeyBinding;
import com.google.inject.spi.ProviderInstanceBinding;
import com.google.inject.spi.UntargettedBinding;

/**
 * Utility methods for discovering the implementations behind Guice bindings.
 */
final class Implementations
{
    // ----------------------------------------------------------------------
    // Static initialization
    // ----------------------------------------------------------------------

    static
    {
        boolean hasGuiceServlet;
        try
        {
            hasGuiceServlet = BindingTargetVisitor.class.isInstance( ServletFinder.THIS );
        }
        catch ( final LinkageError e )
        {
            hasGuiceServlet = false;
        }
        HAS_GUICE_SERVLET = hasGuiceServlet;

        boolean hasJsr250Priority;
        try
        {
            hasJsr250Priority = javax.annotation.Priority.class.isAnnotation();
        }
        catch ( final LinkageError e )
        {
            hasJsr250Priority = false;
        }
        HAS_JSR250_PRIORITY = hasJsr250Priority;
    }

    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final boolean HAS_GUICE_SERVLET;

    private static final boolean HAS_JSR250_PRIORITY;

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

    private Implementations()
    {
        // static utility class, not allowed to create instances
    }

    // ----------------------------------------------------------------------
    // Utility methods
    // ----------------------------------------------------------------------

    /**
     * Attempts to find the implementation behind the given {@link Binding}.
     * 
     * @param binding The binding
     * @return Implementation class behind the binding; {@code null} if it couldn't be found
     */
    public static Class<?> find( final Binding<?> binding )
    {
        return binding.acceptTargetVisitor( ClassFinder.THIS );
    }

    /**
     * Attempts to find an annotation on the implementation behind the given {@link Binding}.
     * 
     * @param binding The binding
     * @param annotationType The annotation type
     * @return Annotation on the bound implementation; {@code null} if it couldn't be found
     */
    public static <T extends Annotation> T getAnnotation( final Binding<?> binding, final Class<T> annotationType )
    {
        final boolean isPriority = Priority.class.equals( annotationType );

        // peek behind servlet/filter extension bindings when checking priority, so we can order them by rank
        final Class<?> implementation =
            binding.acceptTargetVisitor( HAS_GUICE_SERVLET && isPriority ? ServletFinder.THIS : ClassFinder.THIS );

        T annotation = null;
        if ( null != implementation )
        {
            annotation = implementation.getAnnotation( annotationType );
            if ( null == annotation )
            {
                if ( HAS_JSR250_PRIORITY && isPriority )
                {
                    annotation = adaptJsr250( binding, implementation );
                }
                else if ( Description.class.equals( annotationType ) )
                {
                    annotation = adaptLegacy( binding, implementation );
                }
            }
        }
        return annotation;
    }

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

    @SuppressWarnings( "unchecked" )
    private static <T extends Annotation> T adaptJsr250( final Binding<?> binding, final Class<?> clazz )
    {
        final javax.annotation.Priority jsr250 = clazz.getAnnotation( javax.annotation.Priority.class );
        return null != jsr250 ? (T) new PrioritySource( binding.getSource(), jsr250.value() ) : null;
    }

    @SuppressWarnings( { "unchecked", "deprecation" } )
    private static <T extends Annotation> T adaptLegacy( final Binding<?> binding, final Class<?> clazz )
    {
        final org.sonatype.inject.Description legacy = clazz.getAnnotation( org.sonatype.inject.Description.class );
        return null != legacy ? (T) new DescriptionSource( binding.getSource(), legacy.value() ) : null;
    }

    // ----------------------------------------------------------------------
    // Implementation types
    // ----------------------------------------------------------------------

    /**
     * {@link BindingTargetVisitor} that attempts to find the implementations behind bindings.
     */
    static class ClassFinder
        extends DefaultBindingTargetVisitor<Object, Class<?>>
    {
        // ----------------------------------------------------------------------
        // Constants
        // ----------------------------------------------------------------------

        static final BindingTargetVisitor<Object, Class<?>> THIS = new ClassFinder();

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

        @Override
        public Class<?> visit( final UntargettedBinding<?> binding )
        {
            return binding.getKey().getTypeLiteral().getRawType();
        }

        @Override
        public Class<?> visit( final LinkedKeyBinding<?> binding )
        {
            // this assumes only one level of indirection: api-->impl
            return binding.getLinkedKey().getTypeLiteral().getRawType();
        }

        @Override
        public Class<?> visit( final ConstructorBinding<?> binding )
        {
            return binding.getConstructor().getDeclaringType().getRawType();
        }

        @Override
        public Class<?> visit( final InstanceBinding<?> binding )
        {
            return binding.getInstance().getClass();
        }

        @Override
        public Class<?> visit( final ProviderInstanceBinding<?> binding )
        {
            final Provider<?> provider = Guice4.getProviderInstance( binding );
            if ( provider instanceof DeferredProvider<?> )
            {
                try
                {
                    // deferred providers let us peek at the underlying implementation type
                    return ( (DeferredProvider<?>) provider ).getImplementationClass().load();
                }
                catch ( final TypeNotPresentException e ) // NOPMD
                {
                    // fall-through
                }
            }
            return null;
        }

        @Override
        public Class<?> visit( final ExposedBinding<?> binding )
        {
            return binding.getPrivateElements().getInjector().getBinding( binding.getKey() ).acceptTargetVisitor( this );
        }
    }

    /**
     * {@link ClassFinder} that can also peek behind servlet/filter bindings.
     */
    static final class ServletFinder
        extends ClassFinder
        implements com.google.inject.servlet.ServletModuleTargetVisitor<Object, Class<?>>
    {
        // ----------------------------------------------------------------------
        // Constants
        // ----------------------------------------------------------------------

        @SuppressWarnings( "hiding" )
        static final BindingTargetVisitor<Object, Class<?>> THIS = new ServletFinder();

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

        public Class<?> visit( final com.google.inject.servlet.InstanceFilterBinding binding )
        {
            return binding.getFilterInstance().getClass();
        }

        public Class<?> visit( final com.google.inject.servlet.InstanceServletBinding binding )
        {
            return binding.getServletInstance().getClass();
        }

        public Class<?> visit( final com.google.inject.servlet.LinkedFilterBinding binding )
        {
            // this assumes only one level of indirection: api-->impl
            return binding.getLinkedKey().getTypeLiteral().getRawType();
        }

        public Class<?> visit( final com.google.inject.servlet.LinkedServletBinding binding )
        {
            // this assumes only one level of indirection: api-->impl
            return binding.getLinkedKey().getTypeLiteral().getRawType();
        }
    }
}
