package org.apache.maven.tools.plugin.annotations.scanner;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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.
 */

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugins.annotations.Component;
import org.apache.maven.plugins.annotations.Execute;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.tools.plugin.annotations.datamodel.ComponentAnnotationContent;
import org.apache.maven.tools.plugin.annotations.datamodel.ExecuteAnnotationContent;
import org.apache.maven.tools.plugin.annotations.datamodel.MojoAnnotationContent;
import org.apache.maven.tools.plugin.annotations.datamodel.ParameterAnnotationContent;
import org.apache.maven.tools.plugin.annotations.scanner.visitors.MojoAnnotationVisitor;
import org.apache.maven.tools.plugin.annotations.scanner.visitors.MojoClassVisitor;
import org.apache.maven.tools.plugin.annotations.scanner.visitors.MojoFieldVisitor;
import org.apache.maven.tools.plugin.extractor.ExtractionException;
import org.codehaus.plexus.logging.AbstractLogEnabled;
import org.codehaus.plexus.util.DirectoryScanner;
import org.codehaus.plexus.util.IOUtil;
import org.codehaus.plexus.util.StringUtils;
import org.codehaus.plexus.util.reflection.Reflector;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.Type;

import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

/**
 * @author Olivier Lamy
 * @since 3.0
 */
