/*
 * Copyright (C) 2014 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.build.gradle.tasks;

import com.android.annotations.NonNull;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.build.gradle.api.ApkOutputFile;
import com.android.build.gradle.internal.model.FilterDataImpl;
import com.android.build.gradle.internal.scope.TaskConfigAction;
import com.android.build.gradle.internal.scope.VariantOutputScope;
import com.android.build.gradle.internal.scope.VariantScope;
import com.android.build.gradle.internal.variant.BaseVariantData;
import com.android.build.gradle.internal.variant.BaseVariantOutputData;
import com.android.builder.core.AndroidBuilder;
import com.android.builder.core.VariantConfiguration;
import com.android.builder.model.SigningConfig;
import com.android.builder.packaging.SigningException;
import com.android.builder.packaging.ZipAbortException;
import com.android.ide.common.signing.KeytoolException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.common.util.concurrent.Callables;

import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFiles;
import org.gradle.api.tasks.Nested;
import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.OutputFiles;
import org.gradle.api.tasks.ParallelizableTask;
import org.gradle.api.tasks.TaskAction;

import java.io.File;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Package each split resources into a specific signed apk file.
 */
@ParallelizableTask
public class PackageSplitRes extends SplitRelatedTask {

    private Set<String> densitySplits;

    private Set<String> languageSplits;

    private String outputBaseName;

    private SigningConfig signingConfig;

    /**
     * This directories are not officially input/output to the task as they are shared among tasks.
     * To be parallelizable, we must only define our I/O in terms of files...
     */
    private File inputDirectory;

    private File outputDirectory;

    @InputFiles
    public List<File> getInputFiles() {
        final ImmutableList.Builder<File> builder = ImmutableList.builder();
        forEachInputFile(new SplitFileHandler() {
            @Override
            public void execute(String split, File file) {
                builder.add(file);
            }
        });
        return builder.build();
    }

    @OutputFiles
    public List<File> getOutputFiles() {
        ImmutableList.Builder<File> builder = ImmutableList.builder();
        for (ApkOutputFile apk : getOutputSplitFiles()) {
            builder.add(apk.getOutputFile());
        }
        return builder.build();
    }

    @Override
    public File getApkMetadataFile() {
        return null;
    }

    /**
     * Calculates the list of output files, coming from the list of input files, mangling the output
     * file name.
     */
    @Override
    public List<ApkOutputFile> getOutputSplitFiles() {
        final ImmutableList.Builder<ApkOutputFile> builder = ImmutableList.builder();
        forEachInputFile(new SplitFileHandler() {
            @Override
            public void execute(String split, File file) {
                // find the split identification, if null, the split is not requested any longer.
                FilterData filterData = null;
                for (String density : densitySplits) {
                    if (split.startsWith(density)) {
                        filterData = FilterDataImpl.build(
                                OutputFile.FilterType.DENSITY.toString(), density);
                    }

                }

                if (languageSplits.contains(unMangleSplitName(split))) {
                    filterData = FilterDataImpl.build(
                            OutputFile.FilterType.LANGUAGE.toString(), unMangleSplitName(split));
                }
                if (filterData != null) {
                    builder.add(new ApkOutputFile(
                            OutputFile.OutputType.SPLIT,
                            ImmutableList.of(filterData),
                            Callables.returning(
                                    new File(outputDirectory, getOutputFileNameForSplit(split)))));
                }

            }
        });
        return builder.build();
    }

    @TaskAction
    protected void doFullTaskAction() {
        forEachInputFile(
                new SplitFileHandler() {
                    @Override
                    public void execute(String split, File file) {
                        File outFile = new File(outputDirectory, getOutputFileNameForSplit(split));
                        try {
                            AndroidBuilder.signApk(file, signingConfig, outFile);
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        } catch (KeytoolException e) {
                            throw new RuntimeException(e);
                        } catch (SigningException e) {
                            throw new RuntimeException(e);
                        } catch (NoSuchAlgorithmException e) {
                            throw new RuntimeException(e);
                        } catch (ZipAbortException e) {
                            throw new RuntimeException(e);
                        } catch (com.android.builder.signing.SigningException e) {
                            throw new RuntimeException(e);
                        }
                    }
                });
    }

    private interface SplitFileHandler {
        void execute(String split, File file);
    }

    /**
     * Runs the handler for each task input file, providing the split identifier (possibly with a
     * suffix generated by aapt) and the input file handle.
     */
    private void forEachInputFile(SplitFileHandler handler) {
        Pattern resourcePattern = Pattern.compile("resources-" + outputBaseName + ".ap__(.*)");

        // make a copy of the expected densities and languages filters.
        List<String> densitiesCopy = Lists.newArrayList(densitySplits);
        List<String> languagesCopy = Lists.newArrayList(languageSplits);

        // resources- and .ap_ should be shared in a setting somewhere. see BasePlugin:1206
        File[] fileLists = inputDirectory.listFiles();
        if (fileLists != null) {
            for (File file : fileLists) {
                Matcher match = resourcePattern.matcher(file.getName());
                // each time we match, we remove the associated filter from our copies.
                if (match.matches() && !match.group(1).isEmpty()
                        && isValidSplit(densitiesCopy, languagesCopy, match.group(1))) {
                    handler.execute(match.group(1), file);
                }
            }
        }
        // manually invoke the handler for filters we did not find associated files, apply best
        // guess on the actual file names.
        for (String density : densitiesCopy) {
            handler.execute(density,
                    new File(inputDirectory, "resources-" + outputBaseName + ".ap__" + density));
        }
        for (String language : languagesCopy) {
            handler.execute(language,
                    new File(inputDirectory, "resources-" + outputBaseName + ".ap__" + language));

        }
    }

