/*
 * Copyright (C) 2008 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.avd;

import com.android.prefs.AndroidLocation;
import com.android.prefs.AndroidLocation.AndroidLocationException;
import com.android.sdklib.IAndroidTarget;
import com.android.sdklib.ISdkLog;
import com.android.sdklib.SdkConstants;
import com.android.sdklib.SdkManager;
import com.android.sdklib.internal.avd.AvdManager.AvdInfo.AvdStatus;
import com.android.sdklib.internal.project.ProjectProperties;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.FilenameFilter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Android Virtual Device Manager to manage AVDs.
 */
public final class AvdManager {

    /**
     * Exception thrown when something is wrong with a target path.
     */
    private final static class InvalidTargetPathException extends Exception {
        private static final long serialVersionUID = 1L;

        InvalidTargetPathException(String message) {
            super(message);
        }
    }

    public static final String AVD_FOLDER_EXTENSION = ".avd";  //$NON-NLS-1$

    public final static String AVD_INFO_PATH = "path";         //$NON-NLS-1$
    public final static String AVD_INFO_TARGET = "target";     //$NON-NLS-1$

    /**
     * AVD/config.ini key name representing the SDK-relative path of the skin folder, if any,
     * or a 320x480 like constant for a numeric skin size.
     *
     * @see #NUMERIC_SKIN_SIZE
     */
    public final static String AVD_INI_SKIN_PATH = "skin.path"; //$NON-NLS-1$
    /**
     * AVD/config.ini key name representing an UI name for the skin.
     * This config key is ignored by the emulator. It is only used by the SDK manager or
     * tools to give a friendlier name to the skin.
     * If missing, use the {@link #AVD_INI_SKIN_PATH} key instead.
     */
    public final static String AVD_INI_SKIN_NAME = "skin.name"; //$NON-NLS-1$
    /**
     * AVD/config.ini key name representing the path to the sdcard file.
     * If missing, the default name "sdcard.img" will be used for the sdcard, if there's such
     * a file.
     *
     * @see #SDCARD_IMG
     */
    public final static String AVD_INI_SDCARD_PATH = "sdcard.path"; //$NON-NLS-1$
    /**
     * AVD/config.ini key name representing the size of the SD card.
     * This property is for UI purposes only. It is not used by the emulator.
     *
     * @see #SDCARD_SIZE_PATTERN
     */
    public final static String AVD_INI_SDCARD_SIZE = "sdcard.size"; //$NON-NLS-1$
    /**
     * AVD/config.ini key name representing the first path where the emulator looks
     * for system images. Typically this is the path to the add-on system image or
     * the path to the platform system image if there's no add-on.
     * <p/>
     * The emulator looks at {@link #AVD_INI_IMAGES_1} before {@link #AVD_INI_IMAGES_2}.
     */
    public final static String AVD_INI_IMAGES_1 = "image.sysdir.1"; //$NON-NLS-1$
    /**
     * AVD/config.ini key name representing the second path where the emulator looks
     * for system images. Typically this is the path to the platform system image.
     *
     * @see #AVD_INI_IMAGES_1
     */
    public final static String AVD_INI_IMAGES_2 = "image.sysdir.2"; //$NON-NLS-1$

    /**
     * Pattern to match pixel-sized skin "names", e.g. "320x480".
     */
    public final static Pattern NUMERIC_SKIN_SIZE = Pattern.compile("([0-9]{2,})x([0-9]{2,})"); //$NON-NLS-1$

    private final static String USERDATA_IMG = "userdata.img"; //$NON-NLS-1$
    private final static String CONFIG_INI = "config.ini"; //$NON-NLS-1$
    private final static String SDCARD_IMG = "sdcard.img"; //$NON-NLS-1$

    private final static String INI_EXTENSION = ".ini"; //$NON-NLS-1$
    private final static Pattern INI_NAME_PATTERN = Pattern.compile("(.+)\\" + //$NON-NLS-1$
            INI_EXTENSION + "$",                                               //$NON-NLS-1$
            Pattern.CASE_INSENSITIVE);

    private final static Pattern IMAGE_NAME_PATTERN = Pattern.compile("(.+)\\.img$", //$NON-NLS-1$
            Pattern.CASE_INSENSITIVE);

    /**
     * Pattern for matching SD Card sizes, e.g. "4K" or "16M".
     */
    public final static Pattern SDCARD_SIZE_PATTERN = Pattern.compile("(\\d+)([MK])"); //$NON-NLS-1$

    /** Regex used to validate characters that compose an AVD name. */
    public final static Pattern RE_AVD_NAME = Pattern.compile("[a-zA-Z0-9._-]+"); //$NON-NLS-1$

    /** List of valid characters for an AVD name. Used for display purposes. */
    public final static String CHARS_AVD_NAME = "a-z A-Z 0-9 . _ -"; //$NON-NLS-1$

    public final static String HARDWARE_INI = "hardware.ini"; //$NON-NLS-1$

    /** An immutable structure describing an Android Virtual Device. */
    public static final class AvdInfo implements Comparable<AvdInfo> {

        /**
         * Status for an {@link AvdInfo}. Indicates whether or not this AVD is valid.
         */
        public static enum AvdStatus {
            /** No error */
            OK,
            /** Missing 'path' property in the ini file */
            ERROR_PATH,
            /** Missing config.ini file in the AVD data folder */
            ERROR_CONFIG,
            /** Missing 'target' property in the ini file */
            ERROR_TARGET_HASH,
            /** Target was not resolved from its hash */
            ERROR_TARGET,
            /** Unable to parse config.ini */
            ERROR_PROPERTIES,
            /** System Image folder in config.ini doesn't exist */
            ERROR_IMAGE_DIR;
        }

        private final String mName;
        private final String mPath;
        private final String mTargetHash;
        private final IAndroidTarget mTarget;
        private final Map<String, String> mProperties;
        private final AvdStatus mStatus;

