/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * 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 com.simpligility.maven.plugins.android.phase08preparepackage;

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.IncludeExcludeSet;
import com.simpligility.maven.plugins.android.configuration.D8;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import static com.simpligility.maven.plugins.android.InclusionExclusionResolver.filterArtifacts;

/**
 * Converts compiled Java classes (including those containing Java 8 syntax) to the Android dex format.
 * It is a replacement for the {@link DexMojo}.
 *
 * You should only run one or the other.
 * By default D8 will run and Dex will not. But this is determined by the
 *
 * @author william.ferguson@xandar.com.au
 */
@Mojo(
    name = "d8",
    defaultPhase = LifecyclePhase.PREPARE_PACKAGE,
    requiresDependencyResolution = ResolutionScope.COMPILE
)
public class D8Mojo extends AbstractAndroidMojo
{
    private static final String JAR = "jar";

    /**
     * Configuration for the D8 command execution. It can be configured in the plugin configuration like so
     *
     *

     * <dex>
     *   <dexMechanism>d8|dex</dexMechanism>
     *   <jvmArguments>
     *     <jvmArgument>-Xms256m</jvmArgument>
     *     <jvmArgument>-Xmx512m</jvmArgument>
     *   </jvmArguments>
     *   <intermediate>true|false</intermediate>
     *   <mainDexList>path to class list file</mainDexList>
     *   <release>path to class list file</release>
     *   <minApi>path to class list file</minApi>
     *   <arguments>
     *     <argument>--someOtherArgA</argument>
     *     <argument>--someOtherArgB</argument>
     *   </arguments>
     * </dex>
     *

     *
     * or via properties dex* or command line parameters android.dex.*
     */
    @Parameter
    private D8 dex;

    /**
     * Extra JVM Arguments. Using these you can e.g. increase memory for the jvm running the build.
     */
    @Parameter( property = "android.dex.jvmArguments", defaultValue = "-Xmx1024M" )
    private String[] dexJvmArguments;

    /**
     * Decides whether to pass the --intermediate flag to d8.
     */
    @Parameter( property = "android.dex.intermediate", defaultValue = "false" )
    private boolean dexIntermediate;

    /**
     * Full path to class list to multi dex
     */
    @Parameter( property = "android.dex.maindexlist" )
    private String dexMainDexList;

    /**
     * Whether to pass the --release flag to d8.
     */
    @Parameter( property = "android.dex.release", defaultValue = "false" )
    private boolean dexRelease;

    /**
     * The minApi (if any) to pass to d8.
     */
    @Parameter( property = "android.dex.release" )
    private Integer dexMinApi;

    /**
     * Additional command line parameters passed to d8.
     */
    @Parameter( property = "android.dex.dexarguments" )
    private String dexArguments;

    /**
     * The name of the obfuscated JAR.
     */
    @Parameter( property = "android.proguard.obfuscatedJar" )
    private File obfuscatedJar;

    /**
     * Skips transitive dependencies. May be useful if the target classes directory is populated with the
     * {@code maven-dependency-plugin} and already contains all dependency classes.
     */
    @Parameter( property = "skipDependencies", defaultValue = "false" )
    private boolean skipDependencies;

    /**
     * Allows to include or exclude artifacts by type. The {@code include} parameter has higher priority than the
     * {@code exclude} parameter. These two parameters can be overridden by the {@code artifactSet} parameter. Empty
     * strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>aar</include>
     *         <includes>
     *         <excludes>
     *             <exclude>jar</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactTypeSet" )
    private IncludeExcludeSet artifactTypeSet;

    /**
     * Allows to include or exclude artifacts by {@code groupId}, {@code artifactId}, and {@code versionId}. The
     * {@code include} parameter has higher priority than the {@code exclude} parameter. These two parameters can
     * override the {@code artifactTypeSet} and {@code skipDependencies} parameters. Artifact {@code groupId},
     * {@code artifactId}, and {@code versionId} are specified by a string with the respective values separated using
     * a colon character {@code :}. {@code artifactId} and {@code versionId} can be optional covering an artifact
     * range. Empty strings are ignored. Example:
     *

     *     <artifactTypeSet>
     *         <includes>
     *             <include>foo-group:foo-artifact:1.0-SNAPSHOT</include>
     *             <include>bar-group:bar-artifact:1.0-SNAPSHOT</include>
     *             <include>baz-group:*</include>
     *         <includes>
     *         <excludes>
     *             <exclude>qux-group:qux-artifact:*</exclude>
     *         <excludes>
     *     </artifactTypeSet>
     *

     */
    @Parameter( property = "artifactSet" )
    private IncludeExcludeSet artifactSet;

    private String[] parsedJvmArguments;
    private boolean parsedIntermediate;
    private String parsedMainDexList;
    private String parsedDexArguments;
    private DexMechanism parsedDexMechanism;
    private boolean parsedRelease;
    private Integer parsedMinApi;

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    @Override
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        parseConfiguration();

        getLog().debug( "DexMechanism set to " + parsedDexMechanism );
        if ( parsedDexMechanism != DexMechanism.D8 )
        {
            getLog().info( "Not executing D8Mojo because DexMechanism set to " + parsedDexMechanism );
            return;
        }

        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( getLog() );

        if ( generateApk )
        {
            runD8( executor );
        }

        if ( attachJar )
        {
            File jarFile = new File( targetDirectory + File.separator
                + finalName + ".jar" );
            projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
        }

