/*
 * 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.ide.common.build;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.build.FilterData;
import com.android.build.OutputFile;
import com.android.build.VariantOutput;
import com.android.builder.testing.api.DeviceConfigProvider;
import com.android.ide.common.process.ProcessException;
import com.android.ide.common.process.ProcessExecutor;
import com.android.resources.Density;
import com.google.common.collect.ImmutableList;

import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Helper class to help with installation of multi-output variants.
 */
public class SplitOutputMatcher {


    /**
     * Determines and return the list of APKs to use based on given device density and abis.
     *
     * if there are pure splits, use the split-select tool otherwise revert to store logic.
     *
     * @param processExecutor an executor to execute native processes.
     * @param splitSelectExe the split select tool optionally.
     * @param deviceConfigProvider the device configuration.
     * @param outputs the tested variant outpts.
     * @param variantAbiFilters a list of abi filters applied to the variant. This is used in place
     *                          of the outputs, if there is a single output with no abi filters.
     *                          If the list is null or empty, then the variant does not restrict ABI
     *                          packaging.
     * @return the list of APK files to install.
     * @throws ProcessException
     */
    @NonNull
    public static List<File> computeBestOutput(
            @NonNull ProcessExecutor processExecutor,
            @Nullable File splitSelectExe,
            @NonNull DeviceConfigProvider deviceConfigProvider,
            @NonNull List<? extends VariantOutput> outputs,
            @Nullable Collection<String> variantAbiFilters) throws ProcessException {


        // build the list of APKs.
        List<String> splitApksPath = new ArrayList<String>();
        OutputFile mainApk = null;
        for (VariantOutput output : outputs) {
            for (OutputFile outputFile : output.getOutputs()) {
                if (!outputFile.getOutputFile().getAbsolutePath().equals(
                        output.getMainOutputFile().getOutputFile().getAbsolutePath())) {

                    splitApksPath.add(outputFile.getOutputFile().getAbsolutePath());
                }
            }
            mainApk = output.getMainOutputFile();
        }
        if (mainApk == null) {
            throw new RuntimeException(
                    "Cannot retrieve the main APK from variant outputs");
        }
        if (splitApksPath.isEmpty()) {
            List<File> apkFiles = new ArrayList<File>();

            // now look for a matching output file
            List<OutputFile> outputFiles = SplitOutputMatcher.computeBestOutput(
                    outputs,
                    variantAbiFilters,
                    deviceConfigProvider.getDensity(),
                    deviceConfigProvider.getAbis());
            for (OutputFile outputFile : outputFiles) {
                apkFiles.add(outputFile.getOutputFile());
            }
            return apkFiles;
        } else {
            if (splitSelectExe == null) {
                throw new RuntimeException(
                        "Pure splits installation requires build tools 22 or above");
            }
            return computeBestOutput(processExecutor, splitSelectExe, deviceConfigProvider,
                    mainApk.getOutputFile(), splitApksPath);
        }
    }

    /**
     * Determines and return the list of APKs to use based on given device density and abis.
     *
     * if there are pure splits, use the split-select tool otherwise revert to store logic.
     *
     * @param processExecutor an executor to execute native processes.
     * @param splitSelectExe the split select tool optionally.
     * @param deviceConfigProvider the device configuration.
     * @param mainApk the main apk file.
     * @param splitApksPath the list of split apks path.
     * @return the list of APK files to install.
     * @throws ProcessException
     */
    @NonNull
    public static List<File> computeBestOutput(
            @NonNull ProcessExecutor processExecutor,
            @NonNull File splitSelectExe,
            @NonNull DeviceConfigProvider deviceConfigProvider,
            @NonNull File mainApk,
            @NonNull Collection<String> splitApksPath) throws ProcessException {

        // build the list of APKs.
        if (splitApksPath.isEmpty()) {
            return ImmutableList.of(mainApk);
        } else {
            List<File> apkFiles = new ArrayList<File>();

            Set<String> resultApksPath = new HashSet<String>();
            for (String abi : deviceConfigProvider.getAbis()) {
                resultApksPath.addAll(SplitSelectTool.splitSelect(
                        processExecutor,
                        splitSelectExe,
                        deviceConfigProvider.getConfigFor(abi),
                        mainApk.getAbsolutePath(),
                        splitApksPath));
            }
            for (String resultApkPath : resultApksPath) {
                apkFiles.add(new File(resultApkPath));
            }
            // and add back the main APK.
            apkFiles.add(mainApk);
            return apkFiles;
        }
    }