        /**
         * Creates a new valid AVD info. Values are immutable.
         * <p/>
         * Such an AVD is available and can be used.
         * The error string is set to null.
         *
         * @param name The name of the AVD (for display or reference)
         * @param path The path to the config.ini file
         * @param targetHash the target hash
         * @param target The target. Can be null, if the target was not resolved.
         * @param properties The property map. Cannot be null.
         */
        public AvdInfo(String name, String path, String targetHash, IAndroidTarget target,
                Map<String, String> properties) {
            this(name, path, targetHash, target, properties, AvdStatus.OK);
        }

        /**
         * Creates a new <em>invalid</em> AVD info. Values are immutable.
         * <p/>
         * Such an AVD is not complete and cannot be used.
         * The error string must be non-null.
         *
         * @param name The name of the AVD (for display or reference)
         * @param path The path to the config.ini file
         * @param targetHash the target hash
         * @param target The target. Can be null, if the target was not resolved.
         * @param properties The property map. Can be null.
         * @param status The {@link AvdStatus} of this AVD. Cannot be null.
         */
        public AvdInfo(String name, String path, String targetHash, IAndroidTarget target,
                Map<String, String> properties, AvdStatus status) {
            mName = name;
            mPath = path;
            mTargetHash = targetHash;
            mTarget = target;
            mProperties = properties == null ? null : Collections.unmodifiableMap(properties);
            mStatus = status;
        }

        /** Returns the name of the AVD. */
        public String getName() {
            return mName;
        }

        /** Returns the path of the AVD data directory. */
        public String getPath() {
            return mPath;
        }

        /**
         * Returns the target hash string.
         */
        public String getTargetHash() {
            return mTargetHash;
        }

        /** Returns the target of the AVD, or <code>null</code> if it has not been resolved. */
        public IAndroidTarget getTarget() {
            return mTarget;
        }

        /** Returns the {@link AvdStatus} of the receiver. */
        public AvdStatus getStatus() {
            return mStatus;
        }

        /**
         * Helper method that returns the .ini {@link File} for a given AVD name.
         * @throws AndroidLocationException if there's a problem getting android root directory.
         */
        public static File getIniFile(String name) throws AndroidLocationException {
            String avdRoot;
            avdRoot = getBaseAvdFolder();
            return new File(avdRoot, name + INI_EXTENSION);
        }

        /**
         * Returns the .ini {@link File} for this AVD.
         * @throws AndroidLocationException if there's a problem getting android root directory.
         */
        public File getIniFile() throws AndroidLocationException {
            return getIniFile(mName);
        }

        /**
         * Helper method that returns the Config {@link File} for a given AVD name.
         */
        public static File getConfigFile(String path) {
            return new File(path, CONFIG_INI);
        }

        /**
         * Returns the Config {@link File} for this AVD.
         */
        public File getConfigFile() {
            return getConfigFile(mPath);
        }

        /**
         * Returns an unmodifiable map of properties for the AVD. This can be null.
         */
        public Map<String, String> getProperties() {
            return mProperties;
        }

        /**
         * Returns the error message for the AVD or <code>null</code> if {@link #getStatus()}
         * returns {@link AvdStatus#OK}
         */
        public String getErrorMessage() {
            try {
                switch (mStatus) {
                    case ERROR_PATH:
                        return String.format("Missing AVD 'path' property in %1$s", getIniFile());
                    case ERROR_CONFIG:
                        return String.format("Missing config.ini file in %1$s", mPath);
                    case ERROR_TARGET_HASH:
                        return String.format("Missing 'target' property in %1$s", getIniFile());
                    case ERROR_TARGET:
                        return String.format("Unknown target '%1$s' in %2$s",
                                mTargetHash, getIniFile());
                    case ERROR_PROPERTIES:
                        return String.format("Failed to parse properties from %1$s",
                                getConfigFile());
                    case ERROR_IMAGE_DIR:
                        return String.format(
                                "Invalid value in image.sysdir. Run 'android update avd -n %1$s'",
                                mName);
                    case OK:
                        assert false;
                        return null;
                }
            } catch (AndroidLocationException e) {
                return "Unable to get HOME folder.";
            }

            return null;
        }

        /**
         * Returns whether an emulator is currently running the AVD.
         */
        public boolean isRunning() {
            File f = new File(mPath, "userdata-qemu.img.lock");
            return f.isFile();
        }

        /**
         * Compares this object with the specified object for order. Returns a
         * negative integer, zero, or a positive integer as this object is less
         * than, equal to, or greater than the specified object.
         *
         * @param o the Object to be compared.
         * @return a negative integer, zero, or a positive integer as this object is
         *         less than, equal to, or greater than the specified object.
         */
        public int compareTo(AvdInfo o) {
            // first handle possible missing targets (if the AVD failed to load for
            // unresolved targets.
            if (mTarget == null) {
                return +1;
            } else if (o.mTarget == null) {
                return -1;
            }

            // then compare the targets
            int targetDiff = mTarget.compareTo(o.mTarget);

            if (targetDiff == 0) {
                // same target? compare on the avd name
                return mName.compareTo(o.mName);
            }

            return targetDiff;
        }
    }

    private final ArrayList<AvdInfo> mAllAvdList = new ArrayList<AvdInfo>();
    private AvdInfo[] mValidAvdList;
    private AvdInfo[] mBrokenAvdList;
    private final SdkManager mSdkManager;

    /**
     * Returns the base folder where AVDs are created.
     *
     * @throws AndroidLocationException
     */
    public static String getBaseAvdFolder() throws AndroidLocationException {
        return AndroidLocation.getFolder() + AndroidLocation.FOLDER_AVD;
    }

    /**
     * Creates an AVD Manager for a given SDK represented by a {@link SdkManager}.
     * @param sdkManager The SDK.
     * @param log The log object to receive the log of the initial loading of the AVDs.
     *            This log object is not kept by this instance of AvdManager and each
     *            method takes its own logger. The rationale is that the AvdManager
     *            might be called from a variety of context, each with different
     *            logging needs. Cannot be null.
     * @throws AndroidLocationException
     */
    public AvdManager(SdkManager sdkManager, ISdkLog log) throws AndroidLocationException {
        mSdkManager = sdkManager;
        buildAvdList(mAllAvdList, log);
    }