        if ( attachSources )
        {
            // Also attach an .apksources, containing sources from this project.
            final File apksources = createApkSourcesFile();
            projectHelper.attachArtifact( project, "apksources", apksources );
        }
    }

    private List<File> getDependencies()
    {
        final List<File> libraries = new ArrayList<>();
        for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
                artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
                artifactSet.getExcludes() ) )
        {
            if ( "jar".equals( artifact.getType() ) )
            {
                libraries.add( artifact.getFile() );
            }
        }

        return libraries;
    }

    /**
     * @return Set of input files for dex. This is a combination of directories and jar files.
     */
    private Set< File > getDexInputFiles()
    {
        final Set< File > inputs = new HashSet< File >();

        if ( obfuscatedJar != null && obfuscatedJar.exists() )
        {
            // proguard has been run, use this jar
            getLog().debug( "Adding dex input (obfuscatedJar) : " + obfuscatedJar );
            inputs.add( obfuscatedJar );
        }
        else
        {
            getLog().debug( "Using non-obfuscated input" );
            final File classesJar = new File( targetDirectory, finalName + ".jar" );
            inputs.add( classesJar );
            getLog().debug( "Adding dex input from : " + classesJar );

            for ( Artifact artifact : filterArtifacts( getTransitiveDependencyArtifacts(), skipDependencies,
                    artifactTypeSet.getIncludes(), artifactTypeSet.getExcludes(), artifactSet.getIncludes(),
                    artifactSet.getExcludes() ) )
            {
                if ( artifact.getType().equals( JAR ) )
                {
                    getLog().debug( "Adding dex input : " + artifact.getFile() );
                    inputs.add( artifact.getFile().getAbsoluteFile() );
                }
            }
        }

        return inputs;
    }

    private void parseConfiguration()
    {
        // config in pom found
        if ( dex != null )
        {
            // the if statements make sure that properties/command line
            // parameter overrides configuration
            // and that the dafaults apply in all cases;
            if ( dex.getJvmArguments() == null )
            {
                parsedJvmArguments = dexJvmArguments;
            }
            else
            {
                parsedJvmArguments = dex.getJvmArguments();
            }
            if ( dex.isIntermediate() == null )
            {
                parsedIntermediate = dexIntermediate;
            }
            else
            {
                parsedIntermediate = dex.isIntermediate();
            }
            if ( dex.getMainDexList() == null )
            {
                parsedMainDexList = dexMainDexList;
            }
            else
            {
                parsedMainDexList = dex.getMainDexList();
            }
            if ( dex.getDexArguments() == null )
            {
                parsedDexArguments = dexArguments;
            }
            else
            {
                parsedDexArguments = dex.getDexArguments();
            }
            parsedDexMechanism = dex.getDexMechanism();
            if ( dex.isRelease() == null )
            {
                parsedRelease = dexRelease;
            }
            else
            {
                parsedRelease = dex.isRelease();
            }
            if ( dex.getMinApi() == null )
            {
                parsedMinApi = dexMinApi;
            }
            else
            {
                parsedMinApi = dex.getMinApi();
            }
        }
        else
        {
            parsedJvmArguments = dexJvmArguments;
            parsedIntermediate = dexIntermediate;
            parsedMainDexList = dexMainDexList;
            parsedDexArguments = dexArguments;
            parsedDexMechanism = DexMechanism.Dex;
            parsedRelease = dexRelease;
            parsedMinApi = dexMinApi;
        }
    }

    private List< String > dexDefaultCommands() throws MojoExecutionException
    {
        List< String > commands = jarDefaultCommands();
        commands.add( getAndroidSdk().getD8JarPath() );
        return commands;
    }

    private List<String> jarDefaultCommands()
    {
        List< String > commands = javaDefaultCommands();
        commands.add( "-jar" );
        return commands;
    }

    private List<String> javaDefaultCommands()
    {
        List< String > commands = new ArrayList< String > ();
        if ( parsedJvmArguments != null )
        {
            for ( String jvmArgument : parsedJvmArguments )
            {
                // preserve backward compatibility allowing argument with or
                // without dash (e.g. Xmx512m as well as
                // -Xmx512m should work) (see
                // http://code.google.com/p/maven-android-plugin/issues/detail?id=153)
                if ( !jvmArgument.startsWith( "-" ) )
                {
                    jvmArgument = "-" + jvmArgument;
                }
                getLog().debug( "Adding jvm argument " + jvmArgument );
                commands.add( jvmArgument );
            }
        }
        return commands;
    }

    private void runD8( CommandExecutor executor )
        throws MojoExecutionException
    {
        final List< String > commands = dexDefaultCommands();
        final Set< File > inputFiles = getDexInputFiles();
        if ( parsedIntermediate )
        {
            commands.add( "--intermediate" );
        }
        if ( parsedMainDexList != null )
        {
            commands.add( "--main-dex-list " + parsedMainDexList );
        }
        if ( parsedDexArguments != null )
        {
            commands.add( parsedDexArguments );
        }

        if ( parsedRelease )
        {
            commands.add( "--release" );
        }

        if ( parsedMinApi != null )
        {
            commands.add( "--min api" );
            commands.add( parsedMinApi.toString() );
        }

        commands.add( "--output" );
        commands.add( targetDirectory.getAbsolutePath() );

        final File androidJar = getAndroidSdk().getAndroidJar();
        commands.add( "--lib "  );
        commands.add( androidJar.getAbsolutePath() );

        // Add project classpath
        final List<File> dependencies = getDependencies();
        for ( final File file : dependencies )
        {
            commands.add( "--classpath" );
            commands.add( file.getAbsolutePath() );
        }

        for ( File inputFile : inputFiles )
        {
            commands.add( inputFile.getAbsolutePath() );
        }

        getLog().info( "Convert classes to Dex : " + targetDirectory );
        executeJava( commands, executor );
    }

    private String executeJava( final List<String> commands, CommandExecutor executor ) throws MojoExecutionException
    {
        final String javaExecutable = getJavaExecutable().getAbsolutePath();
        getLog().debug( javaExecutable + " " + commands.toString() );
        try
        {
            executor.setCaptureStdOut( true );
            executor.executeCommand( javaExecutable, commands, project.getBasedir(), false );
            return executor.getStandardOut();
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

    /**
     * Figure out the full path to the current java executable.
     *
     * @return the full path to the current java executable.
     */
    private static File getJavaExecutable()
    {
        final String javaHome = System.getProperty( "java.home" );
        final String slash = File.separator;
        return new File( javaHome + slash + "bin" + slash + "java" );
    }

    /**
     * @return
     * @throws MojoExecutionException
     */
    protected File createApkSourcesFile() throws MojoExecutionException
    {
        final File apksources = new File( targetDirectory, finalName
            + ".apksources" );
        FileUtils.deleteQuietly( apksources );

        try
        {
            JarArchiver jarArchiver = new JarArchiver();
            jarArchiver.setDestFile( apksources );

            addDirectory( jarArchiver, assetsDirectory, "assets" );
            addDirectory( jarArchiver, resourceDirectory, "res" );
            addDirectory( jarArchiver, sourceDirectory, "src/main/java" );
            addJavaResources( jarArchiver, resources );

            jarArchiver.createArchive();
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while creating .apksource file.", e );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException while creating .apksource file.", e );
        }

        return apksources;
    }

    /**
     * Makes sure the string ends with "/"
     *
     * @param prefix
     *            any string, or null.
     * @return the prefix with a "/" at the end, never null.
     */
    protected String endWithSlash( String prefix )
    {
        prefix = StringUtils.defaultIfEmpty( prefix, "/" );
        if ( !prefix.endsWith( "/" ) )
        {
            prefix = prefix + "/";
        }
        return prefix;
    }

    /**
     * Adds a directory to a {@link JarArchiver} with a directory prefix.
     *
     * @param jarArchiver
     * @param directory
     *            The directory to add.
     * @param prefix
     *            An optional prefix for where in the Jar file the directory's contents should go.
     */
    protected void addDirectory( JarArchiver jarArchiver, File directory, String prefix )
    {
        if ( directory != null && directory.exists() )
        {
            final DefaultFileSet fileSet = new DefaultFileSet();
            fileSet.setPrefix( endWithSlash( prefix ) );
            fileSet.setDirectory( directory );
            jarArchiver.addFileSet( fileSet );
        }
    }

    /**
     * @param jarArchiver
     * @param javaResources
     */
    protected void addJavaResources( JarArchiver jarArchiver, List< Resource > javaResources )
    {
        for ( Resource javaResource : javaResources )
        {
            addJavaResource( jarArchiver, javaResource );
        }
    }

    /**
     * Adds a Java Resources directory (typically "src/main/resources") to a {@link JarArchiver}.
     *
     * @param jarArchiver
     * @param javaResource
     *            The Java resource to add.
     */
    protected void addJavaResource( JarArchiver jarArchiver, Resource javaResource )
    {
        if ( javaResource != null )
        {
            final File javaResourceDirectory = new File( javaResource.getDirectory() );
            if ( javaResourceDirectory.exists() )
            {
                final DefaultFileSet javaResourceFileSet = new DefaultFileSet();
                javaResourceFileSet.setDirectory( javaResourceDirectory );
                javaResourceFileSet.setPrefix( endWithSlash( "src/main/resources" ) );
                jarArchiver.addFileSet( javaResourceFileSet );
            }
        }
    }
}


