/*
 * 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 org.apache.hadoop.maven.plugin.cmakebuilder;

import java.util.Locale;
import org.apache.hadoop.maven.plugin.util.Exec.OutputBufferThread;
import org.apache.hadoop.maven.plugin.util.Exec;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugins.annotations.LifecyclePhase;
import org.apache.maven.plugins.annotations.Mojo;
import org.apache.maven.plugins.annotations.Parameter;

import java.io.File;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Goal which builds the native sources.
 */
@Mojo(name="cmake-compile", defaultPhase = LifecyclePhase.COMPILE)
public class CompileMojo extends AbstractMojo {
  private static int availableProcessors =
      Runtime.getRuntime().availableProcessors();

  /**
   * Location of the build products.
   */
  @Parameter(defaultValue="${project.build.directory}/native")
  private File output;

  /**
   * Location of the source files.
   * This should be where the sources are checked in.
   */
  @Parameter(defaultValue="${basedir}/src/main/native", required=true)
  private File source;

  /**
   * CMake build target.
   */
  @Parameter
  private String target;

  /**
   * Environment variables to pass to CMake.
   *
   * Note that it is usually better to use a CMake variable than an environment
   * variable.  To quote the CMake FAQ:
   *
   * "One should avoid using environment variables for controlling the flow of
   * CMake code (such as in IF commands). The build system generated by CMake
   * may re-run CMake automatically when CMakeLists.txt files change. The
   * environment in which this is executed is controlled by the build system and
   * may not match that in which CMake was originally run. If you want to
   * control build settings on the CMake command line, you need to use cache
   * variables set with the -D option. The settings will be saved in
   * CMakeCache.txt so that they don't have to be repeated every time CMake is
   * run on the same build tree."
   */
  @Parameter
  private Map<String, String> env;

  /**
   * CMake cached variables to set.
   */
  @Parameter
  private Map<String, String> vars;

  // TODO: support Windows
  private static void validatePlatform() throws MojoExecutionException {
    if (System.getProperty("os.name").toLowerCase(Locale.ENGLISH)
        .startsWith("windows")) {
      throw new MojoExecutionException("CMakeBuilder does not yet support " +
          "the Windows platform.");
    }
  }

  public void execute() throws MojoExecutionException {
    long start = System.nanoTime();
    validatePlatform();
    runCMake();
    runMake();
    long end = System.nanoTime();
    getLog().info("cmake compilation finished successfully in " +
          TimeUnit.MILLISECONDS.convert(end - start, TimeUnit.NANOSECONDS) +
          " millisecond(s).");
  }

  /**
   * Validate that source parameters look sane.
   */
  static void validateSourceParams(File source, File output)
      throws MojoExecutionException {
    String cOutput = null, cSource = null;
    try {
      cOutput = output.getCanonicalPath();
    } catch (IOException e) {
      throw new MojoExecutionException("error getting canonical path " +
          "for output", e);
    }
    try {
      cSource = source.getCanonicalPath();
    } catch (IOException e) {
      throw new MojoExecutionException("error getting canonical path " +
          "for source", e);
    }

    // This doesn't catch all the bad cases-- we could be following symlinks or
    // hardlinks, etc.  However, this will usually catch a common mistake.
    if (cSource.startsWith(cOutput)) {
      throw new MojoExecutionException("The source directory must not be " +
          "inside the output directory (it would be destroyed by " +
          "'mvn clean')");
    }
  }

  public void runCMake() throws MojoExecutionException {
    validatePlatform();
    validateSourceParams(source, output);

    if (output.mkdirs()) {
      getLog().info("mkdirs '" + output + "'");
    }
    List<String> cmd = new LinkedList<String>();
    cmd.add("cmake");
    cmd.add(source.getAbsolutePath());
    for (Map.Entry<String, String> entry : vars.entrySet()) {
      if ((entry.getValue() != null) && (!entry.getValue().equals(""))) {
        cmd.add("-D" + entry.getKey() + "=" + entry.getValue());
      }
    }
    cmd.add("-G");
    cmd.add("Unix Makefiles");
    String prefix = "";
    StringBuilder bld = new StringBuilder();
    for (String c : cmd) {
      bld.append(prefix).append(c);
      prefix = " ";
    }
    getLog().info("Running " + bld.toString());
    getLog().info("with extra environment variables " + Exec.envToString(env));
    ProcessBuilder pb = new ProcessBuilder(cmd);
    pb.directory(output);
    pb.redirectErrorStream(true);
    Exec.addEnvironment(pb, env);
    Process proc = null;
    OutputBufferThread outThread = null;
    int retCode = -1;
    try {
      proc = pb.start();
      outThread = new OutputBufferThread(proc.getInputStream());
      outThread.start();

      retCode = proc.waitFor();
      if (retCode != 0) {
        throw new MojoExecutionException("CMake failed with error code " +
            retCode);
      }
    } catch (IOException e) {
      throw new MojoExecutionException("Error executing CMake", e);
    } catch (InterruptedException e) {
      throw new MojoExecutionException("Interrupted while waiting for " +
          "CMake process", e);
    } finally {
      if (proc != null) {
        proc.destroy();
      }
      if (outThread != null) {
        try {
          outThread.interrupt();
          outThread.join();
        } catch (InterruptedException e) {
          getLog().error("Interrupted while joining output thread", e);
        }
        if (retCode != 0) {
          for (String line : outThread.getOutput()) {
            getLog().warn(line);
          }
        }
      }
    }
  }

  public void runMake() throws MojoExecutionException {
    List<String> cmd = new LinkedList<String>();
    cmd.add("make");
    cmd.add("-j");
    cmd.add(String.valueOf(availableProcessors));
    cmd.add("VERBOSE=1");
    if (target != null) {
      cmd.add(target);
    }
    StringBuilder bld = new StringBuilder();
    String prefix = "";
    for (String c : cmd) {
      bld.append(prefix).append(c);
      prefix = " ";
    }
    getLog().info("Running " + bld.toString());
    ProcessBuilder pb = new ProcessBuilder(cmd);
    pb.directory(output);
    Process proc = null;
    int retCode = -1;
    OutputBufferThread stdoutThread = null, stderrThread = null;
    try {
      proc = pb.start();
      stdoutThread = new OutputBufferThread(proc.getInputStream());
      stderrThread = new OutputBufferThread(proc.getErrorStream());
      stdoutThread.start();
      stderrThread.start();
      retCode = proc.waitFor();
      if (retCode != 0) {
        throw new MojoExecutionException("make failed with error code " +
            retCode);
      }
    } catch (InterruptedException e) {
      throw new MojoExecutionException("Interrupted during Process#waitFor", e);
    } catch (IOException e) {
      throw new MojoExecutionException("Error executing make", e);
    } finally {
      if (stdoutThread != null) {
        try {
          stdoutThread.join();
        } catch (InterruptedException e) {
          getLog().error("Interrupted while joining stdoutThread", e);
        }
        if (retCode != 0) {
          for (String line: stdoutThread.getOutput()) {
            getLog().warn(line);
          }
        }
      }
      if (stderrThread != null) {
        try {
          stderrThread.join();
        } catch (InterruptedException e) {
          getLog().error("Interrupted while joining stderrThread", e);
        }
        // We always print stderr, since it contains the compiler warning
        // messages.  These are interesting even if compilation succeeded.
        for (String line: stderrThread.getOutput()) {
          getLog().warn(line);
        }
      }
      if (proc != null) {
        proc.destroy();
      }
    }
  }
}
