/*
 * Copyright (C) 2010 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.sdklib.internal.export;

import com.android.resources.Density;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.internal.export.MultiApkExportHelper.ExportException;
import com.android.sdklib.internal.project.ApkSettings;
import com.android.sdklib.internal.project.ProjectProperties;
import com.android.sdklib.internal.project.ProjectProperties.PropertyType;
import com.android.sdklib.xml.ManifestData;
import com.android.sdklib.xml.ManifestData.SupportsScreens;

import java.io.File;
import java.io.FilenameFilter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

/**
 * Class representing an Android project and its properties.
 *
 * Only the properties that pertain to the multi-apk export are present.
 */
public final class ProjectConfig {

    private static final String PROP_API = "api";
    private static final String PROP_SCREENS = "screens";
    private static final String PROP_GL = "gl";
    private static final String PROP_ABI = "splitByAbi";
    private static final String PROP_DENSITY = "splitByDensity";
    private static final String PROP_LOCALEFILTERS = "localeFilters";

    /**
     * List of densities and their associated aapt filter.
     */
    private static final String[][] DENSITY_LIST = new String[][] {
        new String[] { Density.HIGH.getResourceValue(),
                Density.HIGH.getResourceValue() + "," + Density.NODPI.getResourceValue() },
                new String[] { Density.MEDIUM.getResourceValue(),
                        Density.MEDIUM.getResourceValue() + "," +
                                Density.NODPI.getResourceValue() },
                        new String[] { Density.MEDIUM.getResourceValue(),
                Density.MEDIUM.getResourceValue() + "," + Density.NODPI.getResourceValue() },
    };

    private final File mProjectFolder;
    private final String mRelativePath;

    private final int mMinSdkVersion;
    private final int mGlEsVersion;
    private final SupportsScreens mSupportsScreens;
    private final boolean mSplitByAbi;
    private final boolean mSplitByDensity;
    private final Map<String, String> mLocaleFilters;
    /** List of ABIs not defined in the properties but actually existing in the project as valid
     * .so files */
    private final List<String> mAbis;

    static ProjectConfig create(File projectFolder, String relativePath,
            ManifestData manifestData) throws ExportException {
        // load the project properties
        ProjectProperties projectProp = ProjectProperties.load(projectFolder.getAbsolutePath(),
                PropertyType.DEFAULT);
        if (projectProp == null) {
            throw new ExportException(String.format("%1$s is missing for project %2$s",
                    PropertyType.DEFAULT.getFilename(), relativePath));
        }

        ApkSettings apkSettings = new ApkSettings(projectProp);

        return new ProjectConfig(projectFolder,
                relativePath,
                manifestData.getMinSdkVersion(),
                manifestData.getGlEsVersion(),
                manifestData.getSupportsScreensValues(),
                apkSettings.isSplitByAbi(),
                apkSettings.isSplitByDensity(),
                apkSettings.getLocaleFilters());
    }


    private ProjectConfig(File projectFolder, String relativePath,
            int minSdkVersion, int glEsVersion,
            SupportsScreens supportsScreens, boolean splitByAbi, boolean splitByDensity,
            Map<String, String> localeFilters) {
        mProjectFolder = projectFolder;
        mRelativePath = relativePath;
        mMinSdkVersion = minSdkVersion;
        mGlEsVersion = glEsVersion;
        mSupportsScreens = supportsScreens;
        mSplitByAbi = splitByAbi;
        mSplitByDensity = splitByDensity;
        mLocaleFilters = localeFilters;
        if (mSplitByAbi) {
            mAbis = findAbis();
        } else {
            mAbis = null;
        }
    }

    public File getProjectFolder() {
        return mProjectFolder;
    }


    public String getRelativePath() {
        return mRelativePath;
    }

    List<ApkData> getApkDataList() {
        // there are 3 cases:
        // 1. ABI split generate multiple apks with different build info, so they are different
        //    ApkData for all of them. Special case: split by abi but no native code => 1 ApkData.
        // 2. split by density or locale filters generate soft variant only, so they all go
        //    in the same ApkData.
        // 3. Both 1. and 2. means that more than one ApkData are created and they all get soft
        //    variants.

        ArrayList<ApkData> list = new ArrayList<ApkData>();

        Map<String, String> softVariants = computeSoftVariantMap();

        if (mSplitByAbi) {
            if (mAbis.size() > 0) {
                for (String abi : mAbis) {
                    list.add(new ApkData(this, abi, softVariants));
                }
            } else {
                // if there are no ABIs, then just generate a single ApkData with no specific ABI.
                list.add(new ApkData(this, softVariants));
            }
        } else {
            // create a single ApkData.
            list.add(new ApkData(this, softVariants));
        }

        return list;
    }

    int getMinSdkVersion() {
        return mMinSdkVersion;
    }

    SupportsScreens getSupportsScreens() {
        return mSupportsScreens;
    }

    int getGlEsVersion() {
        return mGlEsVersion;
    }

    boolean isSplitByDensity() {
        return mSplitByDensity;
    }

    boolean isSplitByAbi() {
        return mSplitByAbi;
    }