/*
 * Copyright (C) 2009, 2010 Jayway AB
 *
 * 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 com.simpligility.maven.plugins.android;

import com.android.SdkConstants;
import com.android.annotations.Nullable;
import com.android.repository.Revision;
import com.android.sdklib.AndroidTargetHash;
import com.android.sdklib.AndroidVersion;
import com.android.sdklib.BuildToolInfo;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.repository.AndroidSdkHandler;
import com.android.sdklib.repository.targets.AndroidTargetManager;
import org.apache.maven.plugin.MojoExecutionException;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.Properties;

/**
 * Represents an Android SDK.
 *
 * @author hugo.josefson@jayway.com
 * @author Manfred Moser - manfred@simpligility.com
 */
public class AndroidSdk
{
    /**
     * the default API level for the SDK used as a fall back if none is supplied,
     * should ideally point to the latest available version
     */
    private static final String DEFAULT_ANDROID_API_LEVEL = "23";
    /**
     * property file in each platform folder with details about platform.
     */
    private static final String SOURCE_PROPERTIES_FILENAME = "source.properties";
    /**
     * property name for the sdk tools revision in sdk/tools/lib source.properties
     */
    private static final String SDK_TOOLS_REVISION_PROPERTY = "Pkg.Revision";

    /**
     * folder name for the sdk sub folder that contains the different platform versions.
     */
    private static final String PLATFORMS_FOLDER_NAME = "platforms";

