// 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.devtools.build.lib.shell.Subprocess;
import com.google.devtools.build.lib.windows.jni.WindowsProcesses;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * A Windows subprocess backed by a native object.
 */
public class WindowsSubprocess implements Subprocess {
  // For debugging purposes.
  private String commandLine;

  /**
   * Output stream for writing to the stdin of a Windows process.
   */
  private class ProcessOutputStream extends OutputStream {
    private ProcessOutputStream() {
    }

    @Override
    public void write(int b) throws IOException {
      byte[] buf = new byte[]{ (byte) b };
      write(buf, 0, 1);
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
      writeStream(b, off, len);
    }
  }

  /**
   * Input stream for reading the stdout or stderr of a Windows process.
   *
   * <p>This class is non-static for debugging purposes.
   */
  private static final class ProcessInputStream extends InputStream {
    private long nativeStream;

    ProcessInputStream(long nativeStream) {
      this.nativeStream = nativeStream;
    }

    @Override
    public int read() throws IOException {
      byte[] buf = new byte[1];
      if (read(buf, 0, 1) != 1) {
        return -1;
      } else {
        return buf[0] & 0xff;
      }
    }

    @Override
    public synchronized int read(byte[] b, int off, int len) throws IOException {
      if (nativeStream == WindowsProcesses.INVALID) {
        throw new IllegalStateException();
      }

      int result = WindowsProcesses.readStream(nativeStream, b, off, len);

      if (result == 0) {
        return -1; // EOF
      }
      if (result == -1) {
        throw new IOException(WindowsProcesses.streamGetLastError(nativeStream));
      }

      return result;
    }

    @Override
    public synchronized void close() {
      if (nativeStream != WindowsProcesses.INVALID) {
        WindowsProcesses.closeStream(nativeStream);
        nativeStream = WindowsProcesses.INVALID;
      }
    }

    @Override
    protected void finalize() throws Throwable {
      close();
      super.finalize();
    }
  }

  private static final AtomicInteger THREAD_SEQUENCE_NUMBER = new AtomicInteger(1);
  private static final ExecutorService WAITER_POOL = Executors.newCachedThreadPool(
      new ThreadFactory() {
        @Override
        public Thread newThread(Runnable runnable) {
          Thread thread = new Thread(null, runnable,
              "Windows-Process-Waiter-Thread-" + THREAD_SEQUENCE_NUMBER.getAndIncrement(),
              16 * 1024);
          thread.setDaemon(true);
          return thread;
        }
      });

  private volatile long nativeProcess;
  private final OutputStream stdinStream;
  private final ProcessInputStream stdoutStream;
  private final ProcessInputStream stderrStream;
  private final CountDownLatch waitLatch;
  private final long timeoutMillis;
  private final AtomicBoolean timedout = new AtomicBoolean(false);

  WindowsSubprocess(long nativeProcess, String commandLine, boolean stdoutRedirected,
      boolean stderrRedirected, long timeoutMillis) {
    this.commandLine = commandLine;
    this.nativeProcess = nativeProcess;
    // As per the spec of Command, we should only apply timeouts that are > 0.
    this.timeoutMillis = timeoutMillis <= 0 ? -1 : timeoutMillis;
    stdoutStream =
        stdoutRedirected ? null : new ProcessInputStream(WindowsProcesses.getStdout(nativeProcess));
    stderrStream =
        stderrRedirected ? null : new ProcessInputStream(WindowsProcesses.getStderr(nativeProcess));
    stdinStream = new ProcessOutputStream();
    waitLatch = new CountDownLatch(1);
    // Every Windows process we start consumes a thread here. This is suboptimal, but seems to be
    // the sanest way to reconcile WaitForMultipleObjects() and Java-style interruption.
    @SuppressWarnings("unused")
    Future<?> possiblyIgnoredError = WAITER_POOL.submit(this::waiterThreadFunc);
  }

  private void waiterThreadFunc() {
    switch (WindowsProcesses.waitFor(nativeProcess, timeoutMillis)) {
      case 0:
        // Excellent, process finished in time.
        break;

      case 1:
        // Timeout. Terminate the process if we can.
        timedout.set(true);
        WindowsProcesses.terminate(nativeProcess);
        break;

      case 2:
        // Error. There isn't a lot we can do -- the process is still alive but
        // WaitForMultipleObjects() failed for some odd reason. We'll pretend it terminated and
        // log a message to jvm.out .
        System.err.println(
            "Waiting for process " + WindowsProcesses.getProcessPid(nativeProcess) + " failed");
        break;
    }

    waitLatch.countDown();
  }

  @Override
  public synchronized void finalize() throws Throwable {
    if (nativeProcess != WindowsProcesses.INVALID) {
      close();
    }
    super.finalize();
  }

  @Override
  public synchronized boolean destroy() {
    checkLiveness();
    return WindowsProcesses.terminate(nativeProcess);
  }

  @Override
  public synchronized int exitValue() {
    checkLiveness();

    int result = WindowsProcesses.getExitCode(nativeProcess);
    String error = WindowsProcesses.processGetLastError(nativeProcess);
    if (!error.isEmpty()) {
      throw new IllegalStateException(error);
    }

    return result;
  }

  @Override
  public boolean finished() {
    return waitLatch.getCount() == 0;
  }

  @Override
  public boolean timedout() {
    return timedout.get();
  }

  @Override
  public void waitFor() throws InterruptedException {
    waitLatch.await();
  }

  @Override
  public synchronized void close() {
    if (nativeProcess != WindowsProcesses.INVALID) {
      stdoutStream.close();
      stderrStream.close();
      long process = nativeProcess;
      nativeProcess = WindowsProcesses.INVALID;
      WindowsProcesses.deleteProcess(process);
    }
  }

  @Override
  public OutputStream getOutputStream() {
    return stdinStream;
  }

  @Override
  public InputStream getInputStream() {
    return stdoutStream;
  }

  @Override
  public InputStream getErrorStream() {
    return stderrStream;
  }

  private synchronized void writeStream(byte[] b, int off, int len) throws IOException {
    checkLiveness();

    int remaining = len;
    int currentOffset = off;
    while (remaining != 0) {
      int written = WindowsProcesses.writeStdin(nativeProcess, b, currentOffset, remaining);
      // I think the Windows API never returns 0 in dwNumberOfBytesWritten
      // Verify.verify(written != 0);
      if (written == -1) {
        throw new IOException(WindowsProcesses.processGetLastError(nativeProcess));
      }

      remaining -= written;
      currentOffset += written;
    }
  }

  private void checkLiveness() {
    if (nativeProcess == WindowsProcesses.INVALID) {
      throw new IllegalStateException();
    }
  }

  @Override
  public String toString() {
    return String.format("%s:[%s]", super.toString(), commandLine);
  }
}
