/*
 * Copyright 2017 Google Inc.
 *
 * 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.j2cl.bazel;

import com.google.common.base.Splitter;
import com.google.common.collect.Iterables;
import com.google.common.io.Files;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkRequest;
import com.google.devtools.build.lib.worker.WorkerProtocol.WorkResponse;
import com.google.j2cl.common.Problems;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.function.Supplier;
import org.kohsuke.args4j.CmdLineException;
import org.kohsuke.args4j.CmdLineParser;

/**
 * A base class for running processes as blaze workers. Used for both the transpiler
 * and @GwtIncompatible stripper tool.
 *
 * <p>Partially adapted from {@code com.google.devtools.build.buildjar.BazelJavaBuilder}.
 */
public abstract class BazelWorker {

  protected abstract Problems run();

  /**
   * Process the request described by the arguments. Note that you must output errors and warnings
   * via {@link Problems} to avoid interrupting the worker protocol which occurs over stdout.
   */
  private Problems processRequest(String[] args) {
    CmdLineParser parser = new CmdLineParser(this);
    Problems problems = new Problems();

    try {
      parser.parseArgument(args);
    } catch (CmdLineException e) {
      problems.error(e.getMessage());
      return problems;
    }

    try {
      return run();
    } catch (Problems.Exit e) {
      return e.getProblems();
    }
  }

  public static final void start(String[] args, Supplier<BazelWorker> workerSupplier)
      throws Exception {
    if (args.length == 1 && args[0].equals("--persistent_worker")) {
      runPersistentWorker(workerSupplier);
    } else {
      runStandaloneWorker(workerSupplier, args);
    }
  }

  private static void runStandaloneWorker(Supplier<BazelWorker> workerSupplier, String[] args)
      throws IOException {
    // This is a single invocation of builder that exits after it processed the request.
    try (PrintWriter err = new PrintWriter(System.err)) {
      int exitCode = execute(workerSupplier, expandFlagFile(args), err);
      err.flush();
      System.exit(exitCode);
    }
  }

  private static void runPersistentWorker(Supplier<BazelWorker> workerSupplier) throws IOException {
    while (true) {
      WorkRequest request = WorkRequest.parseDelimitedFrom(System.in);

      if (request == null) {
        break;
      }

      try (StringWriter sw = new StringWriter();
          PrintWriter pw = new PrintWriter(sw)) {
        String[] args = request.getArgumentsList().toArray(new String[0]);
        int exitCode = execute(workerSupplier, args, pw);
        WorkResponse.newBuilder()
            .setOutput(sw.toString())
            .setExitCode(exitCode)
            .build()
            .writeDelimitedTo(System.out);
        System.out.flush();

        // Hint to the system that now would be a good time to run a gc.  After a compile
        // completes lots of objects should be available for collection and it should be cheap to
        // collect them.
        System.gc();
      }
    }
  }

  private static final int execute(
      Supplier<BazelWorker> workerSupplier, String[] args, PrintWriter err) {
    return workerSupplier.get().processRequest(args).reportAndGetExitCode(err);
  }

  /**
   * Loads a potential flag file and returns the flags. Flag files are only allowed as the last
   * parameter and need to start with an '@'.
   */
  private static String[] expandFlagFile(String[] args) throws IOException {
    if (args.length == 0) {
      return args;
    }

    String lastArg = args[args.length - 1];
    if (lastArg == null || !lastArg.startsWith("@") || lastArg.length() == 1) {
      return args;
    }

    List<String> combinedArgs = new ArrayList<>();
    Collections.addAll(combinedArgs, args);
    combinedArgs.remove(combinedArgs.size() - 1); // Remove flag file
    Iterables.addAll(combinedArgs, getArgsFromFlagFile(lastArg.substring(1)));
    return combinedArgs.toArray(new String[0]);
  }

  private static Iterable<String> getArgsFromFlagFile(String flagFileName) throws IOException {
    String flagFileContent =
        Files.asCharSource(new File(flagFileName), StandardCharsets.UTF_8).read();
    return Splitter.on('\n').omitEmptyStrings().split(flagFileContent);
  }
}

@@ -616,16 +616,72 @@
 bazel;%0A%0A
+import static java.nio.charset.StandardCharsets.UTF_8;%0A%0A
 import c
@@ -976,16 +976,47 @@
 oblems;%0A
+import java.io.BufferedWriter;%0A
 import j
@@ -1052,24 +1052,59 @@
 OException;%0A
+import java.io.OutputStreamWriter;%0A
 import java.
@@ -2799,25 +2799,177 @@
 equest.%0A
+%0A
     
-try (
+// DO NOT close the err PrintWriter, since it will close the System.err stream and swallow%0A    // whatever gets sent to it, e.g. exception stack trace.%0A    
 PrintWri
@@ -2977,16 +2977,24 @@
 er err =
+%0A       
  new Pri
@@ -3006,25 +3006,72 @@
 ter(
-System.err)) %7B%0A  
+new BufferedWriter(new OutputStreamWriter(System.err, UTF_8)));%0A
     
@@ -3141,18 +3141,16 @@
 r);%0A    
-  
 err.flus
@@ -3150,26 +3150,24 @@
 rr.flush();%0A
-  
     System.e
@@ -3181,22 +3181,16 @@
 tCode);%0A
-    %7D%0A
   %7D%0A%0A  p
@@ -3426,24 +3426,30 @@
 ak;%0A      %7D%0A
+      
 %0A      try (

Make BazelWorker not swallow exceptions if the transpiler aborted abnormally.