    private static final String BIN_FOLDER_NAME_IN_TOOLS = "bin";

    private static final String PARAMETER_MESSAGE = "Please provide a proper Android SDK directory path as "
            + "configuration parameter <sdk><path>...</path></sdk> in the plugin <configuration/>. As an alternative,"
            + " you may add the parameter to commandline: -Dandroid.sdk.path=... or set environment variable "
            + AbstractAndroidMojo.ENV_ANDROID_HOME + ".";

    private final File sdkPath;
    private File platformToolsPath;
    private File toolsPath;

    private final IAndroidTarget androidTarget;
    private AndroidSdkHandler sdkManager;
    private int sdkMajorVersion;
    private String buildToolsVersion;
    private ProgressIndicatorImpl progressIndicator;

    public AndroidSdk( File sdkPath, String apiLevel )
    {
        this( sdkPath, apiLevel, null );
    }

    public AndroidSdk( File sdkPath, String apiLevel, @Nullable String buildToolsVersion )
    {
        this.sdkPath = sdkPath;
        this.buildToolsVersion = buildToolsVersion;
        this.progressIndicator = new ProgressIndicatorImpl();

        if ( sdkPath != null )
        {
            sdkManager = AndroidSdkHandler.getInstance( sdkPath );
            platformToolsPath = new File( sdkPath, SdkConstants.FD_PLATFORM_TOOLS );
            toolsPath = new File( sdkPath, SdkConstants.FD_TOOLS );

            if ( sdkManager == null )
            {
                throw invalidSdkException( sdkPath, apiLevel );
            }
        }
        loadSDKToolsMajorVersion();

        if ( apiLevel == null )
        {
            apiLevel = DEFAULT_ANDROID_API_LEVEL;
        }

        androidTarget = findPlatformByApiLevel( apiLevel );
        if ( androidTarget == null )
        {
            throw invalidSdkException( sdkPath, apiLevel );
        }
    }

    private InvalidSdkException invalidSdkException( File sdkPath, String platformOrApiLevel )
    {
        throw new InvalidSdkException( "Invalid SDK: Platform/API level " + platformOrApiLevel
                + " not available. This command should give you all you need:\n" + sdkPath.getAbsolutePath()
                + File.separator + "tools" + File.separator + "android update sdk --no-ui --obsolete --force" );
    }

    private IAndroidTarget findPlatformByApiLevel( String apiLevel )
    {
        // try find by api level first
        AndroidVersion version = null;
        try
        {
            version = new AndroidVersion( apiLevel );
            String hashString = AndroidTargetHash.getPlatformHashString( version );
            IAndroidTarget target = sdkManager.getAndroidTargetManager( progressIndicator )
                    .getTargetFromHashString( hashString, progressIndicator );

            // SdkManager may return a non-null IAndroidTarget that references nothing.
            // I suspect it points to an SDK that has been removed.
            if ( target != null && target.getLocation() != null )
            {
                return target;
            }
        }
        catch ( AndroidVersion.AndroidVersionException ignore )
        {
            throw new InvalidSdkException( "Error AndroidVersion: " + ignore.getMessage() );
        }

        // fallback to searching for platform on standard Android platforms (isPlatform() is true)
        for ( IAndroidTarget t: sdkManager.getAndroidTargetManager( null ).getTargets( null ) )
        {
            if ( t.isPlatform() && apiLevel.equals( t.getVersionName() ) )
            {
                return t;
            }
        }
        return null;
    }

    private void assertPathIsDirectory( final File path )
    {
        if ( path == null )
        {
            throw new InvalidSdkException( PARAMETER_MESSAGE );
        }
        if ( !path.isDirectory() )
        {
            throw new InvalidSdkException( "Path \"" + path + "\" is not a directory. " + PARAMETER_MESSAGE );
        }
    }

