/*
 * Copyright 2008-2011 Rickard Öberg. 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 org.qi4j.bootstrap;

import java.io.File;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.regex.Pattern;
import org.qi4j.functional.Function;
import org.qi4j.functional.Iterables;
import org.qi4j.functional.Specification;

import static org.qi4j.functional.Iterables.filter;
import static org.qi4j.functional.Iterables.flatten;
import static org.qi4j.functional.Iterables.flattenIterables;
import static org.qi4j.functional.Iterables.iterable;
import static org.qi4j.functional.Iterables.map;

/**
 * Scan classpath for classes that matches given criteria. Useful for automated assemblies with lots of similar classes.
 */
public class ClassScanner
{
    /**
     * Get all classes from the same package of the given class, and recursively in all subpackages.
     * <p>
     * This only works if the seed class is loaded from a file: URL. Jar files are possible as well. Abstract classes
     * are not included in the results. For further filtering use e.g. Iterables.filter.
     * </p>
     * @param seedClass starting point for classpath scanning
     *
     * @return iterable of all concrete classes in the same package as the seedclass, and also all classes in subpackages.
     */
    public static Iterable<Class<?>> findClasses( final Class<?> seedClass )
    {
        CodeSource codeSource = seedClass.getProtectionDomain().getCodeSource();
        if( codeSource == null )
        {
            return Iterables.empty();
        }

        URL location = codeSource.getLocation();

        if( !location.getProtocol().equals( "file" ) )
        {
            throw new IllegalArgumentException( "Can only enumerate classes from file system locations. URL is:" + location );
        }

        final File file;
        try
        {
            file = new File( location.toURI().getPath() );
        }
        catch( URISyntaxException e )
        {
            throw new IllegalArgumentException( "The file location of codebase is invalid. Can not convert to URI. URL is:" + location );
        }

        if( file.getName().endsWith( ".jar" ) )
        {
            try
            {
                final String packageName = seedClass.getPackage().getName().replace( '.', '/' );

                JarFile jarFile = new JarFile( file );
                Iterable<JarEntry> entries = Iterables.iterable( jarFile.entries() );
                try
                {
                    return Iterables.toList( filter( new ValidClass(),
                                                     map( new Function<JarEntry, Class<?>>()
                                                     {
                                                         @Override
                                                         public Class map( JarEntry jarEntry )
                                                         {
                                                             String name = jarEntry.getName();
                                                             name = name.substring( 0, name.length() - 6 );
                                                             name = name.replace( '/', '.' );
                                                             try
                                                             {
                                                                 return seedClass.getClassLoader().loadClass( name );
                                                             }
                                                             catch( ClassNotFoundException e )
                                                             {
                                                                 return null;
                                                             }
                                                         }
                                                     }
                                                         , filter( new Specification<JarEntry>()
                                                     {
                                                         @Override
                                                         public boolean satisfiedBy( JarEntry jarEntry )
                                                         {
                                                             return jarEntry.getName()
                                                                        .startsWith( packageName ) && jarEntry.getName()
                                                                 .endsWith( ".class" );
                                                         }
                                                     }, entries ) ) ) );
                }
                finally
                {
                    jarFile.close();
                }
            }
            catch( IOException e )
            {
                throw new IllegalArgumentException( "Could not open jar file " + file, e );
            }
        }
        else
        {
            final File path = new File( file, seedClass.getPackage().getName().replace( '.', File.separatorChar ) );
            Iterable<File> files = findFiles( path, new Specification<File>()
            {
                @Override
                public boolean satisfiedBy( File file )
                {
                    return file.getName().endsWith( ".class" );
                }
            } );

            return filter( new ValidClass(),
                           map( new Function<File, Class<?>>()
                           {
                               @Override
                               public Class<?> map( File f )
                               {
                                   String fileName = f.getAbsolutePath().substring( file.toString().length() + 1 );
                                   fileName = fileName.replace( File.separatorChar, '.' )
                                       .substring( 0, fileName.length() - 6 );
                                   try
                                   {
                                       return seedClass.getClassLoader().loadClass( fileName );
                                   }
                                   catch( ClassNotFoundException e )
                                   {
                                       return null;
                                   }
                               }
                           }, files ) );
        }
    }

    /**
     * Useful specification for filtering classes based on a regular expression matching the class names.
     * <p>
     * Example: matches(".*Model") -&gt; match only class names that end with Model
     * </p>
     *
     * @param regex The regular expression to be matched.
     *
     * @return regex class name specification
     */
    public static Specification<Class<?>> matches( String regex )
    {
        final Pattern pattern = Pattern.compile( regex );

        return new Specification<Class<?>>()
        {
            @Override
            public boolean satisfiedBy( Class<?> aClass )
            {
                return pattern.matcher( aClass.getName() ).matches();
            }
        };
    }

    private static Iterable<File> findFiles( File directory, final Specification<File> filter )
    {
        return flatten( filter( filter, iterable( directory.listFiles() ) ),
                        flattenIterables( map( new Function<File, Iterable<File>>()
                        {
                            @Override
                            public Iterable<File> map( File file )
                            {
                                return findFiles( file, filter );
                            }
                        }, filter( new Specification<File>()
                        {
                            @Override
                            public boolean satisfiedBy( File file )
                            {
                                return file.isDirectory();
                            }
                        }, iterable( directory.listFiles() ) ) ) ) );
    }

    private static class ValidClass
        implements Specification<Class<?>>
    {
        @Override
        public boolean satisfiedBy( Class<?> item )
        {
            return ( item.isInterface() || !Modifier.isAbstract( item.getModifiers() ) ) && ( !item.isEnum() && !item.isAnonymousClass() );
        }
    }
}
