/*
 * Copyright (C) 2009 The Android Open Source Project
 *
 * 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.android.ant;

import com.android.sdklib.build.ApkBuilder;
import com.android.sdklib.build.ApkCreationException;
import com.android.sdklib.build.DuplicateFileException;
import com.android.sdklib.build.SealedApkException;
import com.android.sdklib.build.ApkBuilder.FileEntry;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.types.Path;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

public class ApkBuilderTask extends SingleDependencyTask {

    private final static Pattern PATTERN_JAR_EXT = Pattern.compile("^.+\\.jar$",
            Pattern.CASE_INSENSITIVE);

    private String mOutFolder;
    private String mApkFilepath;
    private String mResourceFile;
    private boolean mVerbose = false;
    private boolean mDebugPackaging = false;
    private boolean mDebugSigning = false;
    private boolean mHasCode = true;

    private Path mDexPath;

    private final ArrayList<Path> mZipList = new ArrayList<Path>();
    private final ArrayList<Path> mSourceList = new ArrayList<Path>();
    private final ArrayList<Path> mJarfolderList = new ArrayList<Path>();
    private final ArrayList<Path> mJarfileList = new ArrayList<Path>();
    private final ArrayList<Path> mNativeList = new ArrayList<Path>();

    private static class SourceFolderInputPath extends InputPath {
        public SourceFolderInputPath(File file) {
            super(file);
        }

        @Override
        public boolean ignores(File file) {
            if (file.isDirectory()) {
                return !ApkBuilder.checkFolderForPackaging(file.getName());
            } else {
                return !ApkBuilder.checkFileForPackaging(file.getName());
            }
        }
    }

    /**
     * Sets the value of the "outfolder" attribute.
     * @param outFolder the value.
     */
    public void setOutfolder(Path outFolder) {
        mOutFolder = TaskHelper.checkSinglePath("outfolder", outFolder);
    }

    /**
     * Sets the full filepath to the apk to generate.
     * @param filepath
     */
    public void setApkfilepath(String filepath) {
        mApkFilepath = filepath;
    }

    /**
     * Sets the resourcefile attribute
     * @param resourceFile
     */
    public void setResourcefile(String resourceFile) {
        mResourceFile = resourceFile;
    }

    /**
     * Sets the value of the "verbose" attribute.
     * @param verbose the value.
     */
    public void setVerbose(boolean verbose) {
        mVerbose = verbose;
    }

    /**
     * Sets the value of the "debug" attribute.
     * @param debug the debug mode value.
     */
    public void setDebug(boolean debug) {
        System.out.println("WARNNG: Using deprecated 'debug' attribute in ApkBuilderTask." +
        "Use 'debugpackaging' and 'debugsigning' instead.");
        mDebugPackaging = debug;
        mDebugSigning = debug;
    }

    /**
     * Sets the value of the "debugpackaging" attribute.
     * @param debug the debug mode value.
     */
    public void setDebugpackaging(boolean debug) {
        mDebugPackaging = debug;
    }

    /**
     * Sets the value of the "debugsigning" attribute.
     * @param debug the debug mode value.
     */
    public void setDebugsigning(boolean debug) {
        mDebugSigning = debug;
    }

    /**
     * Sets the hascode attribute. Default is true.
     * If set to false, then <dex> and <sourcefolder> nodes are ignored and not processed.
     * @param hasCode the value of the attribute.
     */
    public void setHascode(boolean hasCode) {
        mHasCode   = hasCode;
    }

    /**
     * Returns an object representing a nested <var>zip</var> element.
     */
    public Object createZip() {
        Path path = new Path(getProject());
        mZipList.add(path);
        return path;
    }

    /**
     * Returns an object representing a nested <var>dex</var> element.
     * This is similar to a nested <var>file</var> element, except when {@link #mHasCode}
     * is <code>false</code> in which case it's ignored.
     */
    public Object createDex() {
        if (mDexPath == null) {
            return mDexPath = new Path(getProject());
        } else {
            throw new BuildException("Only one <dex> inner element can be provided");
        }
    }

    /**
     * Returns an object representing a nested <var>sourcefolder</var> element.
     */
    public Object createSourcefolder() {
        Path path = new Path(getProject());
        mSourceList.add(path);
        return path;
    }

    /**
     * Returns an object representing a nested <var>jarfolder</var> element.
     */
    public Object createJarfolder() {
        Path path = new Path(getProject());
        mJarfolderList.add(path);
        return path;
    }

    /**
     * Returns an object representing a nested <var>jarfile</var> element.
     */
    public Object createJarfile() {
        Path path = new Path(getProject());
        mJarfileList.add(path);
        return path;
    }

    /**
     * Returns an object representing a nested <var>nativefolder</var> element.
     */
    public Object createNativefolder() {
        Path path = new Path(getProject());
        mNativeList.add(path);
        return path;
    }

    @Override
    public void execute() throws BuildException {

        File outputFile;
        if (mApkFilepath != null) {
            outputFile = new File(mApkFilepath);
        } else {
            throw new BuildException("missing attribute 'apkFilepath'");
        }

        if (mResourceFile == null) {
            throw new BuildException("missing attribute 'resourcefile'");
        }

        if (mOutFolder == null) {
            throw new BuildException("missing attribute 'outfolder'");
        }

        // check dexPath is only one file.
        File dexFile = null;
        if (mHasCode) {
            String[] dexFiles = mDexPath.list();
            if (dexFiles.length != 1) {
                throw new BuildException(String.format(
                        "Expected one dex file but path value resolve to %d files.",
                        dexFiles.length));
            }
            dexFile = new File(dexFiles[0]);
        }

        try {
            // build list of input files/folders to compute dependencies
            // add the content of the zip files.
            List<InputPath> inputPaths = new ArrayList<InputPath>();

            // resource file
            InputPath resourceInputPath = new InputPath(new File(mOutFolder, mResourceFile));
            inputPaths.add(resourceInputPath);

            // dex file
            if (dexFile != null) {
                inputPaths.add(new InputPath(dexFile));
            }

            // zip input files
            List<File> zipFiles = new ArrayList<File>();
            for (Path pathList : mZipList) {
                for (String path : pathList.list()) {
                    File f =  new File(path);
                    zipFiles.add(f);
                    inputPaths.add(new InputPath(f));
                }
            }

            // now go through the list of source folders used to add non java files.
            List<File> sourceFolderList = new ArrayList<File>();
            if (mHasCode) {
                for (Path pathList : mSourceList) {
                    for (String path : pathList.list()) {
                        File f =  new File(path);
                        sourceFolderList.add(f);
                        // because this is a source folder but we only care about non
                        // java files.
                        inputPaths.add(new SourceFolderInputPath(f));
                    }
                }
            }

            // now go through the list of jar folders.
            List<File> jarFileList = new ArrayList<File>();
            for (Path pathList : mJarfolderList) {
                for (String path : pathList.list()) {
                    // it's ok if top level folders are missing
                    File folder = new File(path);
                    if (folder.isDirectory()) {
                        String[] filenames = folder.list(new FilenameFilter() {
                            public boolean accept(File dir, String name) {
                                return PATTERN_JAR_EXT.matcher(name).matches();
                            }
                        });

                        for (String filename : filenames) {
                            File f = new File(folder, filename);
                            jarFileList.add(f);
                            inputPaths.add(new InputPath(f));
                        }
                    }
                }
            }

            // now go through the list of jar files.
            for (Path pathList : mJarfileList) {
                for (String path : pathList.list()) {
                    File f = new File(path);
                    jarFileList.add(f);
                    inputPaths.add(new InputPath(f));
                }
            }

            // now the native lib folder.
            List<FileEntry> nativeFileList = new ArrayList<FileEntry>();
            for (Path pathList : mNativeList) {
                for (String path : pathList.list()) {
                    // it's ok if top level folders are missing
                    File folder = new File(path);
                    if (folder.isDirectory()) {
                        List<FileEntry> entries = ApkBuilder.getNativeFiles(folder,
                                mDebugPackaging);
                        // add the list to the list of native files and then create an input
                        // path for each file
                        nativeFileList.addAll(entries);

                        for (FileEntry entry : entries) {
                            inputPaths.add(new InputPath(entry.mFile));
                        }
                    }
                }
            }

            // Finally figure out the path to the dependency file.
            String depFile = outputFile.getAbsolutePath() + ".d";

            // check dependencies
            if (initDependencies(depFile, inputPaths) && dependenciesHaveChanged() == false) {
                System.out.println(
                        "No changes. No need to create apk.");
                return;
            }

            if (mDebugSigning) {
                System.out.println(String.format(
                        "Creating %s and signing it with a debug key...", outputFile.getName()));
            } else {
                System.out.println(String.format(
                        "Creating %s for release...", outputFile.getName()));
            }

            ApkBuilder apkBuilder = new ApkBuilder(
                    outputFile,
                    resourceInputPath.getFile(),
                    dexFile,
                    mDebugSigning ? ApkBuilder.getDebugKeystore() : null,
                    mVerbose ? System.out : null);
            apkBuilder.setDebugMode(mDebugPackaging);


            // add the content of the zip files.
            for (File f : zipFiles) {
                apkBuilder.addZipFile(f);
            }

            // now go through the list of file to directly add the to the list.
            for (File f : sourceFolderList) {
                apkBuilder.addSourceFolder(f);
            }

            // now go through the list of jar folders.
            for (Path pathList : mJarfolderList) {
                for (String path : pathList.list()) {
                    // it's ok if top level folders are missing
                    File folder = new File(path);
                    if (folder.isDirectory()) {
                        String[] filenames = folder.list(new FilenameFilter() {
                            public boolean accept(File dir, String name) {
                                return PATTERN_JAR_EXT.matcher(name).matches();
                            }
                        });

                        for (String filename : filenames) {
                            apkBuilder.addResourcesFromJar(new File(folder, filename));
                        }
                    }
                }
            }

            // now go through the list of jar files.
            for (File f : jarFileList) {
                apkBuilder.addResourcesFromJar(f);
            }

            // and finally the native files
            apkBuilder.addNativeLibraries(nativeFileList);

            // close the archive
            apkBuilder.sealApk();

            // and generate the dependency file
            generateDependencyFile(depFile, inputPaths, outputFile.getAbsolutePath());
        } catch (DuplicateFileException e) {
            System.err.println(String.format(
                    "Found duplicate file for APK: %1$s\nOrigin 1: %2$s\nOrigin 2: %3$s",
                    e.getArchivePath(), e.getFile1(), e.getFile2()));
            throw new BuildException(e);
        } catch (ApkCreationException e) {
            throw new BuildException(e);
        } catch (SealedApkException e) {
            throw new BuildException(e);
        } catch (IllegalArgumentException e) {
            throw new BuildException(e);
        }
    }

    @Override
    protected String getExecTaskName() {
        return "apkbuilder";
    }
}