    /**
     * Get the aapt tool path.
     *
     * @return
     */
    public String getAaptPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.AAPT );
    }

    /**
     * Get the aild tool path
     * @return
     */
    public String getAidlPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.AIDL );
    }

    /**
     * Get the path for dx.jar
     * @return
     */
    public String getDxJarPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.DX_JAR );
    }

    /**
     * @return the path to the dx.jar
     */
    public String getD8JarPath()
    {
        final File pathToDexJar = new File( getPathForBuildTool( BuildToolInfo.PathId.DX_JAR ) );
        final File pathToD8Jar = new File( pathToDexJar.getParent(), "d8.jar" );
        return pathToD8Jar.getAbsolutePath();
    }

    /**
     * Get the path for proguard.jar
     * @return
     */
    public String getProguardJarPath()
    {
        File directory = new File( getToolsPath(), "proguard" + File.separator + "lib" + File.separator );
        File proguardJar = new File( directory, "proguard.jar" );
        if ( proguardJar.exists() )
        {
            return proguardJar.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + proguardJar );
    }

    /**
     * Get the path for shrinkedAndroid.jar
     * @return
     */
    public String getShrinkedAndroidJarPath()
    {
        File shrinkedAndroidJar = new File( getBuildToolsLibDirectoryPath(), "shrinkedAndroid.jar" );
        if ( shrinkedAndroidJar.exists() )
        {
            return shrinkedAndroidJar.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + shrinkedAndroidJar );
    }

    /**
     * Get the path for build-tools lib directory
     * @return
     */
    public String getBuildToolsLibDirectoryPath()
    {
        File buildToolsLib = new File( getBuildToolInfo().getLocation(), "lib" );
        if ( buildToolsLib.exists() )
        {
            return buildToolsLib.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + buildToolsLib );
    }

    /**
     * Get the path for mainDexClasses.rules
     * @return
     */
    public String getMainDexClassesRulesPath()
    {
        File mainDexClassesRules = new File( getBuildToolInfo().getLocation(),
                "mainDexClasses.rules" );
        if ( mainDexClassesRules.exists() )
        {
            return mainDexClassesRules.getAbsolutePath();
        }
        throw new InvalidSdkException( "Cannot find " + mainDexClassesRules );
    }

    public void assertThatBuildToolsVersionIsAtLeast( String version, String feature )
            throws InvalidSdkException, NumberFormatException
    {
        if ( getBuildToolInfo().getRevision().
                compareTo( Revision.parseRevision( version ) ) < 0 )
        {
            throw new InvalidSdkException( "Version of build tools must be at least "
                    + version + " for " + feature + " to work" );
        }
    }

    /**
     * Get the android debug tool path (adb).
     *
     * @return
     */
    public String getAdbPath()
    {
        return getPathForPlatformTool( SdkConstants.FN_ADB );
    }

    /**
     * Get the android zipalign path.
     *
     * @return
     */
    public String getZipalignPath()
    {
        return getPathForBuildTool( BuildToolInfo.PathId.ZIP_ALIGN );
    }

    /**
     * Get the android lint path.
     *
     * @return
     */
    public String getLintPath()
    {
        return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "lint" + ext( ".bat", "" ) );
    }

    /**
     * Get the android monkey runner path.
     *
     * @return
     */
    public String getMonkeyRunnerPath()
    {
        return getPathForTool( BIN_FOLDER_NAME_IN_TOOLS + "/" + "monkeyrunner" + ext( ".bat", "" ) );
    }

    /**
     * Get the apkbuilder path.
     *
     * @return
     */
    public String getApkBuilderPath()
    {
        return getPathForTool( "apkbuilder" + ext( ".bat", "" ) );
    }

    /**
     * Get the android tool path.
     *
     * @return
     */
    public String getAndroidPath()
    {
        String cmd = "android";
        String ext = SdkConstants.currentPlatform() == 2 ? ".bat" : "";

        return getPathForTool( cmd + ext );
    }

    /**
     * Get the path to the tools directory.
     * @return
     */
    public File getToolsPath()
    {
        return toolsPath;
    }

    private String getPathForBuildTool( BuildToolInfo.PathId pathId )
    {
        return getBuildToolInfo().getPath( pathId );
    }

    private BuildToolInfo getBuildToolInfo()
    {
        //First we use the build tools specified in the pom file
        if ( buildToolsVersion != null && !buildToolsVersion.equals( "" ) )
        {
            BuildToolInfo buildToolInfo = sdkManager.getBuildToolInfo( Revision.parseRevision( buildToolsVersion ),
                    progressIndicator );
            if ( buildToolInfo != null )
            {
                return buildToolInfo;
            }
            //Since we cannot find the build tool specified by the user we make it fail
            // instead of using the latest build tool version
            throw new InvalidSdkException( "Invalid SDK: Build-tools " + buildToolsVersion + " not found."
                    + " Check your Android SDK to install the build tools " + buildToolsVersion );
        }

        if ( androidTarget != null )
        {
            BuildToolInfo buildToolInfo = androidTarget.getBuildToolInfo();
            if ( buildToolInfo != null )
            {
                return buildToolInfo;
            }
        }
        // if no valid target is defined, or it has no build tools installed, try to use the latest
        BuildToolInfo latestBuildToolInfo = sdkManager.getLatestBuildTool( progressIndicator, true );
        if ( latestBuildToolInfo == null )
        {
            throw new InvalidSdkException( "Invalid SDK: Build-tools not found. Check the content of '"
                + sdkPath.getAbsolutePath() + File.separator + "build-tools', or run '"
                + sdkPath.getAbsolutePath() + File.separator + "tools" + File.separator
                + "android sdk' to install them" );
        }
        return latestBuildToolInfo;
    }

    private String getPathForPlatformTool( String tool )
    {
        return new File( platformToolsPath, tool ).getAbsolutePath();
    }

    private String getPathForTool( String tool )
    {
        return new File( toolsPath, tool ).getAbsolutePath();
    }

    private static String ext( String windowsExtension, String nonWindowsExtension )
    {
        if ( SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS )
        {
            return windowsExtension;
        }
        else
        {
            return nonWindowsExtension;
        }
    }

    /**
     * Returns the complete path for framework.aidl, based on this SDK.
     *
     * @return the complete path as a String, including the filename.
     */
    public String getPathForFrameworkAidl()
    {
        return androidTarget.getPath( IAndroidTarget.ANDROID_AIDL );
    }

    /**
     * Resolves the android.jar from this SDK.
     *
     * @return a File pointing to the android.jar file.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *             if the file can not be resolved.
     */
    public File getAndroidJar() throws MojoExecutionException
    {
        final String androidJarPath = androidTarget.getPath( IAndroidTarget.ANDROID_JAR );
        if ( androidJarPath == null )
        {
            throw new MojoExecutionException( "No AndroidJar found for " + androidTarget.getLocation() );
        }
        return new File ( androidJarPath );
    }

    /**
     * Resolves the path for this SDK.
     *
     * @return a File pointing to the SDk Directory.
     * @throws org.apache.maven.plugin.MojoExecutionException
     *             if the file can not be resolved.
     */
    public File getSdkPath() throws MojoExecutionException
    {
        if ( sdkPath.exists() )
        {
            return sdkPath;
        }
        throw new MojoExecutionException( "Can't find the SDK directory : " + sdkPath.getAbsolutePath() );
    }

    /**
     * This method returns the previously specified version. However, if none have been specified it returns the
     * "latest" version.
     */
    public File getPlatform()
    {
        assertPathIsDirectory( sdkPath );

        final File platformsDirectory = new File( sdkPath, PLATFORMS_FOLDER_NAME );
        assertPathIsDirectory( platformsDirectory );

        final File platformDirectory;
        if ( androidTarget == null )
        {
            IAndroidTarget latestTarget = null;
            AndroidTargetManager targetManager = sdkManager.getAndroidTargetManager( progressIndicator );
            for ( IAndroidTarget target: targetManager.getTargets( progressIndicator ) )
            {
                if ( target.isPlatform() )
                {
                    if ( latestTarget == null
                            || target.getVersion().getApiLevel() > latestTarget.getVersion().getApiLevel() )
                    {
                        latestTarget = target;
                    }
                }
            }
            platformDirectory = new File ( latestTarget.getLocation() );
        }
        else
        {
            platformDirectory = new File( androidTarget.getLocation() );
        }
        assertPathIsDirectory( platformDirectory );
        return platformDirectory;
    }

    /**
     * Loads the SDK Tools version
     */
    private void loadSDKToolsMajorVersion()
    {
        File propFile = new File( sdkPath, "tools/" + SOURCE_PROPERTIES_FILENAME );
        Properties properties = new Properties();
        try
        {
            properties.load( new FileInputStream( propFile ) );
        }
        catch ( IOException e )
        {
            throw new InvalidSdkException( "Error reading " + propFile.getAbsoluteFile() );
        }

        if ( properties.containsKey( SDK_TOOLS_REVISION_PROPERTY ) )
        {
            try
            {
                String versionString = properties.getProperty( SDK_TOOLS_REVISION_PROPERTY );
                String majorVersion;
                if ( versionString.matches( ".*[\\.| ].*" ) )
                {
                    String[] versions = versionString.split( "[\\.| ]" );
                    majorVersion = versions[ 0 ];
                }
                else
                {
                    majorVersion = versionString;
                }
                sdkMajorVersion = Integer.parseInt( majorVersion );
            }
            catch ( NumberFormatException e )
            {
                throw new InvalidSdkException( "Error - The property '" + SDK_TOOLS_REVISION_PROPERTY
                        + "' in the SDK source.properties file  number is not an Integer: "
                        + properties.getProperty( SDK_TOOLS_REVISION_PROPERTY ) );
            }
        }
    }

    /**
     * Returns the version of the SDK Tools.
     *
     * @return
     */
    public int getSdkMajorVersion()
    {
        return sdkMajorVersion;
    }
}


