/*
 * Copyright (C) 2011 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.tradefed.targetprep;

import com.android.ddmlib.FileListingService;
import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

/**
 * A default implementation of tests zip installer.
 */
public class DefaultTestsZipInstaller implements ITestsZipInstaller {
    private static final int RM_ATTEMPTS = 3;
    private static final String DEVICE_DATA_PATH = buildAbsPath(FileListingService.DIRECTORY_DATA);

    /**
     * A list of /data subdirectories to NOT wipe when doing UserDataFlashOption.TESTS_ZIP
     */
    private Set<String> mDataWipeSkipList;

    /**
     * Default constructor.
     */
    public DefaultTestsZipInstaller() {
    }

    /**
     * This convenience constructor allows the caller to set the skip list directly, rather than
     * needing to call {@link #setDataWipeSkipList} separately.
     *
     * @param skipList The collection of paths under {@code /data} to keep when clearing the
     * filesystem @see #setDataWipeSkipList
     */
    public DefaultTestsZipInstaller(Collection<String> skipList) {
        setDataWipeSkipList(skipList);
    }

    /**
     * This convenience constructor allows the caller to set the skip list directly, rather than
     * needing to call {@link #setDataWipeSkipList} separately.
     *
     * @param skipList The collection of paths under {@code /data} to keep when clearing the
     * filesystem @see #setDataWipeSkipList
     */
    public DefaultTestsZipInstaller(String... skipList) {
        setDataWipeSkipList(skipList);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataWipeSkipList(Collection<String> skipList) {
        mDataWipeSkipList = new HashSet<String>(skipList.size());
        mDataWipeSkipList.addAll(skipList);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataWipeSkipList(String... skipList) {
        mDataWipeSkipList = new HashSet<String>(skipList.length);
        mDataWipeSkipList.addAll(Arrays.asList(skipList));
    }

    /**
     * Get the directory of directories to wipe, used for testing only.
     * @return the set of directories to skip when wiping a directory
     */
    public Set<String> getDataWipeSkipList() {
        return mDataWipeSkipList;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation will reboot the device into userland before
     * proceeding. It will also stop the Android runtime and leave it down upon return
     */
    @Override
    public void pushTestsZipOntoData(ITestDevice device, IDeviceBuildInfo deviceBuild)
            throws DeviceNotAvailableException, TargetSetupError {
        CLog.i(String.format("Pushing test zips content onto userdata on %s",
                device.getSerialNumber()));

        RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
        device.setRecoveryMode(RecoveryMode.ONLINE);

        doDeleteData(device);

        CLog.d("Syncing test files/apks");
        File hostDir = new File(deviceBuild.getTestsDir(), "DATA");

        File[] hostDataFiles = getTestsZipDataFiles(hostDir, device);
        for (File hostSubDir : hostDataFiles) {
            device.syncFiles(hostSubDir, DEVICE_DATA_PATH);
        }

        device.executeShellCommand("chown -R system.system " + DEVICE_DATA_PATH);

        device.setRecoveryMode(cachedRecoveryMode);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteData(ITestDevice device) throws DeviceNotAvailableException,
            TargetSetupError {
        RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
        device.setRecoveryMode(RecoveryMode.ONLINE);

        doDeleteData(device);

        device.setRecoveryMode(cachedRecoveryMode);
    }

    /**
     * Deletes userdata from device without toggling {@link RecoveryMode}.
     * <p/>
     * Expects callers to have set device to {@link RecoveryMode#ONLINE}.
     */
    private void doDeleteData(ITestDevice device) throws DeviceNotAvailableException,
            TargetSetupError {
        // Stop the runtime, so it doesn't notice us mucking with the filesystem
        device.executeShellCommand("stop");
        // Stop installd to prevent it from writing to /data/data
        device.executeShellCommand("stop installd");

        CLog.d("clearing " + FileListingService.DIRECTORY_DATA + " directory on device "
                + device.getSerialNumber());

        // Touch a file so that we can make sure the filesystem is mounted and r/w and usable.  If
        // this method is a no-op, then the filesystem might be corrupt and mounted r/o, or might
        // not be mounted at all.
        String turtlePath = buildRelPath(DEVICE_DATA_PATH,
                String.format("turtles-%d.txt", System.currentTimeMillis()));
        boolean yayTurtle = device.pushString("I like turtles", turtlePath);
        if (!yayTurtle) {
            throw new TargetSetupError(String.format("Failed userdata write check on device %s",
                    device.getSerialNumber()), device.getDeviceDescriptor());
        }

        IFileEntry dataEntry = device.getFileEntry(FileListingService.DIRECTORY_DATA);
        if (dataEntry == null) {
            throw new TargetSetupError(String.format("Could not find %s folder on %s",
                    FileListingService.DIRECTORY_DATA, device.getSerialNumber()),
                    device.getDeviceDescriptor());
        }
        for (IFileEntry dataSubDir : dataEntry.getChildren(false)) {
            if (!mDataWipeSkipList.contains(dataSubDir.getName())) {
                deleteDir(device, dataSubDir.getFullEscapedPath());
            }
        }
    }

    /**
     * @param fullEscapedPath
     * @throws DeviceNotAvailableException
     * @throws TargetSetupError
     */
    private void deleteDir(ITestDevice device, String fullEscapedPath)
            throws DeviceNotAvailableException, TargetSetupError {
        for (int i = 1; i <= RM_ATTEMPTS; i++) {
            device.deleteFile(fullEscapedPath);
            if (!device.doesFileExist(fullEscapedPath)) {
                return;
            }
            CLog.d(
                    "Failed to delete dir %s on device %s on attempt %d of %d.",
                    fullEscapedPath, device.getSerialNumber(), i, RM_ATTEMPTS);
            // do exponential backoff
            getRunUtil().sleep(1000 * i * i);
        }
        throw new TargetSetupError(
                String.format("Failed to delete dir %s.", fullEscapedPath),
                device.getDeviceDescriptor());
    }

    /**
     * Get the {@link IRunUtil} object to use.
     * <p/>
     * Exposed so unit tests can mock.
     */
    IRunUtil getRunUtil() {
        return RunUtil.getDefault();
    }

    private static String buildRelPath(String... parts) {
        return ArrayUtil.join(FileListingService.FILE_SEPARATOR, (Object[]) parts);
    }

    private static String buildAbsPath(String... parts) {
        return FileListingService.FILE_SEPARATOR + buildRelPath(parts);
    }

    /**
     * Retrieves the set of files contained in given tests.zip/DATA directory.
     * <p/>
     * Exposed so unit tests can mock.
     *
     * @param hostDir the {@link File} directory, representing the local path extracted tests.zip
     *            contents 'DATA' sub-folder
     * @return array of {@link File}
     */
    File[] getTestsZipDataFiles(File hostDir, ITestDevice device) throws TargetSetupError {
        if (!hostDir.isDirectory()) {
            throw new TargetSetupError("Unrecognized tests.zip content: missing DATA folder",
                    device.getDeviceDescriptor());
        }
        File[] childFiles = hostDir.listFiles();
        if (childFiles == null || childFiles.length <= 0) {
            throw new TargetSetupError(
                    "Unrecognized tests.zip content: DATA folder has no content",
                    device.getDeviceDescriptor());
        }
        return childFiles;
    }
}