    /**
     * Returns true if the passed split identifier is a valid identifier (valid mean it is a
     * requested split for this task). A density split identifier can be suffixed with characters
     * added by aapt.
     */
    private static boolean isValidSplit(
            List<String> densities,
            List<String> languages,
            @NonNull String splitWithOptionalSuffix) {
        for (String density : densities) {
            if (splitWithOptionalSuffix.startsWith(density)) {
                densities.remove(density);
                return true;
            }
        }
        String mangledName = unMangleSplitName(splitWithOptionalSuffix);
        if (languages.contains(mangledName)) {
            languages.remove(mangledName);
            return true;
        }
        return false;
    }

    public String getOutputFileNameForSplit(final String split) {
        String archivesBaseName = (String)getProject().getProperties().get("archivesBaseName");
        String apkName = archivesBaseName + "-" + outputBaseName + "_" + split;
        return apkName + (signingConfig == null ? "-unsigned.apk" : "-unaligned.apk");
    }

    @Override
    public List<FilterData> getSplitsData() {
        ImmutableList.Builder<FilterData> filterDataBuilder = ImmutableList.builder();
        addAllFilterData(filterDataBuilder, densitySplits, OutputFile.FilterType.DENSITY);
        addAllFilterData(filterDataBuilder, languageSplits, OutputFile.FilterType.LANGUAGE);
        return filterDataBuilder.build();
    }

    /**
     * Un-mangle a split name as created by the aapt tool to retrieve a split name as configured in
     * the project's build.gradle.
     *
     * when dealing with several split language in a single split, each language (+ optional region)
     * will be seperated by an underscore.
     *
     * note that there is currently an aapt bug, remove the 'r' in the region so for instance,
     * fr-rCA becomes fr-CA, temporarily put it back until it is fixed.
     *
     * @param splitWithOptionalSuffix the mangled split name.
     */
    public static String unMangleSplitName(String splitWithOptionalSuffix) {
        String mangledName = splitWithOptionalSuffix.replaceAll("_", ",");
        return mangledName.contains("-r") ? mangledName : mangledName.replace("-", "-r");
    }

    @Input
    public Set<String> getDensitySplits() {
        return densitySplits;
    }

    public void setDensitySplits(Set<String> densitySplits) {
        this.densitySplits = densitySplits;
    }

    @Input
    public Set<String> getLanguageSplits() {
        return languageSplits;
    }

    public void setLanguageSplits(Set<String> languageSplits) {
        this.languageSplits = languageSplits;
    }

    @Input
    public String getOutputBaseName() {
        return outputBaseName;
    }

    public void setOutputBaseName(String outputBaseName) {
        this.outputBaseName = outputBaseName;
    }

    @Nested
    @Optional
    public SigningConfig getSigningConfig() {
        return signingConfig;
    }

    public void setSigningConfig(SigningConfig signingConfig) {
        this.signingConfig = signingConfig;
    }

    public File getInputDirectory() {
        return inputDirectory;
    }

    public void setInputDirectory(File inputDirectory) {
        this.inputDirectory = inputDirectory;
    }

    public File getOutputDirectory() {
        return outputDirectory;
    }

    public void setOutputDirectory(File outputDirectory) {
        this.outputDirectory = outputDirectory;
    }

    // ----- ConfigAction -----

    public static class ConfigAction implements TaskConfigAction<PackageSplitRes> {

        private VariantScope scope;

        public ConfigAction(VariantScope scope) {
            this.scope = scope;
        }

        @Override
        @NonNull
        public String getName() {
            return scope.getTaskName("package", "SplitResources");
        }

        @Override
        @NonNull
        public Class<PackageSplitRes> getType() {
            return PackageSplitRes.class;
        }

        @Override
        public void execute(@NonNull PackageSplitRes packageSplitResourcesTask) {
            BaseVariantData<? extends BaseVariantOutputData> variantData = scope.getVariantData();
            List<? extends BaseVariantOutputData> outputs = variantData.getOutputs();

            final VariantConfiguration config = variantData.getVariantConfiguration();
            Set<String> densityFilters = variantData.getFilters(OutputFile.FilterType.DENSITY);
            Set<String> languageFilters = variantData.getFilters(OutputFile.FilterType.LANGUAGE);

            final BaseVariantOutputData variantOutputData = outputs.get(0);
            variantOutputData.packageSplitResourcesTask = packageSplitResourcesTask;
            VariantOutputScope variantOutputScope = variantOutputData.getScope();
            packageSplitResourcesTask.setInputDirectory(
                    variantOutputScope.getProcessResourcePackageOutputFile().getParentFile());
            packageSplitResourcesTask.setDensitySplits(densityFilters);
            packageSplitResourcesTask.setLanguageSplits(languageFilters);
            packageSplitResourcesTask.setOutputBaseName(config.getBaseName());
            packageSplitResourcesTask.setSigningConfig(config.getSigningConfig());
            packageSplitResourcesTask.setOutputDirectory(scope.getSplitOutputDirectory());
            packageSplitResourcesTask.setAndroidBuilder(scope.getGlobalScope().getAndroidBuilder());
            packageSplitResourcesTask.setVariantName(config.getFullName());
            packageSplitResourcesTask.dependsOn(
                    variantOutputScope.getProcessResourcesTask().getName());
        }
    }
}
