/*
 * 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.invoker;

import com.android.tradefed.build.BuildInfo;
import com.android.tradefed.build.BuildSerializedVersion;
import com.android.tradefed.build.IBuildInfo;
import com.android.tradefed.build.proto.BuildInformation;
import com.android.tradefed.config.ConfigurationDescriptor;
import com.android.tradefed.config.proto.ConfigurationDescription.Metadata;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.invoker.logger.InvocationMetricLogger;
import com.android.tradefed.invoker.logger.TfObjectTracker;
import com.android.tradefed.invoker.proto.InvocationContext.Context;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.testtype.suite.ITestSuite;
import com.android.tradefed.util.MultiMap;
import com.android.tradefed.util.UniqueMultiMap;

import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
 * Generic implementation of a {@link IInvocationContext}.
 */
public class InvocationContext implements IInvocationContext {
    private static final long serialVersionUID = BuildSerializedVersion.VERSION;

    // Transient field are not serialized
    private transient Map<ITestDevice, IBuildInfo> mAllocatedDeviceAndBuildMap;
    /** Map of the configuration device name and the actual {@link ITestDevice} * */
    private transient Map<String, ITestDevice> mNameAndDeviceMap;
    private Map<String, IBuildInfo> mNameAndBuildinfoMap;
    private final UniqueMultiMap<String, String> mInvocationAttributes =
            new UniqueMultiMap<String, String>();
    /** Invocation test-tag **/
    private String mTestTag;
    /** configuration descriptor */
    private ConfigurationDescriptor mConfigurationDescriptor;
    /** module invocation context (when running as part of a {@link ITestSuite} */
    private IInvocationContext mModuleContext;
    /**
     * List of map the device serials involved in the sharded invocation, empty if not a sharded
     * invocation.
     */
    private Map<Integer, List<String>> mShardSerials;

    private boolean mLocked;
    private boolean mReleasedEarly = false;

    /**
     * Creates a {@link BuildInfo} using default attribute values.
     */
    public InvocationContext() {
        mAllocatedDeviceAndBuildMap = new LinkedHashMap<ITestDevice, IBuildInfo>();
        // Use LinkedHashMap to ensure key ordering by insertion order
        mNameAndDeviceMap = new LinkedHashMap<String, ITestDevice>();
        mNameAndBuildinfoMap = new LinkedHashMap<String, IBuildInfo>();
        mShardSerials = new LinkedHashMap<Integer, List<String>>();
    }

