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

import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.sandbox.ISandbox;
import com.android.tradefed.sandbox.SandboxConfigDump;
import com.android.tradefed.sandbox.SandboxConfigDump.DumpCmd;
import com.android.tradefed.sandbox.SandboxConfigUtil;
import com.android.tradefed.sandbox.SandboxConfigurationException;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.keystore.IKeyStoreClient;
import com.android.tradefed.util.keystore.IKeyStoreFactory;
import com.android.tradefed.util.keystore.KeyStoreException;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
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.stream.Collectors;

/**
 * Special Configuration factory to handle creation of configurations for Sandboxing purpose.
 *
 * <p>TODO: Split the configuration dump part to another class
 */
public class SandboxConfigurationFactory extends ConfigurationFactory {

    private static SandboxConfigurationFactory sInstance = null;
    public static final Set<String> OPTION_IGNORED_ELEMENTS = new HashSet<>();

    static {
        OPTION_IGNORED_ELEMENTS.addAll(SandboxConfigDump.VERSIONED_ELEMENTS);
        OPTION_IGNORED_ELEMENTS.add(Configuration.DEVICE_REQUIREMENTS_TYPE_NAME);
        OPTION_IGNORED_ELEMENTS.add(Configuration.DEVICE_OPTIONS_TYPE_NAME);
        OPTION_IGNORED_ELEMENTS.add(Configuration.DEVICE_RECOVERY_TYPE_NAME);
        OPTION_IGNORED_ELEMENTS.add(Configuration.CMD_OPTIONS_TYPE_NAME);
    }

    /** Get the singleton {@link IConfigurationFactory} instance. */
    public static SandboxConfigurationFactory getInstance() {
        if (sInstance == null) {
            sInstance = new SandboxConfigurationFactory();
        }
        return sInstance;
    }

    /** {@inheritDoc} */
    @Override
    protected ConfigurationDef getConfigurationDef(
            String name, boolean isGlobal, Map<String, String> templateMap)
            throws ConfigurationException {
        // TODO: Extend ConfigurationDef to possibly create a different IConfiguration type and
        // handle more elegantly the parent/subprocess incompatibilities.
        ConfigurationDef def = createConfigurationDef(name);
        new ConfigLoader(isGlobal).loadConfiguration(name, def, null, templateMap, null);
        return def;
    }

    /** Internal method to create {@link ConfigurationDef} */
    protected ConfigurationDef createConfigurationDef(String name) {
        return new ConfigurationDef(name);
    }

    /**
     * When running the dump for a command. Create a config with specific expectations.
     *
     * @param arrayArgs the command line for the run.
     * @param command The dump command in progress
     * @return a {@link IConfiguration} valid for the VERSIONED Sandbox.
     * @throws ConfigurationException
     */
    public IConfiguration createConfigurationFromArgs(String[] arrayArgs, DumpCmd command)
            throws ConfigurationException {
        // Create on a new object to avoid state on the factory.
        SandboxConfigurationFactory loader = new RunSandboxConfigurationFactory(command);
        return loader.createConfigurationFromArgs(arrayArgs, null, getKeyStoreClient());
    }

    /**
     * Create a {@link IConfiguration} based on the command line and sandbox provided.
     *
     * @param args the command line for the run.
     * @param keyStoreClient the {@link IKeyStoreClient} where to load the key from.
     * @param sandbox the {@link ISandbox} used for the run.
     * @param runUtil the {@link IRunUtil} to run commands.
     * @return a {@link IConfiguration} valid for the sandbox.
     * @throws ConfigurationException
     */
    public IConfiguration createConfigurationFromArgs(
            String[] args, IKeyStoreClient keyStoreClient, ISandbox sandbox, IRunUtil runUtil)
            throws ConfigurationException {
        return createConfigurationFromArgs(args, keyStoreClient, sandbox, runUtil, null, false);
    }