@org.codehaus.plexus.component.annotations.Component( role = MojoAnnotationsScanner.class )
public class DefaultMojoAnnotationsScanner
    extends AbstractLogEnabled
    implements MojoAnnotationsScanner
{
    private Reflector reflector = new Reflector();

    public Map<String, MojoAnnotatedClass> scan( MojoAnnotationsScannerRequest request )
        throws ExtractionException
    {
        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
        try
        {

            for ( Artifact dependency : request.getDependencies() )
            {
                File dependencyFile = dependency.getFile();
                if ( dependencyFile != null && dependencyFile.exists() )
                {
                    if ( dependencyFile.isDirectory() )
                    {
                        mojoAnnotatedClasses.putAll(
                            scanDirectory( dependencyFile, request.getIncludePatterns(), dependency, true ) );
                    }
                    else
                    {
                        mojoAnnotatedClasses.putAll(
                            scanFile( dependencyFile, request.getIncludePatterns(), dependency, true ) );
                    }
                }
            }

            for ( File classDirectory : request.getClassesDirectories() )
            {
                if ( classDirectory.exists() && classDirectory.isDirectory() )
                {
                    mojoAnnotatedClasses.putAll(
                        scanDirectory( classDirectory, request.getIncludePatterns(), request.getProject().getArtifact(),
                                       false ) );
                }
            }

            return mojoAnnotatedClasses;
        }
        catch ( IOException e )
        {
            throw new ExtractionException( e.getMessage(), e );
        }
    }

    /**
     * @param archiveFile
     * @param includePatterns
     * @param artifact
     * @param excludeMojo     for dependencies we exclude Mojo annotations found
     * @return
     * @throws IOException
     * @throws ExtractionException
     */
    protected Map<String, MojoAnnotatedClass> scanFile( File archiveFile, List<String> includePatterns,
                                                        Artifact artifact, boolean excludeMojo )
        throws IOException, ExtractionException
    {
        if ( !archiveFile.exists() )
        {
            return Collections.emptyMap();
        }
        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
        ZipInputStream archiveStream = new ZipInputStream( new FileInputStream( archiveFile ) );

        try
        {
            for ( ZipEntry zipEntry = archiveStream.getNextEntry(); zipEntry != null;
                  zipEntry = archiveStream.getNextEntry() )
            {
                if ( zipEntry.getName().endsWith( ".class" ) )
                {
                    MojoClassVisitor mojoClassVisitor = new MojoClassVisitor( getLogger() );

                    ClassReader rdr = new ClassReader( archiveStream );
                    rdr.accept( mojoClassVisitor,
                                ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG );
                    analyzeVisitors( mojoClassVisitor );
                    if ( excludeMojo )
                    {
                        mojoClassVisitor.getMojoAnnotatedClass().setMojo( null );
                    }
                    if ( isStoreClass( mojoClassVisitor.getMojoAnnotatedClass() ) != null )
                    {
                        getLogger().debug(
                            "found MojoAnnotatedClass:" + mojoClassVisitor.getMojoAnnotatedClass().getClassName() + ":"
                                + mojoClassVisitor.getMojoAnnotatedClass() );
                        mojoClassVisitor.getMojoAnnotatedClass().setArtifact( artifact );
                        mojoAnnotatedClasses.put( mojoClassVisitor.getMojoAnnotatedClass().getClassName(),
                                                  mojoClassVisitor.getMojoAnnotatedClass() );
                    }
                }
            }
        }
        finally
        {
            IOUtil.close( archiveStream );
        }
        return mojoAnnotatedClasses;
    }

    /**
     * @param classDirectory
     * @param includePatterns
     * @param artifact
     * @param excludeMojo     for dependencies we exclude Mojo annotations found
     * @return
     * @throws IOException
     * @throws ExtractionException
     */
    protected Map<String, MojoAnnotatedClass> scanDirectory( File classDirectory, List<String> includePatterns,
                                                             Artifact artifact, boolean excludeMojo )
        throws IOException, ExtractionException
    {
        if ( !classDirectory.exists() )
        {
            return Collections.emptyMap();
        }
        Map<String, MojoAnnotatedClass> mojoAnnotatedClasses = new HashMap<String, MojoAnnotatedClass>();
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir( classDirectory );
        scanner.addDefaultExcludes();
        if ( includePatterns != null )
        {
            scanner.setIncludes( includePatterns.toArray( new String[includePatterns.size()] ) );
        }
        scanner.scan();
        String[] classFiles = scanner.getIncludedFiles();

        for ( String classFile : classFiles )
        {
            if ( !classFile.endsWith( ".class" ) )
            {
                continue;
            }

            InputStream is = new BufferedInputStream( new FileInputStream( new File( classDirectory, classFile ) ) );
            try
            {
                MojoClassVisitor mojoClassVisitor = new MojoClassVisitor( getLogger() );
                ClassReader rdr = new ClassReader( is );
                rdr.accept( mojoClassVisitor,
                            ClassReader.SKIP_FRAMES | ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG );
                analyzeVisitors( mojoClassVisitor );

                MojoAnnotatedClass mojoAnnotatedClass = mojoClassVisitor.getMojoAnnotatedClass();

                if ( excludeMojo )
                {
                    mojoAnnotatedClass.setMojo( null );
                }

                if ( isStoreClass( mojoAnnotatedClass ) != null )
                {
                    getLogger().debug( "found MojoAnnotatedClass:" + mojoAnnotatedClass.getClassName() + ":"
                                           + mojoAnnotatedClass );
                    mojoAnnotatedClass.setArtifact( artifact );
                    mojoAnnotatedClasses.put( mojoAnnotatedClass.getClassName(), mojoAnnotatedClass );
                }
            }
            finally
            {
                IOUtil.close( is );
            }
        }
        return mojoAnnotatedClasses;
    }

    private MojoAnnotatedClass isStoreClass( MojoAnnotatedClass mojoAnnotatedClass )
    {
        // see MPLUGIN-206 we can have intermediate classes without annotations
        if ( mojoAnnotatedClass == null )
        {
            return null;
        }
        return mojoAnnotatedClass;
        /**
         if ( !mojoAnnotatedClass.getComponents().isEmpty() || !mojoAnnotatedClass.getParameters().isEmpty()
         || mojoAnnotatedClass.getExecute() != null || mojoAnnotatedClass.getMojo() != null )
         {
         return mojoAnnotatedClass;
         }
         return null;
         **/
    }


    protected void analyzeVisitors( MojoClassVisitor mojoClassVisitor )
        throws ExtractionException
    {

        try
        {
            MojoAnnotationVisitor mojoAnnotationVisitor =
                mojoClassVisitor.getAnnotationVisitorMap().get( Mojo.class.getName() );
            if ( mojoAnnotationVisitor != null )
            {
                MojoAnnotationContent mojoAnnotationContent = new MojoAnnotationContent();
                for ( Map.Entry<String, Object> entry : mojoAnnotationVisitor.getAnnotationValues().entrySet() )
                {
                    reflector.invoke( mojoAnnotationContent, entry.getKey(), new Object[]{ entry.getValue() } );
                }
                mojoClassVisitor.getMojoAnnotatedClass().setMojo( mojoAnnotationContent );
            }

            mojoAnnotationVisitor = mojoClassVisitor.getAnnotationVisitorMap().get( Execute.class.getName() );
            if ( mojoAnnotationVisitor != null )
            {
                ExecuteAnnotationContent executeAnnotationContent = new ExecuteAnnotationContent();

                for ( Map.Entry<String, Object> entry : mojoAnnotationVisitor.getAnnotationValues().entrySet() )
                {
                    reflector.invoke( executeAnnotationContent, entry.getKey(), new Object[]{ entry.getValue() } );
                }
                mojoClassVisitor.getMojoAnnotatedClass().setExecute( executeAnnotationContent );
            }

            List<MojoFieldVisitor> mojoFieldVisitors =
                mojoClassVisitor.findFieldWithAnnotationClass( Parameter.class.getName() );

            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
            {
                ParameterAnnotationContent parameterAnnotationContent =
                    new ParameterAnnotationContent( mojoFieldVisitor.getFieldName(), mojoFieldVisitor.getClassName() );
                if ( mojoFieldVisitor.getMojoAnnotationVisitor() != null )
                {
                    for ( Map.Entry<String, Object> entry : mojoFieldVisitor.getMojoAnnotationVisitor().getAnnotationValues().entrySet() )
                    {
                        reflector.invoke( parameterAnnotationContent, entry.getKey(),
                                          new Object[]{ entry.getValue() } );
                    }

                }
                mojoClassVisitor.getMojoAnnotatedClass().getParameters().put( parameterAnnotationContent.getFieldName(),
                                                                              parameterAnnotationContent );
            }

            mojoFieldVisitors = mojoClassVisitor.findFieldWithAnnotationClass( Component.class.getName() );

            for ( MojoFieldVisitor mojoFieldVisitor : mojoFieldVisitors )
            {
                ComponentAnnotationContent componentAnnotationContent =
                    new ComponentAnnotationContent( mojoFieldVisitor.getFieldName() );

                if ( mojoFieldVisitor.getMojoAnnotationVisitor() != null )
                {
                    for ( Map.Entry<String, Object> entry : mojoFieldVisitor.getMojoAnnotationVisitor().getAnnotationValues().entrySet() )
                    {
                        String methodName = entry.getKey();
                        if ( StringUtils.equals( "role", methodName ) )
                        {
                            Type type = (Type) entry.getValue();
                            componentAnnotationContent.setRoleClassName( type.getClassName() );
                        }
                        else
                        {
                            reflector.invoke( componentAnnotationContent, entry.getKey(),
                                              new Object[]{ entry.getValue() } );
                        }
                    }
                    if ( StringUtils.isEmpty( componentAnnotationContent.getRoleClassName() ) )
                    {
                        componentAnnotationContent.setRoleClassName( mojoFieldVisitor.getClassName() );
                    }
                }
                mojoClassVisitor.getMojoAnnotatedClass().getComponents().put( componentAnnotationContent.getFieldName(),
                                                                              componentAnnotationContent );
            }

        }
        catch ( Exception e )
        {
            throw new ExtractionException( e.getMessage(), e );
        }
    }
}
