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

import com.android.tradefed.config.ConfigurationException;
import com.android.tradefed.config.Option;
import com.android.tradefed.config.OptionClass;
import com.android.tradefed.error.HarnessRuntimeException;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.result.error.InfraErrorIdentifier;
import com.android.tradefed.util.RunInterruptedException;

import java.io.File;
import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.Semaphore;

/**
 * Host options holder class.
 * This class is used to store host-wide options.
 */
@OptionClass(alias = "host_options", global_namespace = false)
public class HostOptions implements IHostOptions {

    @Option(name = "concurrent-flasher-limit", description =
            "The maximum number of concurrent flashers (may be useful to avoid memory constraints)")
    private Integer mConcurrentFlasherLimit = 1;

    @Option(
        name = "concurrent-download-limit",
        description =
                "The maximum number of concurrent downloads (may be useful to avoid network "
                        + "constraints)"
    )
    private Integer mConcurrentDownloadLimit = null;

    @Option(
            name = "concurrent-virtual-device-startup-limit",
            description =
                    "The maximum number of concurrent virtual device startup to avoid resource"
                            + " contentions depending on factors such as network, CPU, I/O etc.")
    private Integer mConcurrentVirtualDeviceStartupLimit = null;

    @Option(name = "concurrent-limits", description =
            "The maximum number of concurrent actions of a given type.")
    private Map<PermitLimitType, Integer> mConcurrentLimit = new HashMap<>();

    @Option(
        name = "fastboot-tmpdir",
        description = "The location of temporary directory used by fastboot"
    )
    private File mFastbootTmpDir = null;

    @Option(
            name = "enable-fastbootd-mode",
            description = "Feature flag to enable the support for fastbootd.")
    private boolean mEnableFastbootdMode = true;

    @Option(name = "download-cache-dir", description = "the directory for caching downloaded "
            + "flashing files. Should be on the same filesystem as java.io.tmpdir.  Consider "
            + "changing the java.io.tmpdir property if you want to move downloads to a different "
            + "filesystem.")
    private File mDownloadCacheDir = new File(System.getProperty("java.io.tmpdir"), "lc_cache");

    @Option(name = "use-sso-client", description = "Use a SingleSignOn client for HTTP requests.")
    private Boolean mUseSsoClient = true;

    @Option(
        name = "service-account-json-key-file",
        description =
                "Specify a service account json key file, and a String key name to identify it."
    )
    private Map<String, File> mJsonServiceAccountMap = new HashMap<>();

    @Option(name = "label", description = "Labels to describe the host.")
    private List<String> mLabels = new ArrayList<>();

    @Option(
            name = "known-gce-device-ip-pool",
            description =
                    "known remote device available via ip associated with the "
                            + "gce-device placeholder.")
    private Set<String> mKnownGceDeviceIpPool = new HashSet<>();

    @Option(
            name = "known-remote-device-ip-pool",
            description =
                    "known remote device available via ip associated with the "
                            + "remote-device placeholder.")
    private Set<String> mKnownRemoteDeviceIpPool = new HashSet<>();

    @Option(
            name = "use-zip64-in-partial-download",
            description = "Whether to use zip64 format in partial download.")
    private boolean mUseZip64InPartialDownload = true;

    @Option(
            name = "use-network-interface",
            description = "The network interface used to connect to test devices.")
    private String mNetworkInterface = null;

    @Option(
            name = "preconfigured-virtual-device-pool",
            description = "Preconfigured virtual device pool. (Value format: $hostname:$user.)")
    private List<String> mPreconfiguredVirtualDevicePool = new ArrayList<>();

    @Option(
            name = "flash-with-fuse-zip",
            description = "Use `fastboot flashall` on a folder of fuse mounted device image zip "
                    + "instead of `fastboot update` with zip")
    private boolean mFlashWithFuseZip = false;

    @Option(
            name = "cache-size-limit",
            description =
                    "The maximum allowed size(bytes) of the local file cache. (default: 15GB)")
    private Long mCacheSizeLimit = 15L * 1024L * 1024L * 1024L;