/*
 * Copyright (C) 2009 Jayway AB
 * Copyright (C) 2007-2008 JVending Masa
 *
 * 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 com.simpligility.maven.plugins.android.phase09package;

import com.simpligility.maven.plugins.android.AbstractAndroidMojo;
import com.simpligility.maven.plugins.android.CommandExecutor;
import com.simpligility.maven.plugins.android.ExecutionException;
import com.simpligility.maven.plugins.android.common.AaptCommandBuilder;
import com.simpligility.maven.plugins.android.common.AndroidExtension;
import com.simpligility.maven.plugins.android.common.NativeHelper;
import com.simpligility.maven.plugins.android.config.PullParameter;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.model.Resource;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;
import org.apache.maven.plugins.annotations.ResolutionScope;
import org.codehaus.plexus.archiver.ArchiverException;
import org.codehaus.plexus.archiver.jar.JarArchiver;
import org.codehaus.plexus.archiver.util.DefaultFileSet;

import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

import static com.simpligility.maven.plugins.android.common.AndroidExtension.AAR;
import static com.simpligility.maven.plugins.android.common.AndroidExtension.APKLIB;

/**
 * Creates the apklib file.

 * apklib files do not generate deployable artifacts.
 *
 * @author nmaiorana@gmail.com
 * @deprecated Use Aar instead see {@link com.simpligility.maven.plugins.android.phase09package.AarMojo}
 */
@Deprecated
@Mojo( name = "apklib", defaultPhase = LifecyclePhase.PACKAGE, requiresDependencyResolution = ResolutionScope.COMPILE )
public class ApklibMojo extends AbstractAndroidMojo
{
    /**
     * The name of the top level folder in the APKLIB where native libraries are found.
     * NOTE: This is inconsistent with APK where the folder is called "lib"
     */
    public static final String NATIVE_LIBRARIES_FOLDER = "libs";

    /**
     *
Classifier to add to the artifact generated. If given, the artifact will be an attachment instead.

     */
    @Parameter
    private String classifier;

    /**
     * Specifies the application makefile to use for the build (if other than the default Application.mk).
     */
    @Parameter
    @PullParameter
    private String applicationMakefile;

    /**
     * Defines the architecture for the NDK build
     */
    @Parameter( property = "android.ndk.build.architecture" )
    @PullParameter
    private String ndkArchitecture;