    @Override
    public String getInvocationId() {
        List<String> values = mInvocationAttributes.get(INVOCATION_ID);
        return values == null || values.isEmpty() ? null : values.get(0);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public int getNumDevicesAllocated() {
        return mAllocatedDeviceAndBuildMap.size();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addAllocatedDevice(String devicename, ITestDevice testDevice) {
        mNameAndDeviceMap.put(devicename, testDevice);
        // back fill the information if possible
        if (mNameAndBuildinfoMap.get(devicename) != null) {
            mAllocatedDeviceAndBuildMap.put(testDevice, mNameAndBuildinfoMap.get(devicename));
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addAllocatedDevice(Map<String, ITestDevice> deviceWithName) {
        mNameAndDeviceMap.putAll(deviceWithName);
        // back fill the information if possible
        for (Entry<String, ITestDevice> entry : deviceWithName.entrySet()) {
            if (mNameAndBuildinfoMap.get(entry.getKey()) != null) {
                mAllocatedDeviceAndBuildMap.put(
                        entry.getValue(), mNameAndBuildinfoMap.get(entry.getKey()));
            }
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Map<ITestDevice, IBuildInfo> getDeviceBuildMap() {
        return mAllocatedDeviceAndBuildMap;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<ITestDevice> getDevices() {
        return new ArrayList<ITestDevice>(mNameAndDeviceMap.values());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<IBuildInfo> getBuildInfos() {
        return new ArrayList<IBuildInfo>(mNameAndBuildinfoMap.values());
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> getSerials() {
        List<String> listSerials = new ArrayList<String>();
        for (ITestDevice testDevice : mNameAndDeviceMap.values()) {
            listSerials.add(testDevice.getSerialNumber());
        }
        return listSerials;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public List<String> getDeviceConfigNames() {
        List<String> listNames = new ArrayList<String>();
        listNames.addAll(mNameAndDeviceMap.keySet());
        return listNames;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ITestDevice getDevice(String deviceName) {
        return mNameAndDeviceMap.get(deviceName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IBuildInfo getBuildInfo(String deviceName) {
        return mNameAndBuildinfoMap.get(deviceName);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public IBuildInfo getBuildInfo(ITestDevice testDevice) {
        return mAllocatedDeviceAndBuildMap.get(testDevice);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addDeviceBuildInfo(String deviceName, IBuildInfo buildinfo) {
        mNameAndBuildinfoMap.put(deviceName, buildinfo);
        mAllocatedDeviceAndBuildMap.put(getDevice(deviceName), buildinfo);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void addInvocationAttribute(String attributeName, String attributeValue) {
        if (mLocked) {
            throw new IllegalStateException(
                    "Attempting to add invocation attribute during a test.");
        }
        mInvocationAttributes.put(attributeName, attributeValue);
    }

    /** {@inheritDoc} */
    @Override
    public void addInvocationAttributes(MultiMap<String, String> attributesMap) {
        if (mLocked) {
            throw new IllegalStateException(
                    "Attempting to add invocation attribute during a test.");
        }
        mInvocationAttributes.putAll(attributesMap);
    }

    /** {@inheritDoc} */
    @Override
    public MultiMap<String, String> getAttributes() {
        // Return a copy of the map to avoid unwanted modifications.
        UniqueMultiMap<String, String> copy = new UniqueMultiMap<>();
        copy.putAll(mInvocationAttributes);
        return copy;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public ITestDevice getDeviceBySerial(String serial) {
        for (ITestDevice testDevice : mNameAndDeviceMap.values()) {
            if (testDevice.getSerialNumber().equals(serial)) {
                return testDevice;
            }
        }
        CLog.d("Device with serial '%s', not found in the metadata", serial);
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public String getDeviceName(ITestDevice device) {
        for (String name : mNameAndDeviceMap.keySet()) {
            if (device.equals(getDevice(name))) {
                return name;
            }
        }
        CLog.d(
                "Device with serial '%s' doesn't match a name in the metadata",
                device.getSerialNumber());
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public String getBuildInfoName(IBuildInfo info) {
        for (String name : mNameAndBuildinfoMap.keySet()) {
            if (info.equals(getBuildInfo(name))) {
                return name;
            }
        }
        CLog.d("Build info doesn't match a name in the metadata");
        return null;
    }

    /** {@inheritDoc} */
    @Override
    public String getTestTag() {
        return mTestTag;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setTestTag(String testTag) {
        mTestTag = testTag;
    }

    @Override
    public boolean wasReleasedEarly() {
        return mReleasedEarly;
    }

    @Override
    public void markReleasedEarly() {
        mReleasedEarly = true;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setRecoveryModeForAllDevices(RecoveryMode mode) {
        for (ITestDevice device : getDevices()) {
            device.setRecoveryMode(mode);
        }
    }

    /** {@inheritDoc} */
    @Override
    public void setConfigurationDescriptor(ConfigurationDescriptor configurationDescriptor) {
        mConfigurationDescriptor = configurationDescriptor;
    }

    /** {@inheritDoc} */
    @Override
    public ConfigurationDescriptor getConfigurationDescriptor() {
        return mConfigurationDescriptor;
    }

    /** {@inheritDoc} */
    @Override
    public void setModuleInvocationContext(IInvocationContext invocationContext) {
        mModuleContext = invocationContext;
    }

    /** {@inheritDoc} */
    @Override
    public IInvocationContext getModuleInvocationContext() {
        return mModuleContext;
    }

    /** Lock the context to prevent more invocation attributes to be added. */
    public void lockAttributes() {
        mLocked = true;
    }

    /** Private method to unlock the attributes. Used for sandbox test mode only. */
    @SuppressWarnings("unused")
    private void unlock() {
        mLocked = false;
    }

    /** Log the {@link InvocationMetricLogger} attributes to the invocation. */
    public void logInvocationMetrics() {
        Map<String, String> metrics = InvocationMetricLogger.getInvocationMetrics();
        if (!metrics.isEmpty()) {
            mInvocationAttributes.putAll(new MultiMap<>(metrics));
        }
        Map<String, Long> usage = TfObjectTracker.getUsage();
        if (!usage.isEmpty()) {
            mInvocationAttributes.put(
                    TfObjectTracker.TF_OBJECTS_TRACKING_KEY, Joiner.on(",").join(usage.entrySet()));
        }
    }

    /** {@inheritDoc} */
    @Override
    public void addSerialsFromShard(Integer index, List<String> serials) {
        if (mLocked) {
            throw new IllegalStateException(
                    "Attempting to add serial from shard attribute during a test.");
        }
        mShardSerials.put(index, serials);
    }

    /** {@inheritDoc} */
    @Override
    public Map<Integer, List<String>> getShardsSerials() {
        return new LinkedHashMap<>(mShardSerials);
    }

    /** Special java method that allows for custom deserialization. */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        // our "pseudo-constructor"
        in.defaultReadObject();
        // now we are a "live" object again, so let's init the transient field
        mAllocatedDeviceAndBuildMap = new LinkedHashMap<ITestDevice, IBuildInfo>();
        mNameAndDeviceMap = new LinkedHashMap<String, ITestDevice>();
    }

    /** {@inheritDoc} */
    @Override
    public Context toProto() {
        Context.Builder contextBuilder = Context.newBuilder();
        // The invocation test tag.
        if (mTestTag != null) {
            contextBuilder.setTestTag(mTestTag);
        }
        // Map name to build info
        Map<String, BuildInformation.BuildInfo> mapBuild = new LinkedHashMap<>();
        for (String name : mNameAndBuildinfoMap.keySet()) {
            mapBuild.put(name, mNameAndBuildinfoMap.get(name).toProto());
        }
        contextBuilder.putAllNameBuildInfo(mapBuild);
        // Metadata
        List<Metadata> metadatas = new ArrayList<>();
        for (String key : mInvocationAttributes.keySet()) {
            if (mInvocationAttributes.get(key) != null) {
                try {
                    Metadata value =
                            Metadata.newBuilder()
                                    .setKey(key)
                                    .addAllValue(mInvocationAttributes.get(key))
                                    .build();
                    metadatas.add(value);
                } catch (RuntimeException e) {
                    CLog.e(
                            "Invocation attribute key '%s' raised exception. values: %s",
                            key, mInvocationAttributes.get(key));
                    CLog.e(e);
                }

            } else {
                CLog.e("Invocation attribute Key '%s' has null value.", key);
            }
        }
        contextBuilder.addAllMetadata(metadatas);
        // Configuration Description
        if (mConfigurationDescriptor != null) {
            contextBuilder.setConfigurationDescription(mConfigurationDescriptor.toProto());
        }
        // Module Context if it exists
        if (mModuleContext != null) {
            contextBuilder.setModuleContext(mModuleContext.toProto());
        }
        return contextBuilder.build();
    }

    /** Inverse operation to {@link InvocationContext#toProto()} to get the instance back. */
    public static InvocationContext fromProto(Context protoContext) {
        InvocationContext context = new InvocationContext();
        // Test Tag.
        context.mTestTag = protoContext.getTestTag();
        // Map Build Info
        for (String key : protoContext.getNameBuildInfoMap().keySet()) {
            context.mNameAndBuildinfoMap.put(
                    key, BuildInfo.fromProto(protoContext.getNameBuildInfoMap().get(key)));
        }
        // Metadata
        for (Metadata meta : protoContext.getMetadataList()) {
            for (String value : meta.getValueList()) {
                context.mInvocationAttributes.put(meta.getKey(), value);
            }
        }
        // Configuration Description
        context.mConfigurationDescriptor =
                ConfigurationDescriptor.fromProto(protoContext.getConfigurationDescription());
        // Module Context - context module will have some property set: module-id at the minimum
        if (protoContext.hasModuleContext()) {
            // TODO: Check explicitly for module-id
            context.mModuleContext = InvocationContext.fromProto(protoContext.getModuleContext());
        }
        return context;
    }

    /** Returns whether we detect presubmit based on trigger type. */
    public static boolean isPresubmit(IInvocationContext context) {
        Set<String> presubmitTrigger = ImmutableSet.of("WORK_NODE", "TREEHUGGER");
        return presubmitTrigger.contains(context.getAttribute("trigger"));
    }

    /** Returns whether we detect on demand test invocation based on trigger type. */
    public static boolean isOnDemand(IInvocationContext context) {
        Set<String> abtdTrigger = ImmutableSet.of("TRYBOT", "ABTD");
        return abtdTrigger.contains(context.getAttribute("trigger"));
    }
}
