// Copyright 2016 The Bazel Authors. All rights reserved.
//
// 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.google.devtools.build.lib.windows;

import com.google.common.base.Charsets;
import com.google.devtools.build.lib.shell.Subprocess;
import com.google.devtools.build.lib.shell.SubprocessBuilder;
import com.google.devtools.build.lib.shell.SubprocessBuilder.StreamAction;

import java.io.File;
import java.io.IOException;
import java.util.Map;
import java.util.TreeMap;

/**
 * A subprocess factory that uses the Win32 API.
 */
public class WindowsSubprocessFactory implements Subprocess.Factory {
  public static final WindowsSubprocessFactory INSTANCE = new WindowsSubprocessFactory();

  private WindowsSubprocessFactory() {
    // Singleton
  }

  @Override
  public Subprocess create(SubprocessBuilder builder) throws IOException {
    WindowsJniLoader.loadJni();

    String commandLine = WindowsProcesses.quoteCommandLine(builder.getArgv());
    byte[] env = builder.getEnv() == null ? null : convertEnvToNative(builder.getEnv());

    String stdoutPath = getRedirectPath(builder.getStdout(), builder.getStdoutFile());
    String stderrPath = getRedirectPath(builder.getStderr(), builder.getStderrFile());

    long nativeProcess = WindowsProcesses.nativeCreateProcess(
        commandLine, env, builder.getWorkingDirectory().getPath(), stdoutPath, stderrPath);
    String error = WindowsProcesses.nativeProcessGetLastError(nativeProcess);
    if (!error.isEmpty()) {
      WindowsProcesses.nativeDeleteProcess(nativeProcess);
      throw new IOException(error);
    }

    return new WindowsSubprocess(nativeProcess, commandLine, stdoutPath != null,
        stderrPath != null, builder.getTimeoutMillis());
  }

  private String getRedirectPath(StreamAction action, File file) {
    switch (action) {
      case DISCARD:
        return "NUL";  // That's /dev/null on Windows

      case REDIRECT:
        return file.getPath();

      case STREAM:
        return null;

      default:
        throw new IllegalStateException();
    }
  }

  private String getSystemRoot(Map<String, String> env) {
    // Windows environment variables are case-insensitive, so we can't just say
    // System.getenv().get("SystemRoot")
    for (String key : env.keySet()) {
      if (key.toUpperCase().equals("SYSTEMROOT")) {
        return env.get(key);
      }
    }

    return null;
  }

  /**
   * Converts an environment map to the format expected in lpEnvironment by CreateProcess().
   */
  private byte[] convertEnvToNative(Map<String, String> env) throws IOException {
    Map<String, String> realEnv = new TreeMap<>();
    realEnv.putAll(env == null ? System.getenv() : env);
    if (getSystemRoot(realEnv) == null) {
      // Some versions of MSVCRT.DLL require SystemRoot to be set. It's quite a common library to
      // link in, so we add this environment variable regardless of whether the caller requested
      // it or not.
      String systemRoot = getSystemRoot(System.getenv());
      if (systemRoot != null) {
        realEnv.put("SystemRoot", systemRoot);
      }
    }

    if (realEnv.isEmpty()) {
      // Special case: CreateProcess() always expects the environment block to be terminated
      // with two zeros.
      return new byte[] { 0, 0, };
    }

    StringBuilder result = new StringBuilder();
    for (Map.Entry<String, String> entry : realEnv.entrySet()) {
      if (entry.getKey().contains("=")) {
        // lpEnvironment requires no '=' in environment variable name, but on Windows,
        // System.getenv() returns environment variables like '=C:' or '=ExitCode', so it can't
        // be an error, we have ignore them here.
        continue;
      }
      result.append(entry.getKey() + "=" + entry.getValue() + "\0");
    }

    result.append("\0");
    return result.toString().getBytes(Charsets.UTF_8);
  }
}
