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

import com.android.annotations.VisibleForTesting;
import com.android.tradefed.build.IBuildInfo.BuildInfoProperties;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.StubDevice;
import com.android.tradefed.invoker.ExecutionFiles;
import com.android.tradefed.invoker.ExecutionFiles.FilesKey;
import com.android.tradefed.invoker.logger.CurrentInvocation;
import com.android.tradefed.invoker.logger.CurrentInvocation.InvocationInfo;
import com.android.tradefed.invoker.tracing.CloseableTraceScope;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.util.BuildInfoUtil;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.SystemUtil;

import java.io.File;
import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Map.Entry;

/**
 * A {@link IDeviceBuildProvider} that bootstraps build info from the test device
 *
 * <p>
 * This is typically used for devices with an externally supplied build, i.e. not generated by
 * in-house build system. Certain information, specifically the branch, is not actually available
 * from the device, therefore it's artificially generated.
 *
 * <p>All build meta data info comes from various ro.* property fields on device
 *
 * <p>Currently this build provider generates meta data as follows:
 * <ul>
 * <li>branch:
 * $(ro.product.brand)-$(ro.product.name)-$(ro.product.device)-$(ro.build.version.release),
 * for example:
 * <ul>
 *   <li>for Google Play edition Samsung S4 running Android 4.2: samsung-jgedlteue-jgedlte-4.2
 *   <li>for Nexus 7 running Android 4.2: google-nakasi-grouper-4.2
 * </ul>
 * <li>build flavor: as provided by {@link ITestDevice#getBuildFlavor()}
 * <li>build alias: as provided by {@link ITestDevice#getBuildAlias()}
 * <li>build id: as provided by {@link ITestDevice#getBuildId()}
 */
@OptionClass(alias = "bootstrap-build")
public class BootstrapBuildProvider implements IDeviceBuildProvider {

    @Option(name="build-target", description="build target name to supply.")
    private String mBuildTargetName = "bootstrapped";

    @Option(name = "branch", description = "build branch name to supply.")
    private String mBranch = null;

    @Option(
        name = "build-id",
        description = "Specify the build id to report instead of the one from the device."
    )
    private String mBuildId = null;

    @Option(name="shell-available-timeout",
            description="Time to wait in seconds for device shell to become available. " +
            "Default to 300 seconds.")
    private long mShellAvailableTimeout = 5 * 60;

    @Option(name="tests-dir", description="Path to top directory of expanded tests zip")
    private File mTestsDir = null;

    @Option(
            name = "extra-file",
            description =
                    "The extra file to be added to the Build Provider. "
                            + "Can be repeated. For example --extra-file file_key_1=/path/to/file")
    private Map<String, File> mExtraFiles = new LinkedHashMap<>();

    @Option(
            name = "collect-build-attribute",
            description = "Whether to collect build attributes from the device to build-info.")
    private boolean mCollectBuildAttribute = true;

    private boolean mCreatedTestDir = false;

    @Override
    public void cleanUp(IBuildInfo info) {
        if (mCreatedTestDir) {
            FileUtil.recursiveDelete(mTestsDir);
            info.cleanUp();
        }
    }

    @Override
    public IBuildInfo getBuild() throws BuildRetrievalError {
        throw new UnsupportedOperationException("Call getBuild(ITestDevice)");
    }

    @Override
    public IBuildInfo getBuild(ITestDevice device) throws BuildRetrievalError,
            DeviceNotAvailableException {
        IBuildInfo info = new DeviceBuildInfo(mBuildId, mBuildTargetName);
        addFiles(info, mExtraFiles);
        if (!(device.getIDevice() instanceof StubDevice) && !SystemUtil.isLocalMode()) {
            try (CloseableTraceScope ignored = new CloseableTraceScope("wait_for_shell")) {
                if (!device.waitForDeviceShell(mShellAvailableTimeout * 1000)) {
                    throw new DeviceNotAvailableException(
                            String.format(
                                    "Shell did not become available in %d seconds",
                                    mShellAvailableTimeout),
                            device.getSerialNumber());
                }
            }
        } else if (mBranch == null) {
            // In order to avoid issue with a null branch, use a placeholder stub for StubDevice.
            mBranch = "stub";
        }
        if (mCollectBuildAttribute) {
            try (CloseableTraceScope bootstrapAttributes =
                    new CloseableTraceScope("bootstrapDeviceBuildAttributes")) {
                BuildInfoUtil.bootstrapDeviceBuildAttributes(
                        info,
                        device,
                        mBuildId,
                        null /* override build flavor */,
                        mBranch,
                        null /* override build alias */);
            }
        } else {
            info.setBuildBranch(mBranch);
            info.setBuildFlavor(mBuildTargetName);
        }
        if (mTestsDir != null && mTestsDir.isDirectory()) {
            info.setFile("testsdir", mTestsDir, info.getBuildId());
        }
        // Avoid tests dir being null, by creating a temporary dir.
        mCreatedTestDir = false;
        if (mTestsDir == null) {
            mCreatedTestDir = true;
            try {
                mTestsDir =
                        FileUtil.createTempDir(
                                "bootstrap-test-dir",
                                CurrentInvocation.getInfo(InvocationInfo.WORK_FOLDER));
            } catch (IOException e) {
                throw new BuildRetrievalError(
                        e.getMessage(), e, InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
            }
            ((IDeviceBuildInfo) info).setTestsDir(mTestsDir, "1");
        } else {
            // Do not copy if it's an existing tests dir.
            info.setProperties(BuildInfoProperties.DO_NOT_COPY_ON_SHARDING);
        }
        if (getInvocationFiles() != null) {
            getInvocationFiles()
                    .put(
                            FilesKey.TESTS_DIRECTORY,
                            mTestsDir,
                            !mCreatedTestDir /* shouldNotDelete */);
        }
        return info;
    }

    /**
     * Add file to build info.
     *
     * @param buildInfo the {@link IBuildInfo} the build info
     * @param fileMaps the {@link Map} of file_key and file object to be added to the buildInfo
     */
    private void addFiles(IBuildInfo buildInfo, Map<String, File> fileMaps) {
        for (final Entry<String, File> entry : fileMaps.entrySet()) {
            buildInfo.setFile(entry.getKey(), entry.getValue(), "0");
        }
    }

    @VisibleForTesting
    ExecutionFiles getInvocationFiles() {
        return CurrentInvocation.getInvocationFiles();
    }

    public final File getTestsDir() {
        return mTestsDir;
    }
}