    @Option(
            name = "test-phase-timeout",
            description =
                    "the maximum time to wait for test phase to finish before attempting to force"
                            + "stop it. A value of zero will indicate no timeout.",
            isTimeVal = true)
    private long mTestPhaseTimeout = 0;

    @Option(
            name = "enable-incremental-flashing",
            description = "Feature flag to enable incremental flashing on one host.")
    private boolean mEnableIncrementalFlashing = false;

    @Option(
            name = "opt-out-incremental-flashing",
            description = "Allows an host to fully opt-out of incremental flashing.")
    private boolean mOptOutFromIncrementalFlashing = false;

    @Option(
            name = "disable-host-metric-reporting",
            description = "Feature flag to disable the support for host metric reporting.")
    private boolean mDisableHostMetricReporting = false;

    private Map<PermitLimitType, Semaphore> mConcurrentLocks = new HashMap<>();
    private Map<PermitLimitType, Integer> mInternalConcurrentLimits = new HashMap<>();

    /** {@inheritDoc} */
    @Override
    public Long getCacheSizeLimit() {
        return mCacheSizeLimit;
    }

    /** {@inheritDoc} */
    @Override
    public Integer getConcurrentFlasherLimit() {
        return mConcurrentFlasherLimit;
    }

    /** {@inheritDoc} */
    @Override
    public Integer getConcurrentDownloadLimit() {
        return mConcurrentDownloadLimit;
    }

    @Override
    public Integer getConcurrentVirtualDeviceStartupLimit() {
        return mConcurrentVirtualDeviceStartupLimit;
    }