    /**
     * Create a {@link IConfiguration} based on the command line and sandbox provided.
     *
     * @param args the command line for the run.
     * @param keyStoreClient the {@link IKeyStoreClient} where to load the key from.
     * @param sandbox the {@link ISandbox} used for the run.
     * @param runUtil the {@link IRunUtil} to run commands.
     * @return a {@link IConfiguration} valid for the sandbox.
     * @throws ConfigurationException
     */
    public IConfiguration createConfigurationFromArgs(
            String[] args,
            IKeyStoreClient keyStoreClient,
            ISandbox sandbox,
            IRunUtil runUtil,
            File globalConfig,
            boolean skipJavaCheck)
            throws ConfigurationException {
        IConfiguration config = null;
        File xmlConfig = null;
        try {
            runUtil.unsetEnvVariable(GlobalConfiguration.GLOBAL_CONFIG_VARIABLE);
            // Dump the NON_VERSIONED part of the configuration against the current TF and not the
            // sandboxed environment.
            if (globalConfig == null) {
                globalConfig = SandboxConfigUtil.dumpFilteredGlobalConfig(new HashSet<>());
            }
            xmlConfig =
                    SandboxConfigUtil.dumpConfigForVersion(
                            createClasspath(),
                            runUtil,
                            args,
                            DumpCmd.NON_VERSIONED_CONFIG,
                            globalConfig,
                            skipJavaCheck);
            // Get the non version part of the configuration in order to do proper allocation
            // of devices and such.
            config =
                    super.createConfigurationFromArgs(
                            new String[] {xmlConfig.getAbsolutePath()}, null, keyStoreClient);
            // Reset the command line to the original one.
            config.setCommandLine(args);
            config.setConfigurationObject(Configuration.SANDBOX_TYPE_NAME, sandbox);
        } catch (SandboxConfigurationException e) {
            CLog.w("Using thin launcher as fallback");
            // Handle the thin launcher mode: Configuration does not exists in parent version yet.
            config = sandbox.createThinLauncherConfig(args, keyStoreClient, runUtil, globalConfig);
            if (config == null) {
                // Rethrow the original exception.
                CLog.e(e);
                throw e;
            }
        } catch (IOException e) {
            CLog.e(e);
            throw new ConfigurationException("Failed to dump global config.", e);
        } catch (ConfigurationException e) {
            CLog.e(e);
            throw e;
        } finally {
            if (config == null) {
                // In case of error, tear down the sandbox.
                sandbox.tearDown();
            }
            FileUtil.deleteFile(globalConfig);
            FileUtil.deleteFile(xmlConfig);
        }
        return config;
    }

    private IKeyStoreClient getKeyStoreClient() {
        try {
            IKeyStoreFactory f = GlobalConfiguration.getInstance().getKeyStoreFactory();
            if (f != null) {
                try {
                    return f.createKeyStoreClient();
                } catch (KeyStoreException e) {
                    CLog.e("Failed to create key store client");
                    CLog.e(e);
                }
            }
        } catch (IllegalStateException e) {
            CLog.w("Global configuration has not been created, failed to get keystore");
            CLog.e(e);
        }
        return null;
    }

    /** Returns the classpath of the current running Tradefed. */
    private String createClasspath() throws ConfigurationException {
        // Get the classpath property.
        String classpathStr = System.getProperty("java.class.path");
        if (classpathStr == null) {
            throw new ConfigurationException(
                    "Could not find the classpath property: java.class.path");
        }
        return classpathStr;
    }

    private class RunSandboxConfigurationFactory extends SandboxConfigurationFactory {

        private DumpCmd mCommand;

        RunSandboxConfigurationFactory(DumpCmd command) {
            mCommand = command;
        }

        @Override
        protected ConfigurationDef createConfigurationDef(String name) {
            return new ConfigurationDef(name) {
                @Override
                protected void checkRejectedObjects(
                        Map<String, String> rejectedObjects, Throwable cause)
                        throws ClassNotFoundConfigurationException {
                    if (mCommand.equals(DumpCmd.RUN_CONFIG) || mCommand.equals(DumpCmd.TEST_MODE)) {
                        Map<String, String> copyRejected = new HashMap<>();
                        for (Entry<String, String> item : rejectedObjects.entrySet()) {
                            if (SandboxConfigDump.VERSIONED_ELEMENTS.contains(item.getValue())) {
                                copyRejected.put(item.getKey(), item.getValue());
                            }
                        }
                        super.checkRejectedObjects(copyRejected, cause);
                    } else {
                        super.checkRejectedObjects(rejectedObjects, cause);
                    }
                }

                @Override
                protected void injectOptions(IConfiguration config, List<OptionDef> optionList)
                        throws ConfigurationException {
                    List<OptionDef> individualAttempt = new ArrayList<>();
                    if (mCommand.equals(DumpCmd.RUN_CONFIG) || mCommand.equals(DumpCmd.TEST_MODE)) {
                        individualAttempt =
                                optionList
                                        .stream()
                                        .filter(
                                                o ->
                                                        (o.applicableObjectType != null
                                                                && !OPTION_IGNORED_ELEMENTS
                                                                        .contains(
                                                                                o.applicableObjectType)))
                                        .collect(Collectors.toList());
                        optionList.removeAll(individualAttempt);
                    }
                    super.injectOptions(config, optionList);
                    // Try individually each "filtered-option", they will not stop the run if they
                    // cannot be set since they are not part of the versioned process.
                    for (OptionDef item : individualAttempt) {
                        List<OptionDef> tmpList = Arrays.asList(item);
                        try {
                            super.injectOptions(config, tmpList);
                        } catch (ConfigurationException e) {
                            // Ignore
                        }
                    }
                }
            };
        }
    }
}