    /**
     * Returns the {@link SdkManager} associated with the {@link AvdManager}.
     */
    public SdkManager getSdkManager() {
        return mSdkManager;
    }

    /**
     * Returns all the existing AVDs.
     * @return a newly allocated array containing all the AVDs.
     */
    public AvdInfo[] getAllAvds() {
        synchronized (mAllAvdList) {
            return mAllAvdList.toArray(new AvdInfo[mAllAvdList.size()]);
        }
    }

    /**
     * Returns all the valid AVDs.
     * @return a newly allocated array containing all valid the AVDs.
     */
    public AvdInfo[] getValidAvds() {
        synchronized (mAllAvdList) {
            if (mValidAvdList == null) {
                ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
                for (AvdInfo avd : mAllAvdList) {
                    if (avd.getStatus() == AvdStatus.OK) {
                        list.add(avd);
                    }
                }

                mValidAvdList = list.toArray(new AvdInfo[list.size()]);
            }
            return mValidAvdList;
        }
    }

    /**
     * Returns all the broken AVDs.
     * @return a newly allocated array containing all the broken AVDs.
     */
    public AvdInfo[] getBrokenAvds() {
        synchronized (mAllAvdList) {
            if (mBrokenAvdList == null) {
                ArrayList<AvdInfo> list = new ArrayList<AvdInfo>();
                for (AvdInfo avd : mAllAvdList) {
                    if (avd.getStatus() != AvdStatus.OK) {
                        list.add(avd);
                    }
                }
                mBrokenAvdList = list.toArray(new AvdInfo[list.size()]);
            }
            return mBrokenAvdList;
        }
    }

    /**
     * Returns the {@link AvdInfo} matching the given <var>name</var>.
     * <p/>
     * The search is case-insensitive.
     *
     * @param name the name of the AVD to return
     * @param validAvdOnly if <code>true</code>, only look through the list of valid AVDs.
     * @return the matching AvdInfo or <code>null</code> if none were found.
     */
    public AvdInfo getAvd(String name, boolean validAvdOnly) {

        boolean ignoreCase = SdkConstants.currentPlatform() == SdkConstants.PLATFORM_WINDOWS;

        if (validAvdOnly) {
            for (AvdInfo info : getValidAvds()) {
                String name2 = info.getName();
                if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
                    return info;
                }
            }
        } else {
            synchronized (mAllAvdList) {
                for (AvdInfo info : mAllAvdList) {
                    String name2 = info.getName();
                    if (name2.equals(name) || (ignoreCase && name2.equalsIgnoreCase(name))) {
                        return info;
                    }
                }
            }
        }

