/**
 * Copyright (C) 2009 STMicroelectronics
 *
 * This file is part of "Mind Compiler" is free software: you can redistribute 
 * it and/or modify it under the terms of the GNU Lesser General Public License 
 * as published by the Free Software Foundation, either version 3 of the 
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT 
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
 * details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 * Contact: mind@ow2.org
 *
 * Authors: Matthieu Leclercq
 * Contributors: 
 */

package org.ow2.mind.compilation;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;

import org.objectweb.fractal.adl.ADLException;
import org.objectweb.fractal.adl.CompilerError;
import org.objectweb.fractal.adl.error.GenericErrors;
import org.objectweb.fractal.adl.util.FractalADLLogManager;

/**
 * This helper class provides method to execute external commands.
 */
public final class ExecutionHelper {
  private ExecutionHelper() {
  }

  public static class ExecutionResult {
    final int    rValue;
    final String output;

    protected ExecutionResult(final int rValue, final StringBuilder output) {
      this.rValue = rValue;
      if (output.length() == 0)
        this.output = null;
      else
        this.output = output.toString();
    }

    public int getExitValue() {
      return rValue;
    }

    public String getOutput() {
      return output;
    }
  }

  // The io logger
  protected static Logger logger = FractalADLLogManager.getLogger("io");

  /**
   * Executes the given command line and returns the exit value. The given
   * command is splited on space character boundary (this method is equivalent
   * to <code>exec(execTitle, command.split("\\s"))</code>)<br>
   * Note that the {@link #exec(String, List)} method is safer since it while
   * not split command line on space character boundary which may produce
   * unexpected result if arguments contains spaces.
   * 
   * @param command the command to execute.
   * @return if its exit value is zero, return null, otherwise returns a string
   *         that contains the process output.
   * @throws ADLException If an error occurs while running the command.
   * @throws InterruptedException if the calling thread has been interrupted
   *           while waiting for the process to finish.
   * @see #exec(String, List)
   */
  public static ExecutionResult exec(final String command) throws ADLException,
      InterruptedException {
    return exec(null, command);
  }

  /**
   * Executes the given command line and returns the exit value. The given
   * command is splited on space character boundary, unless the space is escaped
   * by a backslash.<br>
   * Note that the {@link #exec(String, List)} method is safer since it while
   * not split command line on space character boundary which may produce
   * unexpected result if arguments contains spaces.
   * 
   * @param execTitle the message to be logged as a header of the execution. May
   *          be <code>null</code>.
   * @param command the command to execute.
   * @return if its exit value is zero, return null, otherwise returns a string
   *         that contains the process output.
   * @throws ADLException If an error occurs while running the command.
   * @throws InterruptedException if the calling thread has been interrupted
   *           while waiting for the process to finish.
   * @see #exec(String, List)
   */
  public static ExecutionResult exec(final String execTitle,
      final String command) throws ADLException, InterruptedException {
    return exec(execTitle, DirectiveHelper.splitOptionString(command));
  }

  /**
   * Executes the given command line and returns the exit value.<br>
   * This method will issue some messages on the <code>io</code> logger. If the
   * {@link Level#FINE FINE} level is enabled, the full command line will be
   * logged. If the {@link Level#INFO INFO} level is enabled, the given
   * <code>execTitle</code> will be logged.
   * 
   * @param execTitle the message to be logged as a header of the execution. May
   *          be <code>null</code>.
   * @param cmdList the command to execute.
   * @return if its exit value is zero, return null, otherwise returns a string
   *         that contains the process output.
   * @throws ADLException If an error occurs while running the command.
   * @throws InterruptedException if the calling thread has been interrupted
   *           while waiting for the process to finish.
   */
  public static ExecutionResult exec(final String execTitle,
      final List<String> cmdList) throws ADLException, InterruptedException {
    final boolean titleLogged;
    if (logger.isLoggable(Level.INFO) && execTitle != null) {
      logger.info(execTitle);
      titleLogged = true;
    } else {
      titleLogged = false;
    }

    if (logger.isLoggable(Level.FINE)) {
      String command = "";
      for (final String cmd : cmdList) {
        command += cmd + " ";
      }
      logger.fine(command);
    }

    final Process process;
    try {
      process = new ProcessBuilder(cmdList).redirectErrorStream(true).start();
    } catch (final IOException e1) {
      throw new ADLException(CompilerErrors.EXECUTION_ERROR, cmdList.get(0));
    }

    final StringBuilder processOutput = new StringBuilder();
    // Read output produced by process in a parallel thread in order to avoid
    // the process to block
    final Thread readerThread = new Thread() {
      @Override
      public void run() {
        // append output and error stream on the processOutput.
        final BufferedReader reader = new BufferedReader(new InputStreamReader(
            process.getInputStream()));
        try {
          String line = reader.readLine();
          if (line != null) {
            // if the title has not been printed yet.
            if (!titleLogged) {
              String command = "";
              for (final String cmd : cmdList) {
                command += cmd + " ";
              }
              logger.severe((execTitle == null) ? command : execTitle);
            }
            do {
              logger.severe(line);
              processOutput.append(line).append("\n");
              line = reader.readLine();
            } while (line != null);
          }
          reader.close();
        } catch (final IOException e) {
          throw new CompilerError(GenericErrors.INTERNAL_ERROR, e,
              "Can't read error stream of process");
        }
      }
    };

    readerThread.start();
    final int rValue = process.waitFor();
    readerThread.join();

    return new ExecutionResult(rValue, processOutput);
  }

  /**
   * Executes the given command line and returns the exit value.
   * 
   * @param execTitle the message to be logged as a header of the execution. May
   *          be <code>null</code>.
   * @param cmdArray the command to execute.
   * @return if its exit value is zero, return null, otherwise returns a string
   *         that contains the process output.
   * @throws ADLException If an error occurs while running the command.
   * @throws InterruptedException if the calling thread has been interrupted
   *           while waiting for the process to finish.
   * @see #exec(String, List)
   * @see Runtime#exec(String[])
   */
  public static ExecutionResult exec(final String execTitle,
      final String[] cmdArray) throws ADLException, InterruptedException {
    return exec(execTitle, Arrays.asList(cmdArray));
  }
}