    /** {@inheritDoc} */
    @Override
    public File getFastbootTmpDir() {
        if (mFastbootTmpDir != null) {
            if (!mFastbootTmpDir.exists() || !mFastbootTmpDir.isDirectory()) {
                throw new HarnessRuntimeException(
                        String.format(
                                "Fastboot tmp dir '%s' is missing and was expected.",
                                mFastbootTmpDir),
                        InfraErrorIdentifier.LAB_HOST_FILESYSTEM_ERROR);
            }
            return mFastbootTmpDir;
        }
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isFastbootdEnable() {
        return mEnableFastbootdMode;
    }

    /** {@inheritDoc} */
    @Override
    public File getDownloadCacheDir() {
        return mDownloadCacheDir;
    }

    /** {@inheritDoc} */
    @Override
    public Boolean shouldUseSsoClient() {
        return mUseSsoClient;
    }

    /** {@inheritDoc} */
    @Override
    public Map<String, File> getServiceAccountJsonKeyFiles() {
        return new HashMap<>(mJsonServiceAccountMap);
    }

    /** {@inheritDoc} */
    @Override
    public void validateOptions() throws ConfigurationException {
        // Validation of host options
    }

    /** {@inheritDoc} */
    @Override
    public List<String> getLabels() {
        return new ArrayList<>(mLabels);
    }

    /** {@inheritDoc} */
    @Override
    public Set<String> getKnownGceDeviceIpPool() {
        return new HashSet<>(mKnownGceDeviceIpPool);
    }

    /** {@inheritDoc} */
    @Override
    public Set<String> getKnownRemoteDeviceIpPool() {
        return new HashSet<>(mKnownRemoteDeviceIpPool);
    }

    /** {@inheritDoc} */
    @Override
    public List<String> getKnownPreconfigureVirtualDevicePool() {
        return new ArrayList<>(mPreconfiguredVirtualDevicePool);
    }

    /** {@inheritDoc} */
    @Override
    public boolean getUseZip64InPartialDownload() {
        return mUseZip64InPartialDownload;
    }

    /** {@inheritDoc} */
    @Override
    public String getNetworkInterface() {
        if (mNetworkInterface != null) {
            return mNetworkInterface;
        }

        try {
            for (Enumeration<NetworkInterface> enNetI = NetworkInterface.getNetworkInterfaces();
                    enNetI.hasMoreElements(); ) {
                NetworkInterface netI = enNetI.nextElement();
                if (!netI.isUp()) {
                    continue;
                }
                for (Enumeration<InetAddress> enumIpAddr = netI.getInetAddresses();
                        enumIpAddr.hasMoreElements(); ) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isAnyLocalAddress()
                            && !inetAddress.isLinkLocalAddress()
                            && !inetAddress.isLoopbackAddress()
                            && !inetAddress.isMulticastAddress()) {
                        mNetworkInterface = netI.getName();
                        return mNetworkInterface;
                    }
                }
            }
        } catch (SocketException e) {
            CLog.w("Failed to get host's active interface");
            CLog.w(e);
        }
        return null;
    }

    @Override
    public long getTestPhaseTimeout() {
        return mTestPhaseTimeout;
    }

    @Override
    public void initConcurrentLocks() {
        // Do not reinit if it has been called before
        if (!mConcurrentLocks.isEmpty()) {
            return;
        }
        mInternalConcurrentLimits.putAll(mConcurrentLimit);
        // Backfill flasher & download limit from their dedicated option
        if (!mInternalConcurrentLimits.containsKey(PermitLimitType.CONCURRENT_FLASHER)) {
            mInternalConcurrentLimits.put(
                    PermitLimitType.CONCURRENT_FLASHER, mConcurrentFlasherLimit);
        }
        if (!mInternalConcurrentLimits.containsKey(PermitLimitType.CONCURRENT_DOWNLOAD)) {
            mInternalConcurrentLimits.put(
                    PermitLimitType.CONCURRENT_DOWNLOAD, mConcurrentDownloadLimit);
        }

        if (!mInternalConcurrentLimits.containsKey(
                PermitLimitType.CONCURRENT_VIRTUAL_DEVICE_STARTUP)) {
            mInternalConcurrentLimits.put(
                    PermitLimitType.CONCURRENT_VIRTUAL_DEVICE_STARTUP,
                    mConcurrentVirtualDeviceStartupLimit);
        }

        for (Entry<PermitLimitType, Integer> limits : mInternalConcurrentLimits.entrySet()) {
            if (limits.getValue() == null) {
                continue;
            }
            mConcurrentLocks.put(limits.getKey(),
                    new Semaphore(limits.getValue(), true /* fair */));
        }
    }

    @Override
    public void takePermit(PermitLimitType type) {
        if (!mConcurrentLocks.containsKey(type)) {
            return;
        }
        CLog.i(
                "Requesting a '%s' permit out of the max limit of %s. Current queue "
                        + "length: %s",
                type,
                mInternalConcurrentLimits.get(type),
                mConcurrentLocks.get(type).getQueueLength());
        try {
            mConcurrentLocks.get(type).acquire();
        } catch (InterruptedException e) {
            throw new RunInterruptedException(e.getMessage(), e, InfraErrorIdentifier.UNDETERMINED);
        }
    }

    @Override
    public void returnPermit(PermitLimitType type) {
        if (!mConcurrentLocks.containsKey(type)) {
            return;
        }
        mConcurrentLocks.get(type).release();
    }

    @Override
    public Integer getAvailablePermits(PermitLimitType type) {
        if (!mConcurrentLocks.containsKey(type)) {
            return Integer.MAX_VALUE;
        }
        return mConcurrentLocks.get(type).availablePermits();
    }

    @Override
    public int getInUsePermits(PermitLimitType type) {
        if (!mConcurrentLocks.containsKey(type)) {
            return 0;
        }
        return mInternalConcurrentLimits.get(type) - mConcurrentLocks.get(type).availablePermits();
    }

    @Override
    public boolean shouldFlashWithFuseZip() {
        return mFlashWithFuseZip;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isIncrementalFlashingEnabled() {
        return mEnableIncrementalFlashing;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isOptOutOfIncrementalFlashing() {
        return mOptOutFromIncrementalFlashing;
    }

    /** {@inheritDoc} */
    @Override
    public boolean isHostMetricReportingDisabled() {
        return mDisableHostMetricReporting;
    }
}
