/*******************************************************************************
 * 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.io.InputStream;
import java.lang.annotation.Annotation;
import java.net.URL;
import java.util.Enumeration;

import org.eclipse.sisu.inject.Logs;
import org.eclipse.sisu.space.asm.ClassReader;
import org.eclipse.sisu.space.asm.Opcodes;
import org.eclipse.sisu.space.asm.Type;

/**
 * Makes a {@link SpaceVisitor} visit a {@link ClassSpace}; can be directed by an optional {@link ClassFinder}.
 */
public final class SpaceScanner
{
    // ----------------------------------------------------------------------
    // Constants
    // ----------------------------------------------------------------------

    private static final int ASM_FLAGS = ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;

    static final ClassFinder DEFAULT_FINDER = new DefaultClassFinder();

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

    private final ClassSpace space;

    private final ClassFinder finder;

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

    public SpaceScanner( final ClassSpace space, final ClassFinder finder )
    {
        this.space = space;
        this.finder = finder;
    }

    public SpaceScanner( final ClassSpace space )
    {
        this( space, DEFAULT_FINDER );
    }

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

    /**
     * Makes the given {@link SpaceVisitor} visit the {@link ClassSpace} of this scanner.
     * 
     * @param visitor The class space visitor
     */
    public void accept( final SpaceVisitor visitor )
    {
        visitor.enterSpace( space );

        for ( final Enumeration<URL> result = finder.findClasses( space ); result.hasMoreElements(); )
        {
            final URL url = result.nextElement();
            final ClassVisitor cv = visitor.visitClass( url );
            if ( null != cv )
            {
                accept( cv, url );
            }
        }

        visitor.leaveSpace();
    }

    /**
     * Makes the given {@link ClassVisitor} visit the class contained in the resource {@link URL}.
     * 
     * @param visitor The class space visitor
     * @param url The class resource URL
     */
    public static void accept( final ClassVisitor visitor, final URL url )
    {
        if ( null == url )
        {
            return; // nothing to visit
        }
        try
        {
            final InputStream in = url.openStream();
            try
            {
                new ClassReader( in ).accept( adapt( visitor ), ASM_FLAGS );
            }
            finally
            {
                in.close();
            }
        }
        catch ( final ArrayIndexOutOfBoundsException e ) // NOPMD
        {
            // ignore broken class constant pool in icu4j
        }
        catch ( final Exception e )
        {
            Logs.trace( "Problem scanning: {}", url, e );
        }
    }

    /**
     * Returns the JVM descriptor for the given annotation class, such as "Ljavax/inject/Qualifier;".
     * 
     * @param clazz The annotation class
     * @return JVM descriptor of the class
     * @see ClassVisitor#visitAnnotation(String)
     */
    public static String jvmDescriptor( final Class<? extends Annotation> clazz )
    {
        return 'L' + clazz.getName().replace( '.', '/' ) + ';';
    }

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

    /**
     * Adapts the given {@link ClassVisitor} to its equivalent ASM form.
     * 
     * @param _cv The class visitor to adapt
     * @return ASM-compatible class visitor
     */
    private static org.eclipse.sisu.space.asm.ClassVisitor adapt( final ClassVisitor _cv )
    {
        return null == _cv ? null : new org.eclipse.sisu.space.asm.ClassVisitor( Opcodes.ASM5 )
        {
            @Override
            public void visit( final int version, final int access, final String name, final String signature,
                               final String superName, final String[] interfaces )
            {
                _cv.enterClass( access, name, superName, interfaces );
            }

            @Override
            public org.eclipse.sisu.space.asm.AnnotationVisitor visitAnnotation( final String desc,
                                                                                 final boolean visible )
            {
                final AnnotationVisitor _av = _cv.visitAnnotation( desc );
                return null == _av ? null : new org.eclipse.sisu.space.asm.AnnotationVisitor( Opcodes.ASM5 )
                {
                    {
                        _av.enterAnnotation();
                    }

                    @Override
                    public void visit( final String name, final Object value )
                    {
                        _av.visitElement( name, value instanceof Type ? ( (Type) value ).getClassName() : value );
                    }

                    @Override
                    public void visitEnd()
                    {
                        _av.leaveAnnotation();
                    }
                };
            }

            @Override
            public void visitEnd()
            {
                _cv.leaveClass();
            }
        };
    }
}