    /**
     * Specifies the classifier with which the artifact should be stored in the repository
     */
    @Parameter( property = "android.ndk.build.native-classifier" )
    @PullParameter
    private String ndkClassifier;

    private List<String> sourceFolders = new ArrayList<String>();

    /**
     * @throws MojoExecutionException
     * @throws MojoFailureException
     */
    public void execute() throws MojoExecutionException, MojoFailureException
    {
        String out = targetDirectory.getPath();
        for ( String src : project.getCompileSourceRoots() )
        {
            if ( !src.startsWith( out ) )
            {
                sourceFolders.add( src );
            }
        }

        generateIntermediateApk();

        File outputFile = createApkLibraryFile();

        if ( classifier == null )
        {
            // Set the generated file as the main artifact (because the pom states <packaging>apklib</packaging>)
            project.getArtifact().setFile( outputFile );
        }
        else
        {
            // If there is a classifier specified, attach the artifact using that
            projectHelper.attachArtifact( project, AndroidExtension.APKLIB, classifier, outputFile );
        }

        if ( attachJar )
        {
            final File jarFile = new File( targetDirectory,
                    finalName + ".jar" );
            projectHelper.attachArtifact( project, "jar", project.getArtifact().getClassifier(), jarFile );
        }
    }

    private File createApkLibraryFile() throws MojoExecutionException
    {
        final File apklibrary = new File( targetDirectory,
                finalName + "." + APKLIB );
        FileUtils.deleteQuietly( apklibrary );

        try
        {
            JarArchiver jarArchiver = new JarArchiver();
            jarArchiver.setDestFile( apklibrary );

            jarArchiver.addFile( destinationManifestFile, "AndroidManifest.xml" );
            addDirectory( jarArchiver, assetsDirectory, "assets" );
            addDirectory( jarArchiver, resourceDirectory, "res" );

            for ( String src : sourceFolders )
            {
                addDirectory( jarArchiver, new File( src ), "src" );
            }

            File[] overlayDirectories = getResourceOverlayDirectories();
            for ( File resOverlayDir : overlayDirectories )
            {
                if ( resOverlayDir != null && resOverlayDir.exists() )
                {
                    addDirectory( jarArchiver, resOverlayDir, "res" );
                }
            }

            addJavaResources( jarArchiver, resources, "src" );

            // Lastly, add any native libraries
            addNativeLibraries( jarArchiver );

            jarArchiver.createArchive();
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "ArchiverException while creating ." + APKLIB + " file.", e );
        }
        catch ( IOException e )
        {
            throw new MojoExecutionException( "IOException while creating ." + APKLIB + " file.", e );
        }