    /**
     * Returns a map of pair values (apk name suffix, aapt res filter) to be used to generate
     * multiple soft apk variants.
     */
    private Map<String, String> computeSoftVariantMap() {
        HashMap<String, String> map = new HashMap<String, String>();

        if (mSplitByDensity && mLocaleFilters.size() > 0) {
            for (String[] density : DENSITY_LIST) {
                for (Entry<String,String> entry : mLocaleFilters.entrySet()) {
                    map.put(density[0] + "-" + entry.getKey(),
                            density[1] + "," + entry.getValue());
                }
            }

        } else if (mSplitByDensity) {
            for (String[] density : DENSITY_LIST) {
                map.put(density[0], density[1]);
            }

        } else if (mLocaleFilters.size() > 0) {
            map.putAll(mLocaleFilters);

        }

        return map;
    }

    /**
     * Finds ABIs in a project folder. This is based on the presence of libs/<abi>/ folder.
     *
     * @param projectPath The OS path of the project.
     * @return A new non-null, possibly empty, list of ABI strings.
     */
    private List<String> findAbis() {
        ArrayList<String> abiList = new ArrayList<String>();
        File libs = new File(mProjectFolder, SdkConstants.FD_NATIVE_LIBS);
        if (libs.isDirectory()) {
            File[] abis = libs.listFiles();
            for (File abi : abis) {
                if (abi.isDirectory()) {
                    // only add the abi folder if there are .so files in it.
                    String[] content = abi.list(new FilenameFilter() {
                        public boolean accept(File dir, String name) {
                            return name.toLowerCase().endsWith(".so");
                        }
                    });

                    if (content.length > 0) {
                        abiList.add(abi.getName());
                    }
                }
            }
        }

        return abiList;
    }

    String getConfigString(boolean onlyManifestData) {
        StringBuilder sb = new StringBuilder();
        LogHelper.write(sb, PROP_API, mMinSdkVersion);
        LogHelper.write(sb, PROP_SCREENS, mSupportsScreens.getEncodedValues());

        if (mGlEsVersion != ManifestData.GL_ES_VERSION_NOT_SET) {
            LogHelper.write(sb, PROP_GL, "0x" + Integer.toHexString(mGlEsVersion));
        }

        if (onlyManifestData == false) {
            if (mSplitByAbi) {
                // need to not only encode true, but also the list of ABIs that will be used when
                // the project is exported. This is because the hard property is not so much
                // whether an apk is generated per ABI, but *how many* of them (since they all take
                // a different build Info).
                StringBuilder value = new StringBuilder(Boolean.toString(true));
                for (String abi : mAbis) {
                    value.append('|').append(abi);
                }
                LogHelper.write(sb, PROP_ABI, value);
            } else {
                LogHelper.write(sb, PROP_ABI, false);
            }

            // in this case we're simply always going to make 3 versions (which may not make sense)
            // so the boolean is enough.
            LogHelper.write(sb, PROP_DENSITY, Boolean.toString(mSplitByDensity));

            if (mLocaleFilters.size() > 0) {
                LogHelper.write(sb, PROP_LOCALEFILTERS, ApkSettings.writeLocaleFilters(mLocaleFilters));
            }
        }

        return sb.toString();
    }

    /**
     * Compares the current project config to a list of properties.
     * These properties are in the format output by {@link #getConfigString()}.
     * @param values the properties to compare to.
     * @return null if the properties exactly match the current config, an error message otherwise
     */
    String compareToProperties(Map<String, String> values) {
        String tmp;
        // Note that most properties must always be present in the map.
        try {
            // api must always be there
            if (mMinSdkVersion != Integer.parseInt(values.get(PROP_API))) {
                return "Attribute minSdkVersion changed";
            }
        } catch (NumberFormatException e) {
            // failed to convert an integer? consider the configs not equal.
            return "Failed to convert attribute minSdkVersion to an Integer";
        }

        try {
            tmp = values.get(PROP_GL); // GL is optional in the config string.
            if (tmp != null) {
                if (mGlEsVersion != Integer.decode(tmp)) {
                    return "Attribute glEsVersion changed";
                }
            }
        } catch (NumberFormatException e) {
            // failed to convert an integer? consider the configs not equal.
            return "Failed to convert attribute glEsVersion to an Integer";
        }

        tmp = values.get(PROP_DENSITY);
        if (tmp == null || mSplitByDensity != Boolean.valueOf(tmp)) {
            return "Property split.density changed or is missing from config file";
        }

        // compare the ABI. If splitByAbi is true, then compares the ABIs present in the project
        // as they must match.
        tmp = values.get(PROP_ABI);
        if (tmp == null) {
            return "Property split.abi is missing from config file";
        }
        String[] abis = tmp.split("\\|");
        if (mSplitByAbi != Boolean.valueOf(abis[0])) { // first value is always the split boolean
            return "Property split.abi changed";
        }
        // now compare the rest if needed.
        if (mSplitByAbi) {
            if (abis.length - 1 != mAbis.size()) {
                return "The number of ABIs available in the project changed";
            }
            for (int i = 1 ; i < abis.length ; i++) {
                if (mAbis.indexOf(abis[i]) == -1) {
                    return "The list of ABIs available in the project changed";
                }
            }
        }

        tmp = values.get(PROP_SCREENS);
        if (tmp != null) {
            SupportsScreens supportsScreens = new SupportsScreens(tmp);
            if (supportsScreens.equals(mSupportsScreens) == false) {
                return "Supports-Screens value changed";
            }
        } else {
            return "Supports-screens value missing from config file";
        }

        tmp = values.get(PROP_LOCALEFILTERS);
        if (tmp != null) {
            if (mLocaleFilters.equals(ApkSettings.readLocaleFilters(tmp)) == false) {
                return "Locale resource filter changed";
            }
        } else {
            // do nothing. locale filter is optional in the config string.
        }

        return null;
    }
}