    /**
     * Determines and return the list of APKs to use based on given device density and abis.
     *
     * This uses the same logic as the store, using two passes:
     * First, find all the compatible outputs.
     * Then take the one with the highest versionCode.
     *
     * @param outputs the outputs to choose from.
     * @param variantAbiFilters a list of abi filters applied to the variant. This is used in place
     *                          of the outputs, if there is a single output with no abi filters.
     *                          If the list is null, then the variant does not restrict ABI
     *                          packaging.
     * @param deviceDensity the density of the device.
     * @param deviceAbis a list of ABIs supported by the device.
     * @return the list of APKs to install or null if none are compatible.
     */
    @NonNull
    public static List<OutputFile> computeBestOutput(
            @NonNull List<? extends VariantOutput> outputs,
            @Nullable Collection<String> variantAbiFilters,
            int deviceDensity,
            @NonNull Collection<String> deviceAbis) {
        Density densityEnum = Density.getEnum(deviceDensity);

        String densityValue;
        if (densityEnum == null) {
            densityValue = null;
        } else {
            densityValue = densityEnum.getResourceValue();
        }

        // gather all compatible matches.
        Set<VariantOutput> matches = new HashSet<VariantOutput>();

        // find a matching output.
        for (VariantOutput variantOutput : outputs) {
            for (OutputFile output : variantOutput.getOutputs()) {
                String densityFilter = getFilter(output, OutputFile.DENSITY);
                String abiFilter = getFilter(output, OutputFile.ABI);

                if (densityFilter != null && !densityFilter.equals(densityValue)) {
                    continue;
                }

                if (abiFilter != null && !deviceAbis.contains(abiFilter)) {
                    continue;
                }
                // variantOutput can be added several times to matches.
                matches.add(variantOutput);
            }
        }

        if (matches.isEmpty()) {
            return ImmutableList.of();
        }

        VariantOutput match = Collections.max(matches, new Comparator<VariantOutput>() {
            @Override
            public int compare(VariantOutput splitOutput, VariantOutput splitOutput2) {
                return splitOutput.getVersionCode() - splitOutput2.getVersionCode();
            }
        });

        OutputFile mainOutputFile = match.getMainOutputFile();
        return isMainApkCompatibleWithDevice(mainOutputFile, variantAbiFilters, deviceAbis)
                ? ImmutableList.of(mainOutputFile)
                : ImmutableList.<OutputFile>of();
    }

    private static boolean isMainApkCompatibleWithDevice(
            OutputFile mainOutputFile,
            Collection<String> variantAbiFilters,
            Collection<String> deviceAbis) {
        // so far, we are not dealing with the pure split files...
        if (getFilter(mainOutputFile, OutputFile.ABI) == null
                && variantAbiFilters != null
                && !variantAbiFilters.isEmpty()) {
            // if we have a match that has no abi filter, and we have variant-level filters, then
            // we need to make sure that the variant filters are compatible with the device abis.
            for (String abi : deviceAbis) {
                if (variantAbiFilters.contains(abi)) {
                    return true;
                }
            }
            return false;
        }
        return true;
    }

    @Nullable
    private static String getFilter(@NonNull OutputFile outputFile, @NonNull String filterType) {
        for (FilterData filterData : outputFile.getFilters()) {
            if (filterData.getFilterType().equals(filterType)) {
                return filterData.getIdentifier();
            }
        }
        return null;
    }
}