        return apklibrary;
    }

    private void addNativeLibraries( final JarArchiver jarArchiver ) throws MojoExecutionException
    {

        try
        {
            if ( nativeLibrariesDirectory.exists() )
            {
                getLog().info( nativeLibrariesDirectory + " exists, adding libraries." );
                addDirectory( jarArchiver, nativeLibrariesDirectory, NATIVE_LIBRARIES_FOLDER );
            }
            else
            {
                getLog().info( nativeLibrariesDirectory
                        + " does not exist, looking for libraries in target directory." );
                // Add native libraries built and attached in this build
                String[] ndkArchitectures = NativeHelper.getNdkArchitectures( ndkArchitecture,
                                                                              applicationMakefile,
                                                                              project.getBasedir() );
                for ( String architecture : ndkArchitectures )
                {
                    final File ndkLibsDirectory = new File( ndkOutputDirectory, architecture );
                    addSharedLibraries( jarArchiver, ndkLibsDirectory, architecture );

                    // Add native library dependencies
                    // FIXME: Remove as causes duplicate libraries when building final APK if this set includes
                    //        libraries from dependencies of the APKLIB
                    //final File dependentLibs = new File( ndkOutputDirectory.getAbsolutePath(), ndkArchitecture );
                    //addSharedLibraries( jarArchiver, dependentLibs, prefix );
                }
            }

            // Removing this as the APK is now able to pull the native libs from the chained apklibs
            // get native libs from other apklibs
//            for ( Artifact apkLibraryArtifact : getTransitiveDependencyArtifacts( APKLIB ) )
//            {
//                final File apklibLibsDirectory = getUnpackedLibNativesFolder( apkLibraryArtifact );
//                if ( apklibLibsDirectory.exists() )
//                {
//                    addDirectory( jarArchiver, apklibLibsDirectory, NATIVE_LIBRARIES_FOLDER );
//                }
//            }
        }
        catch ( ArchiverException e )
        {
            throw new MojoExecutionException( "IOException while creating ." + APKLIB + " file.", e );
        }
        // TODO: Next is to check for any:
        // TODO: - compiled in (as part of this build) libs
        // TODO:    - That is of course easy if the artifact is indeed attached
        // TODO:    - If not attached, it gets a little trickier  - check the target dir for any compiled .so files (generated by NDK mojo)
        // TODO:        - But where is that directory configured?
    }

    protected void addJavaResources( JarArchiver jarArchiver, List<Resource> javaResources, String prefix )
            throws IOException
    {
        for ( Resource javaResource : javaResources )
        {
            addJavaResource( jarArchiver, javaResource, prefix );
        }
    }

    /**
     * Adds a Java Resources directory (typically "src/main/resources") to a {@link JarArchiver}.
     *
     * @param jarArchiver
     * @param javaResource The Java resource to add.
     * @param prefix       An optional prefix for where in the Jar file the directory's contents should go.
     * @throws IOException in case the resource path can not be resolved
     */
    protected void addJavaResource( JarArchiver jarArchiver, Resource javaResource, String prefix )
            throws IOException
    {
        if ( javaResource != null )
        {
            final File javaResourceDirectory = new File( javaResource.getDirectory() );
            if ( javaResourceDirectory.exists() )
            {
                final String resourcePath = javaResourceDirectory.getCanonicalPath();
                final String apkLibUnpackBasePath = getUnpackedLibsDirectory().getCanonicalPath();
                // Don't include our dependencies' resource dirs.
                if ( ! resourcePath.startsWith( apkLibUnpackBasePath ) )
                {
                    final DefaultFileSet javaResourceFileSet = new DefaultFileSet();
                    javaResourceFileSet.setDirectory( javaResourceDirectory );
                    javaResourceFileSet.setPrefix( endWithSlash( prefix ) );
                    jarArchiver.addFileSet( javaResourceFileSet );
                }
            }
        }
    }

    /**
     * Makes sure the string ends with "/"
     *
     * @param prefix any string, or null.
     * @return the prefix with a "/" at the end, never null.
     */
    protected String endWithSlash( String prefix )
    {
        prefix = StringUtils.defaultIfEmpty( prefix, "/" );
        if ( ! prefix.endsWith( "/" ) )
        {
            prefix = prefix + "/";
        }
        return prefix;
    }

    /**
     * Adds a directory to a {@link JarArchiver} with a directory prefix.
     *
     * @param jarArchiver
     * @param directory   The directory to add.
     * @param prefix      An optional prefix for where in the Jar file the directory's contents should go.
     */
    protected void addDirectory( JarArchiver jarArchiver, File directory, String prefix )
    {
        if ( directory != null && directory.exists() )
        {
            final DefaultFileSet fileSet = new DefaultFileSet();
            fileSet.setPrefix( endWithSlash( prefix ) );
            fileSet.setDirectory( directory );

            // XXX: trying to avoid duplicated sources
            fileSet.setExcludes( new String[] { "**/R.java", "**/BuildConfig.java" } );

            jarArchiver.addFileSet( fileSet );
            getLog().debug( "Added files from " + directory );
        }
    }

    /**
     * Adds all shared libraries (.so) to a {@link JarArchiver} under 'libs'.
     *
     * @param jarArchiver The jarArchiver to add files to
     * @param directory   The directory to scan for .so files
     * @param architecture      The prefix for where in the jar the .so files will go.
     */
    protected void addSharedLibraries( JarArchiver jarArchiver, File directory, String architecture )
    {
        getLog().debug( "Searching for shared libraries in " + directory );
        File[] libFiles = directory.listFiles( new FilenameFilter()
        {
            public boolean accept( final File dir, final String name )
            {
                return name.startsWith( "lib" ) && name.endsWith( ".so" );
            }
        } );

        if ( libFiles != null )
        {
            for ( File libFile : libFiles )
            {
                String dest = NATIVE_LIBRARIES_FOLDER + "/" + architecture + "/" + libFile.getName();
                getLog().debug( "Adding " + libFile + " as " + dest );
                jarArchiver.addFile( libFile, dest );
            }
        }
    }

    /**
     * Generates an intermediate apk file (actually .ap_) containing the resources and assets.
     *
     * @throws MojoExecutionException
     */
    private void generateIntermediateApk() throws MojoExecutionException
    {

        CommandExecutor executor = CommandExecutor.Factory.createDefaultCommmandExecutor();
        executor.setLogger( this.getLog() );
        File[] overlayDirectories = getResourceOverlayDirectories();

        File androidJar = getAndroidSdk().getAndroidJar();
        File outputFile = new File( targetDirectory, finalName + ".ap_" );

        List<File> dependencyArtifactResDirectoryList = new ArrayList<File>();
        for ( Artifact libraryArtifact : getTransitiveDependencyArtifacts( APKLIB, AAR ) )
        {
            final File apklibResDirectory = getUnpackedLibResourceFolder( libraryArtifact );
            if ( apklibResDirectory.exists() )
            {
                dependencyArtifactResDirectoryList.add( apklibResDirectory );
            }
        }

        AaptCommandBuilder commandBuilder = AaptCommandBuilder
                .packageResources( getLog() )
                .forceOverwriteExistingFiles()
                .setPathToAndroidManifest( destinationManifestFile )
                .addResourceDirectoriesIfExists( overlayDirectories )
                .addResourceDirectoryIfExists( resourceDirectory )
                .addResourceDirectoriesIfExists( dependencyArtifactResDirectoryList )
                .autoAddOverlay()
                // NB aapt only accepts a single assets parameter - combinedAssets is a merge of all assets
                .addRawAssetsDirectoryIfExists( combinedAssets )
                .addExistingPackageToBaseIncludeSet( androidJar )
                .setOutputApkFile( outputFile )
                .addConfigurations( configurations )
                .setVerbose( aaptVerbose );

        getLog().debug( getAndroidSdk().getAaptPath() + " " + commandBuilder.toString() );
        getLog().info( "Generating apklib" );
        try
        {
            executor.setCaptureStdOut( true );
            List<String> commands = commandBuilder.build();
            executor.executeCommand( getAndroidSdk().getAaptPath(), commands, project.getBasedir(), false );
        }
        catch ( ExecutionException e )
        {
            throw new MojoExecutionException( "", e );
        }
    }

}