        return null;
    }

    /**
     * Reloads the AVD list.
     * @param log the log object to receive action logs. Cannot be null.
     * @throws AndroidLocationException if there was an error finding the location of the
     * AVD folder.
     */
    public void reloadAvds(ISdkLog log) throws AndroidLocationException {
        // build the list in a temp list first, in case the method throws an exception.
        // It's better than deleting the whole list before reading the new one.
        ArrayList<AvdInfo> allList = new ArrayList<AvdInfo>();
        buildAvdList(allList, log);

        synchronized (mAllAvdList) {
            mAllAvdList.clear();
            mAllAvdList.addAll(allList);
            mValidAvdList = mBrokenAvdList = null;
        }
    }

    /**
     * Creates a new AVD. It is expected that there is no existing AVD with this name already.
     *
     * @param avdFolder the data folder for the AVD. It will be created as needed.
     * @param name the name of the AVD
     * @param target the target of the AVD
     * @param skinName the name of the skin. Can be null. Must have been verified by caller.
     * @param sdcard the parameter value for the sdCard. Can be null. This is either a path to
     *        an existing sdcard image or a sdcard size (\d+, \d+K, \dM).
     * @param hardwareConfig the hardware setup for the AVD. Can be null to use defaults.
     * @param removePrevious If true remove any previous files.
     * @param log the log object to receive action logs. Cannot be null.
     * @return The new {@link AvdInfo} in case of success (which has just been added to the
     *         internal list) or null in case of failure.
     */
    public AvdInfo createAvd(File avdFolder, String name, IAndroidTarget target,
            String skinName, String sdcard, Map<String,String> hardwareConfig,
            boolean removePrevious, ISdkLog log) {
        if (log == null) {
            throw new IllegalArgumentException("log cannot be null");
        }

        File iniFile = null;
        boolean needCleanup = false;
        try {
            if (avdFolder.exists()) {
                if (removePrevious) {
                    // AVD already exists and removePrevious is set, try to remove the
                    // directory's content first (but not the directory itself).
                    try {
                        deleteContentOf(avdFolder);
                    } catch (SecurityException e) {
                        log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
                    }
                } else {
                    // AVD shouldn't already exist if removePrevious is false.
                    log.error(null,
                            "Folder %1$s is in the way. Use --force if you want to overwrite.",
                            avdFolder.getAbsolutePath());
                    return null;
                }
            } else {
                // create the AVD folder.
                avdFolder.mkdir();
            }

            // actually write the ini file
            iniFile = createAvdIniFile(name, avdFolder, target);

            // writes the userdata.img in it.
            String imagePath = target.getPath(IAndroidTarget.IMAGES);
            File userdataSrc = new File(imagePath, USERDATA_IMG);

            if (userdataSrc.exists() == false && target.isPlatform() == false) {
                imagePath = target.getParent().getPath(IAndroidTarget.IMAGES);
                userdataSrc = new File(imagePath, USERDATA_IMG);
            }

            if (userdataSrc.exists() == false) {
                log.error(null, "Unable to find a '%1$s' file to copy into the AVD folder.",
                        USERDATA_IMG);
                needCleanup = true;
                return null;
            }

            FileInputStream fis = new FileInputStream(userdataSrc);

            File userdataDest = new File(avdFolder, USERDATA_IMG);
            FileOutputStream fos = new FileOutputStream(userdataDest);

            byte[] buffer = new byte[4096];
            int count;
            while ((count = fis.read(buffer)) != -1) {
                fos.write(buffer, 0, count);
            }

            fos.close();
            fis.close();

            // Config file.
            HashMap<String, String> values = new HashMap<String, String>();

            if (setImagePathProperties(target, values, log) == false) {
                needCleanup = true;
                return null;
            }

            // Now the skin.
            if (skinName == null || skinName.length() == 0) {
                skinName = target.getDefaultSkin();
            }

            if (NUMERIC_SKIN_SIZE.matcher(skinName).matches()) {
                // Skin name is an actual screen resolution.
                // Set skin.name for display purposes in the AVD manager and
                // set skin.path for use by the emulator.
                values.put(AVD_INI_SKIN_NAME, skinName);
                values.put(AVD_INI_SKIN_PATH, skinName);
            } else {
                // get the path of the skin (relative to the SDK)
                // assume skin name is valid
                String skinPath = getSkinRelativePath(skinName, target, log);
                if (skinPath == null) {
                    needCleanup = true;
                    return null;
                }

                values.put(AVD_INI_SKIN_PATH, skinPath);
                values.put(AVD_INI_SKIN_NAME, skinName);
            }

            if (sdcard != null && sdcard.length() > 0) {
                File sdcardFile = new File(sdcard);
                if (sdcardFile.isFile()) {
                    // sdcard value is an external sdcard, so we put its path into the config.ini
                    values.put(AVD_INI_SDCARD_PATH, sdcard);
                } else {
                    // Sdcard is possibly a size. In that case we create a file called 'sdcard.img'
                    // in the AVD folder, and do not put any value in config.ini.

                    // First, check that it matches the pattern for sdcard size
                    Matcher m = SDCARD_SIZE_PATTERN.matcher(sdcard);
                    if (m.matches()) {
                        // get the sdcard values for checks
                        try {
                            long sdcardSize = Long.parseLong(m.group(1));

                            // prevent overflow: no more than 999GB
                            // 10 digit for MiB, 13 for KiB
                            int digitCount = m.group(1).length();

                            String sdcardSizeModifier = m.group(2);
                            if ("K".equals(sdcardSizeModifier)) {
                                sdcardSize *= 1024L;
                            } else { // must be "M" per the pattern
                                sdcardSize *= 1024L * 1024L;
                                digitCount += 3; // convert the number of digit into "KiB"
                            }

                            if (digitCount >= 13) {
                                log.error(null, "SD Card size is too big!");
                                needCleanup = true;
                                return null;
                            }

                            if (sdcardSize < 9 * 1024 * 1024) {
                                log.error(null, "SD Card size must be at least 9MB");
                                needCleanup = true;
                                return null;
                            }
                        } catch (NumberFormatException e) {
                            // this should never happen since the string is validated
                            // by the regexp
                            log.error(null, "Unable to parse SD Card size");
                            needCleanup = true;
                            return null;
                        }

                        // create the sdcard.
                        sdcardFile = new File(avdFolder, SDCARD_IMG);
                        String path = sdcardFile.getAbsolutePath();

                        // execute mksdcard with the proper parameters.
                        File toolsFolder = new File(mSdkManager.getLocation(),
                                SdkConstants.FD_TOOLS);
                        File mkSdCard = new File(toolsFolder, SdkConstants.mkSdCardCmdName());

                        if (mkSdCard.isFile() == false) {
                            log.error(null, "'%1$s' is missing from the SDK tools folder.",
                                    mkSdCard.getName());
                            needCleanup = true;
                            return null;
                        }

                        if (createSdCard(mkSdCard.getAbsolutePath(), sdcard, path, log) == false) {
                            needCleanup = true;
                            return null; // mksdcard output has already been displayed, no need to
                                         // output anything else.
                        }

                        // add a property containing the size of the sdcard for display purpose
                        // only when the dev does 'android list avd'
                        values.put(AVD_INI_SDCARD_SIZE, sdcard);
                    } else {
                        log.error(null, "'%1$s' is not recognized as a valid sdcard value.\n"
                                + "Value should be:\n" + "1. path to an sdcard.\n"
                                + "2. size of the sdcard to create: <size>[K|M]", sdcard);
                        needCleanup = true;
                        return null;
                    }
                }
            }

            // add the hardware config to the config file.
            // priority order is:
            // - values provided by the user
            // - values provided by the skin
            // - values provided by the target (add-on only).
            // In order to follow this priority, we'll add the lowest priority values first and then
            // override by higher priority values.
            // In the case of a platform with override values from the user, the skin value might
            // already be there, but it's ok.

            HashMap<String, String> finalHardwareValues = new HashMap<String, String>();

            File targetHardwareFile = new File(target.getLocation(), AvdManager.HARDWARE_INI);
            if (targetHardwareFile.isFile()) {
                Map<String, String> targetHardwareConfig = ProjectProperties.parsePropertyFile(
                        targetHardwareFile, log);
                if (targetHardwareConfig != null) {
                    finalHardwareValues.putAll(targetHardwareConfig);
                    values.putAll(targetHardwareConfig);
                }
            }

            // get the hardware properties for this skin
            File skinFolder = getSkinPath(skinName, target);
            File skinHardwareFile = new File(skinFolder, AvdManager.HARDWARE_INI);
            if (skinHardwareFile.isFile()) {
                Map<String, String> skinHardwareConfig = ProjectProperties.parsePropertyFile(
                        skinHardwareFile, log);
                if (skinHardwareConfig != null) {
                    finalHardwareValues.putAll(skinHardwareConfig);
                    values.putAll(skinHardwareConfig);
                }
            }

            // finally put the hardware provided by the user.
            if (hardwareConfig != null) {
                finalHardwareValues.putAll(hardwareConfig);
                values.putAll(hardwareConfig);
            }

            File configIniFile = new File(avdFolder, CONFIG_INI);
            writeIniFile(configIniFile, values);

            // Generate the log report first because we want to control where line breaks
            // are located when generating the hardware config list.
            StringBuilder report = new StringBuilder();

            if (target.isPlatform()) {
                report.append(String.format("Created AVD '%1$s' based on %2$s",
                        name, target.getName()));
            } else {
                report.append(String.format("Created AVD '%1$s' based on %2$s (%3$s)", name,
                        target.getName(), target.getVendor()));
            }

            // display the chosen hardware config
            if (finalHardwareValues.size() > 0) {
                report.append(",\nwith the following hardware config:\n");
                for (Entry<String, String> entry : finalHardwareValues.entrySet()) {
                    report.append(String.format("%s=%s\n",entry.getKey(), entry.getValue()));
                }
            } else {
                report.append("\n");
            }

            log.printf(report.toString());

            // create the AvdInfo object, and add it to the list
            AvdInfo newAvdInfo = new AvdInfo(name,
                    avdFolder.getAbsolutePath(),
                    target.hashString(),
                    target, values);

            AvdInfo oldAvdInfo = getAvd(name, false /*validAvdOnly*/);

            synchronized (mAllAvdList) {
                if (oldAvdInfo != null && removePrevious) {
                    mAllAvdList.remove(oldAvdInfo);
                }
                mAllAvdList.add(newAvdInfo);
                mValidAvdList = mBrokenAvdList = null;
            }

            if (removePrevious &&
                    newAvdInfo != null &&
                    oldAvdInfo != null &&
                    !oldAvdInfo.getPath().equals(newAvdInfo.getPath())) {
                log.warning("Removing previous AVD directory at %s", oldAvdInfo.getPath());
                // Remove the old data directory
                File dir = new File(oldAvdInfo.getPath());
                try {
                    deleteContentOf(dir);
                    dir.delete();
                } catch (SecurityException e) {
                    log.error(e, "Failed to delete %1$s", dir.getAbsolutePath());
                }
            }

            return newAvdInfo;
        } catch (AndroidLocationException e) {
            log.error(e, null);
        } catch (IOException e) {
            log.error(e, null);
        } catch (SecurityException e) {
            log.error(e, null);
        } finally {
            if (needCleanup) {
                if (iniFile != null && iniFile.exists()) {
                    iniFile.delete();
                }

                try {
                    deleteContentOf(avdFolder);
                    avdFolder.delete();
                } catch (SecurityException e) {
                    log.error(e, "Failed to delete %1$s", avdFolder.getAbsolutePath());
                }
            }
        }

        return null;
    }

    /**
     * Returns the path to the target images folder as a relative path to the SDK, if the folder
     * is not empty. If the image folder is empty or does not exist, <code>null</code> is returned.
     * @throws InvalidTargetPathException if the target image folder is not in the current SDK.
     */
    private String getImageRelativePath(IAndroidTarget target)
            throws InvalidTargetPathException {
        String imageFullPath = target.getPath(IAndroidTarget.IMAGES);

        // make this path relative to the SDK location
        String sdkLocation = mSdkManager.getLocation();
        if (imageFullPath.startsWith(sdkLocation) == false) {
            // this really really should not happen.
            assert false;
            throw new InvalidTargetPathException("Target location is not inside the SDK.");
        }

        File folder = new File(imageFullPath);
        if (folder.isDirectory()) {
            String[] list = folder.list(new FilenameFilter() {
                public boolean accept(File dir, String name) {
                    return IMAGE_NAME_PATTERN.matcher(name).matches();
                }
            });

            if (list.length > 0) {
                imageFullPath = imageFullPath.substring(sdkLocation.length());
                if (imageFullPath.charAt(0) == File.separatorChar) {
                    imageFullPath = imageFullPath.substring(1);
                }

                return imageFullPath;
            }
        }

        return null;
    }

    /**
     * Returns the path to the skin, as a relative path to the SDK.
     * @param skinName The name of the skin to find. Case-sensitive.
     * @param target The target where to find the skin.
     * @param log the log object to receive action logs. Cannot be null.
     */
    public String getSkinRelativePath(String skinName, IAndroidTarget target, ISdkLog log) {
        if (log == null) {
            throw new IllegalArgumentException("log cannot be null");
        }

        // first look to see if the skin is in the target
        File skin = getSkinPath(skinName, target);

        // skin really does not exist!
        if (skin.exists() == false) {
            log.error(null, "Skin '%1$s' does not exist.", skinName);
            return null;
        }

        // get the skin path
        String path = skin.getAbsolutePath();

        // make this path relative to the SDK location
        String sdkLocation = mSdkManager.getLocation();
        if (path.startsWith(sdkLocation) == false) {
            // this really really should not happen.
            log.error(null, "Target location is not inside the SDK.");
            assert false;
            return null;
        }

        path = path.substring(sdkLocation.length());
        if (path.charAt(0) == File.separatorChar) {
            path = path.substring(1);
        }
        return path;
    }

    /**
     * Returns the full absolute OS path to a skin specified by name for a given target.
     * @param skinName The name of the skin to find. Case-sensitive.
     * @param target The target where to find the skin.
     * @return a {@link File} that may or may not actually exist.
     */
    public File getSkinPath(String skinName, IAndroidTarget target) {
        String path = target.getPath(IAndroidTarget.SKINS);
        File skin = new File(path, skinName);

        if (skin.exists() == false && target.isPlatform() == false) {
            target = target.getParent();

            path = target.getPath(IAndroidTarget.SKINS);
            skin = new File(path, skinName);
        }

        return skin;
    }

    /**
     * Creates the ini file for an AVD.
     *
     * @param name of the AVD.
     * @param avdFolder path for the data folder of the AVD.
     * @param target of the AVD.
     * @throws AndroidLocationException if there's a problem getting android root directory.
     * @throws IOException if {@link File#getAbsolutePath()} fails.
     */
    private File createAvdIniFile(String name, File avdFolder, IAndroidTarget target)
            throws AndroidLocationException, IOException {
        HashMap<String, String> values = new HashMap<String, String>();
        File iniFile = AvdInfo.getIniFile(name);
        values.put(AVD_INFO_PATH, avdFolder.getAbsolutePath());
        values.put(AVD_INFO_TARGET, target.hashString());
        writeIniFile(iniFile, values);

        return iniFile;
    }

    /**
     * Creates the ini file for an AVD.
     *
     * @param info of the AVD.
     * @throws AndroidLocationException if there's a problem getting android root directory.
     * @throws IOException if {@link File#getAbsolutePath()} fails.
     */
    private File createAvdIniFile(AvdInfo info) throws AndroidLocationException, IOException {
        return createAvdIniFile(info.getName(), new File(info.getPath()), info.getTarget());
    }

    /**
     * Actually deletes the files of an existing AVD.
     * <p/>
     * This also remove it from the manager's list, The caller does not need to
     * call {@link #removeAvd(AvdInfo)} afterwards.
     * <p/>
     * This method is designed to somehow work with an unavailable AVD, that is an AVD that
     * could not be loaded due to some error. That means this method still tries to remove
     * the AVD ini file or its folder if it can be found. An error will be output if any of
     * these operations fail.
     *
     * @param avdInfo the information on the AVD to delete
     * @param log the log object to receive action logs. Cannot be null.
     * @return True if the AVD was deleted with no error.
     */
    public boolean deleteAvd(AvdInfo avdInfo, ISdkLog log) {
        try {
            boolean error = false;

            File f = avdInfo.getIniFile();
            if (f != null && f.exists()) {
                log.printf("Deleting file %1$s\n", f.getCanonicalPath());
                if (!f.delete()) {
                    log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
                    error = true;
                }
            }

            String path = avdInfo.getPath();
            if (path != null) {
                f = new File(path);
                if (f.exists()) {
                    log.printf("Deleting folder %1$s\n", f.getCanonicalPath());
                    if (deleteContentOf(f) == false || f.delete() == false) {
                        log.error(null, "Failed to delete %1$s\n", f.getCanonicalPath());
                        error = true;
                    }
                }
            }

            removeAvd(avdInfo);

            if (error) {
                log.printf("\nAVD '%1$s' deleted with errors. See errors above.\n",
                        avdInfo.getName());
            } else {
                log.printf("\nAVD '%1$s' deleted.\n", avdInfo.getName());
                return true;
            }

        } catch (AndroidLocationException e) {
            log.error(e, null);
        } catch (IOException e) {
            log.error(e, null);
        } catch (SecurityException e) {
            log.error(e, null);
        }
        return false;
    }

    /**
     * Moves and/or rename an existing AVD and its files.
     * This also change it in the manager's list.
     * <p/>
     * The caller should make sure the name or path given are valid, do not exist and are
     * actually different than current values.
     *
     * @param avdInfo the information on the AVD to move.
     * @param newName the new name of the AVD if non null.
     * @param paramFolderPath the new data folder if non null.
     * @param log the log object to receive action logs. Cannot be null.
     * @return True if the move succeeded or there was nothing to do.
     *         If false, this method will have had already output error in the log.
     */
    public boolean moveAvd(AvdInfo avdInfo, String newName, String paramFolderPath, ISdkLog log) {

        try {
            if (paramFolderPath != null) {
                File f = new File(avdInfo.getPath());
                log.warning("Moving '%1$s' to '%2$s'.", avdInfo.getPath(), paramFolderPath);
                if (!f.renameTo(new File(paramFolderPath))) {
                    log.error(null, "Failed to move '%1$s' to '%2$s'.",
                            avdInfo.getPath(), paramFolderPath);
                    return false;
                }

                // update AVD info
                AvdInfo info = new AvdInfo(avdInfo.getName(), paramFolderPath,
                        avdInfo.getTargetHash(), avdInfo.getTarget(), avdInfo.getProperties());
                replaceAvd(avdInfo, info);

                // update the ini file
                createAvdIniFile(info);
            }

            if (newName != null) {
                File oldIniFile = avdInfo.getIniFile();
                File newIniFile = AvdInfo.getIniFile(newName);

                log.warning("Moving '%1$s' to '%2$s'.", oldIniFile.getPath(), newIniFile.getPath());
                if (!oldIniFile.renameTo(newIniFile)) {
                    log.error(null, "Failed to move '%1$s' to '%2$s'.",
                            oldIniFile.getPath(), newIniFile.getPath());
                    return false;
                }

                // update AVD info
                AvdInfo info = new AvdInfo(newName, avdInfo.getPath(),
                        avdInfo.getTargetHash(), avdInfo.getTarget(), avdInfo.getProperties());
                replaceAvd(avdInfo, info);
            }

            log.printf("AVD '%1$s' moved.\n", avdInfo.getName());

        } catch (AndroidLocationException e) {
            log.error(e, null);
        } catch (IOException e) {
            log.error(e, null);
        }

        // nothing to do or succeeded
        return true;
    }

    /**
     * Helper method to recursively delete a folder's content (but not the folder itself).
     *
     * @throws SecurityException like {@link File#delete()} does if file/folder is not writable.
     */
    private boolean deleteContentOf(File folder) throws SecurityException {
        for (File f : folder.listFiles()) {
            if (f.isDirectory()) {
                if (deleteContentOf(f) == false) {
                    return false;
                }
            }
            if (f.delete() == false) {
                return false;
            }

        }

        return true;
    }

    /**
     * Returns a list of files that are potential AVD ini files.
     * <p/>
     * This lists the $HOME/.android/avd/<name>.ini files.
     * Such files are properties file than then indicate where the AVD folder is located.
     *
     * @return A new {@link File} array or null. The array might be empty.
     * @throws AndroidLocationException if there's a problem getting android root directory.
     */
    private File[] buildAvdFilesList() throws AndroidLocationException {
        // get the Android prefs location.
        String avdRoot = AvdManager.getBaseAvdFolder();

        // ensure folder validity.
        File folder = new File(avdRoot);
        if (folder.isFile()) {
            throw new AndroidLocationException(
                    String.format("%1$s is not a valid folder.", avdRoot));
        } else if (folder.exists() == false) {
            // folder is not there, we create it and return
            folder.mkdirs();
            return null;
        }

        File[] avds = folder.listFiles(new FilenameFilter() {
            public boolean accept(File parent, String name) {
                if (INI_NAME_PATTERN.matcher(name).matches()) {
                    // check it's a file and not a folder
                    boolean isFile = new File(parent, name).isFile();
                    return isFile;
                }

                return false;
            }
        });

        return avds;
    }

    /**
     * Computes the internal list of available AVDs
     * @param allList the list to contain all the AVDs
     * @param log the log object to receive action logs. Cannot be null.
     *
     * @throws AndroidLocationException if there's a problem getting android root directory.
     */
    private void buildAvdList(ArrayList<AvdInfo> allList, ISdkLog log)
            throws AndroidLocationException {
        File[] avds = buildAvdFilesList();
        if (avds != null) {
            for (File avd : avds) {
                AvdInfo info = parseAvdInfo(avd, log);
                if (info != null) {
                    allList.add(info);
                }
            }
        }
    }

    /**
     * Parses an AVD .ini file to create an {@link AvdInfo}.
     *
     * @param path The path to the AVD .ini file
     * @param log the log object to receive action logs. Cannot be null.
     * @return A new {@link AvdInfo} with an {@link AvdStatus} indicating whether this AVD is
     *         valid or not.
     */
    private AvdInfo parseAvdInfo(File path, ISdkLog log) {
        Map<String, String> map = ProjectProperties.parsePropertyFile(path, log);

        String avdPath = map.get(AVD_INFO_PATH);
        String targetHash = map.get(AVD_INFO_TARGET);

        IAndroidTarget target = null;
        File configIniFile = null;
        Map<String, String> properties = null;

        if (targetHash != null) {
            target = mSdkManager.getTargetFromHashString(targetHash);
        }

        // load the AVD properties.
        if (avdPath != null) {
            configIniFile = new File(avdPath, CONFIG_INI);
        }

        if (configIniFile != null) {
            if (!configIniFile.isFile()) {
                log.warning("Missing file '%1$s'.",  configIniFile.getPath());
            } else {
                properties = ProjectProperties.parsePropertyFile(configIniFile, log);
            }
        }

        // get name
        String name = path.getName();
        Matcher matcher = INI_NAME_PATTERN.matcher(path.getName());
        if (matcher.matches()) {
            name = matcher.group(1);
        }

        // check the image.sysdir are valid
        boolean validImageSysdir = true;
        if (properties != null) {
            String imageSysDir = properties.get(AVD_INI_IMAGES_1);
            if (imageSysDir != null) {
                File f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
                if (f.isDirectory() == false) {
                    validImageSysdir = false;
                } else {
                    imageSysDir = properties.get(AVD_INI_IMAGES_2);
                    if (imageSysDir != null) {
                        f = new File(mSdkManager.getLocation() + File.separator + imageSysDir);
                        if (f.isDirectory() == false) {
                            validImageSysdir = false;
                        }
                    }
                }
            }
        }

        AvdStatus status;

        if (avdPath == null) {
            status = AvdStatus.ERROR_PATH;
        } else if (configIniFile == null) {
            status = AvdStatus.ERROR_CONFIG;
        } else if (targetHash == null) {
            status = AvdStatus.ERROR_TARGET_HASH;
        } else if (target == null) {
            status = AvdStatus.ERROR_TARGET;
        } else if (properties == null) {
            status = AvdStatus.ERROR_PROPERTIES;
        } else if (validImageSysdir == false) {
            status = AvdStatus.ERROR_IMAGE_DIR;
        } else {
            status = AvdStatus.OK;
        }

        AvdInfo info = new AvdInfo(
                name,
                avdPath,
                targetHash,
                target,
                properties,
                status);

        return info;
    }

    /**
     * Writes a .ini file from a set of properties, using UTF-8 encoding.
     *
     * @param iniFile The file to generate.
     * @param values THe properties to place in the ini file.
     * @throws IOException if {@link FileWriter} fails to open, write or close the file.
     */
    private static void writeIniFile(File iniFile, Map<String, String> values)
            throws IOException {
        OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(iniFile),
                SdkConstants.INI_CHARSET);

        for (Entry<String, String> entry : values.entrySet()) {
            writer.write(String.format("%1$s=%2$s\n", entry.getKey(), entry.getValue()));
        }
        writer.close();
    }

    /**
     * Invokes the tool to create a new SD card image file.
     *
     * @param toolLocation The path to the mksdcard tool.
     * @param size The size of the new SD Card, compatible with {@link #SDCARD_SIZE_PATTERN}.
     * @param location The path of the new sdcard image file to generate.
     * @param log the log object to receive action logs. Cannot be null.
     * @return True if the sdcard could be created.
     */
    private boolean createSdCard(String toolLocation, String size, String location, ISdkLog log) {
        try {
            String[] command = new String[3];
            command[0] = toolLocation;
            command[1] = size;
            command[2] = location;
            Process process = Runtime.getRuntime().exec(command);

            ArrayList<String> errorOutput = new ArrayList<String>();
            ArrayList<String> stdOutput = new ArrayList<String>();
            int status = grabProcessOutput(process, errorOutput, stdOutput,
                    true /* waitForReaders */);

            if (status == 0) {
                return true;
            } else {
                for (String error : errorOutput) {
                    log.error(null, error);
                }
            }

        } catch (InterruptedException e) {
            // pass, print error below
        } catch (IOException e) {
            // pass, print error below
        }

        log.error(null, "Failed to create the SD card.");
        return false;
    }

    /**
     * Gets the stderr/stdout outputs of a process and returns when the process is done.
     * Both <b>must</b> be read or the process will block on windows.
     * @param process The process to get the ouput from
     * @param errorOutput The array to store the stderr output. cannot be null.
     * @param stdOutput The array to store the stdout output. cannot be null.
     * @param waitforReaders if true, this will wait for the reader threads.
     * @return the process return code.
     * @throws InterruptedException
     */
    private int grabProcessOutput(final Process process, final ArrayList<String> errorOutput,
            final ArrayList<String> stdOutput, boolean waitforReaders)
            throws InterruptedException {
        assert errorOutput != null;
        assert stdOutput != null;
        // read the lines as they come. if null is returned, it's
        // because the process finished
        Thread t1 = new Thread("") { //$NON-NLS-1$
            @Override
            public void run() {
                // create a buffer to read the stderr output
                InputStreamReader is = new InputStreamReader(process.getErrorStream());
                BufferedReader errReader = new BufferedReader(is);

                try {
                    while (true) {
                        String line = errReader.readLine();
                        if (line != null) {
                            errorOutput.add(line);
                        } else {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                }
            }
        };

        Thread t2 = new Thread("") { //$NON-NLS-1$
            @Override
            public void run() {
                InputStreamReader is = new InputStreamReader(process.getInputStream());
                BufferedReader outReader = new BufferedReader(is);

                try {
                    while (true) {
                        String line = outReader.readLine();
                        if (line != null) {
                            stdOutput.add(line);
                        } else {
                            break;
                        }
                    }
                } catch (IOException e) {
                    // do nothing.
                }
            }
        };

        t1.start();
        t2.start();

        // it looks like on windows process#waitFor() can return
        // before the thread have filled the arrays, so we wait for both threads and the
        // process itself.
        if (waitforReaders) {
            try {
                t1.join();
            } catch (InterruptedException e) {
                // nothing to do here
            }
            try {
                t2.join();
            } catch (InterruptedException e) {
                // nothing to do here
            }
        }

        // get the return code from the process
        return process.waitFor();
    }

    /**
     * Removes an {@link AvdInfo} from the internal list.
     *
     * @param avdInfo The {@link AvdInfo} to remove.
     * @return true if this {@link AvdInfo} was present and has been removed.
     */
    public boolean removeAvd(AvdInfo avdInfo) {
        synchronized (mAllAvdList) {
            if (mAllAvdList.remove(avdInfo)) {
                mValidAvdList = mBrokenAvdList = null;
                return true;
            }
        }

        return false;
    }

    /**
     * Updates an AVD with new path to the system image folders.
     * @param name the name of the AVD to update.
     * @param log the log object to receive action logs. Cannot be null.
     * @throws IOException
     */
    public void updateAvd(String name, ISdkLog log) throws IOException {
        // find the AVD to update. It should be be in the broken list.
        AvdInfo avd = null;
        synchronized (mAllAvdList) {
            for (AvdInfo info : mAllAvdList) {
                if (info.getName().equals(name)) {
                    avd = info;
                    break;
                }
            }
        }

        if (avd == null) {
            // not in the broken list, just return.
            log.error(null, "There is no Android Virtual Device named '%s'.", name);
            return;
        }

        updateAvd(avd, log);
    }


    /**
     * Updates an AVD with new path to the system image folders.
     * @param avd the AVD to update.
     * @param log the log object to receive action logs. Cannot be null.
     * @throws IOException
     */
    public void updateAvd(AvdInfo avd, ISdkLog log) throws IOException {
        // get the properties. This is a unmodifiable Map.
        Map<String, String> oldProperties = avd.getProperties();

        // create a new map
        Map<String, String> properties = new HashMap<String, String>();
        if (oldProperties != null) {
            properties.putAll(oldProperties);
        }

        AvdStatus status;

        // create the path to the new system images.
        if (setImagePathProperties(avd.getTarget(), properties, log)) {
            if (properties.containsKey(AVD_INI_IMAGES_1)) {
                log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_1,
                        properties.get(AVD_INI_IMAGES_1));
            }

            if (properties.containsKey(AVD_INI_IMAGES_2)) {
                log.printf("Updated '%1$s' with value '%2$s'\n", AVD_INI_IMAGES_2,
                        properties.get(AVD_INI_IMAGES_2));
            }

            status = AvdStatus.OK;
        } else {
            log.error(null, "Unable to find non empty system images folders for %1$s",
                    avd.getName());
            //FIXME: display paths to empty image folders?
            status = AvdStatus.ERROR_IMAGE_DIR;
        }

        // now write the config file
        File configIniFile = new File(avd.getPath(), CONFIG_INI);
        writeIniFile(configIniFile, properties);

        // finally create a new AvdInfo for this unbroken avd and add it to the list.
        // instead of creating the AvdInfo object directly we reparse it, to detect other possible
        // errors
        // FIXME: We may want to create this AvdInfo by reparsing the AVD instead. This could detect other errors.
        AvdInfo newAvd = new AvdInfo(
                avd.getName(),
                avd.getPath(),
                avd.getTargetHash(),
                avd.getTarget(),
                properties,
                status);

        replaceAvd(avd, newAvd);
    }

    /**
     * Sets the paths to the system images in a properties map.
     * @param target the target in which to find the system images.
     * @param properties the properties in which to set the paths.
     * @param log the log object to receive action logs. Cannot be null.
     * @return true if success, false if some path are missing.
     */
    private boolean setImagePathProperties(IAndroidTarget target,
            Map<String, String> properties,
            ISdkLog log) {
        properties.remove(AVD_INI_IMAGES_1);
        properties.remove(AVD_INI_IMAGES_2);

        try {
            String property = AVD_INI_IMAGES_1;

            // First the image folders of the target itself
            String imagePath = getImageRelativePath(target);
            if (imagePath != null) {
                properties.put(property, imagePath);
                property = AVD_INI_IMAGES_2;
            }


            // If the target is an add-on we need to add the Platform image as a backup.
            IAndroidTarget parent = target.getParent();
            if (parent != null) {
                imagePath = getImageRelativePath(parent);
                if (imagePath != null) {
                    properties.put(property, imagePath);
                }
            }

            // we need at least one path!
            return properties.containsKey(AVD_INI_IMAGES_1);
        } catch (InvalidTargetPathException e) {
            log.error(e, e.getMessage());
        }

        return false;
    }

    /**
     * Replaces an old {@link AvdInfo} with a new one in the lists storing them.
     * @param oldAvd the {@link AvdInfo} to remove.
     * @param newAvd the {@link AvdInfo} to add.
     */
    private void replaceAvd(AvdInfo oldAvd, AvdInfo newAvd) {
        synchronized (mAllAvdList) {
            mAllAvdList.remove(oldAvd);
            mAllAvdList.add(newAvd);
            mValidAvdList = mBrokenAvdList = null;
        }
    }
}
