package nxm.sys.lib;

import java.io.FileOutputStream;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;

import nxm.sys.inc.InternalUseOnly;

/**
  A class to handle running an OS command from the shell.

  Foreign commands are performed in a sub-process.

  If two $$ are entered on the command line the first is stripped
  off and tells NM that this is an OS command.  The second is
  stripped off here and tells NM to run the command as an external
  subprocess.  This causes the current process to exit and reenter--
  so it is only done if explicitly requested.

  @author Jeff Schoen
  @author Neon Ngo 2011-07-21 Add support to save standard output/error of
                              executed OS command into RESULTS variables.
*/
public class Foreign extends Command {
  private static boolean debug = false; // Turns debugging on/off.

  /** Indicates if the running of external commands is permitted. */
  protected boolean canRunExternal;

  /** Creates a new instance where the ability to run external commands is
      turned on by default. This is the same as <code>Foreign(true)</code>.
   */
  public Foreign() {
    this(true);
  }

  /** Creates a new instance where the ability to run external commands is
      explicitly specified. An example of an instance where the running of
      external commands would be turned off would be when a command is being
      run from within a GUI.
      @param canRunExternal Indicates if it is possible to run external commands.
   */
  public Foreign(boolean canRunExternal) {
    this.canRunExternal = canRunExternal;
  }

  /** Run a foreign command. */
  @Override
  public void run () {
    runEnter();
    boolean external = canRunExternal;
    String cmdline = args.getRaw();
    // Strip off the leading $ or $$ and check to see if user is forcing this to being an internal command.
    if (cmdline.charAt(0)=='$') {
      cmdline = cmdline.substring(1);
      if (cmdline.charAt(0)=='$') {cmdline = cmdline.substring(1);
      } else {external = false; }
    }
    String msf = M.results.getString("ENV.MSF");
    if (M.verify!=0) verify();
    try {
      if (external && M.macro==null && msf!=null) {
        runExternal(M,cmdline);
      } else {
        if (id.length()>0) M.registry.put(id,this);
        runInternal(M,cmdline);
        if (id.length()>0) M.registry.remove(id);
      }
    } catch (Exception e) { processException(e); }
    runExit();
  }

  /** Runs a command in "internal" mode (this will not wait for command to finish *if* it ends with "&amp;".
      All I/O will be to/from NeXtMidas.
      @param M       The midas context to use.
      @param cmdline The OS-specific command line.
      @throws MidasException if no privilege to run OS command -or- exception when running OS command
   */
  public static void runInternal (Midas M, String cmdline) {
    runInternal(M, cmdline, false);
  }

  /** Runs a command in "internal" mode. All I/O will be to/from NeXtMidas.
      @param M       The midas context to use (can be null to not display stdout/stderr).
      @param cmdline The OS-specific command line.
      @param wait    Indicates if there is a need to wait for the command to complete. If this is
                     false and the command line ends with "&amp;" then the method will return 0 once
                     the command has been started.
      @return The status from running the command (most commands return 0 on success).
      @throws MidasException if no privilege to run OS command -or- exception when running OS command
      @see Foreign#runInternal(Midas, String, boolean, String, String)
      @since NeXtMidas 2.1.0
   */
  public static int runInternal (Midas M, String cmdline, boolean wait) {
    return runInternal(M, cmdline, wait, null, null);
  }

  /** Runs a command in "internal" mode. All I/O will be to/from NeXtMidas.
      @param M       The midas context to use.
      @param cmdline The OS-specific command line.
      @param wait    Indicates if there is a need to wait for the command to complete. If this is
                     false and the command line ends with "&amp;" then the method will return 0 once
                     the command has been started.
      @param stdoutResname result name to save OS command's standard output to (when not null);
                           when enabled this will prevent printing stdout to terminal.
      @param stderrResname result name to save OS command's standard error to (when  not null);
                           when enabled this will prevent printing stderr to terminal.
      @return The status from running the command (most commands return 0 on success).
      @throws MidasException if no privilege to run OS command -or- exception when running OS command
      @since NeXtMidas 3.0.0
   */
  public static int runInternal (Midas M, String cmdline, boolean wait,
                                 String stdoutResname, String stderrResname) {
    Appendable out = (stdoutResname != null)? new StringBuilder(512) : null;
    Appendable err = (stderrResname != null)? new StringBuilder(256) : null;

    int status = _runInternal(M, cmdline, wait, out, err);

    if (out != null) { M.results.put(stdoutResname, out.toString()); } // since 3.0.0 for BUG 1357
    if (err != null) { M.results.put(stderrResname, err.toString()); } // since 3.0.0 for BUG 1357

    return status;
  }

  /** Runs a command in "internal" mode and waits for it to run to completion. All I/O will be to/from NeXtMidas.
      <br><br>
      This version is intended to be used from within library code (i.e. not from the macro).
      It functions similar to the python <code>commands.getoutput(..)</code>
      function except that stdout and stderr are separate.
      @param cmdline (IN)  The OS-specific command line.
      @param out     (OUT) Buffer to receive output from stdout (null to ignore).
      @param err     (OUT) Buffer to receive output from stderr (null to ignore).
      @return The status from running the command (most commands return 0 on success).
      @throws MidasException if no privilege to run OS command -or- exception when running OS command
      @since NeXtMidas 3.3.1
   */
  @InternalUseOnly("since created")
  public static int runInternal (String cmdline, Appendable out, Appendable err) {
    return _runInternal(null, cmdline, true, out, err);
  }

  /** Runs a command in "internal" mode. All I/O will be to/from NeXtMidas.
      @param M       (I/O) The midas context to use.
      @param cmdline (IN)  The OS-specific command line.
      @param wait    (IN)  Indicates if there is a need to wait for the command to
                           complete. If this is false and the command line ends with
                           "&" then the method will return 0 once the command has been
                           started.
      @param out     (OUT) Buffer to receive output from stdout (null to ignore).
      @param err     (OUT) Buffer to receive output from stderr (null to ignore).
      @return The status from running the command (most commands return 0 on success).
      @throws MidasException if no privilege to run OS command -or- exception when running OS command
      @since NeXtMidas 3.3.1
   */
  @InternalUseOnly
  private static int _runInternal (Midas M, String cmdline, boolean wait,
                                   Appendable out, Appendable err) {
    int state,status=0,bytes;
    int timeout=-1;
    if (!Shell.getPriv(Privileges.PRIV_EXECUTE))
      throw new MidasException("No privilege to run OS command: '"+cmdline+"'");
    boolean bg = cmdline.endsWith("&");
    if (bg) cmdline = cmdline.substring(0,cmdline.length()-1);
    try {
      boolean hasErrOutput = false;
      Runtime runtime = java.lang.Runtime.getRuntime();

      // START: Fix for Bug 7 - Parsing of command string when passed to OS.
      //Process osProcess = runtime.exec(cmdline);
      Parser parser = new Parser(cmdline, ' ');
      parser.clean(false);
      Process osProcess = runtime.exec(parser.getArray(0));
      // When testing try using the following:
      //  nM> $echo "A B C"
      //  nM> $echo "A B C" 1 2 3
      //  nM> $echo -wl,3 "A B C"  (note that the '-wl,3' is a single argument)
      if (debug && (M != null)) {
        String[] array = parser.getArray(0);
        if (array.length > 0) {
          M.type("Foreign.runInternal: command = '"+array[0]+"'");
          for (int ii = 1; ii < array.length; ii++) {
            M.type("                     arg "+ii+" = '"+array[ii]+"'");
          }
        } else {
          M.type("Foreign.runInternal: no command specified");
        }
      }
      // END: Fix for Bug 7

      if (bg && !wait) return 0;

      OutputStream osInput  = osProcess.getOutputStream();
      InputStream  osOutput = osProcess.getInputStream();
      InputStream  osError  = osProcess.getErrorStream();
      for (state=0; state>=0 && timeout!=0;) {
        if ((bytes=osOutput.available()) > 0) {
          byte barray[] = new byte[bytes];
          bytes = osOutput.read(barray);
          if (bytes>0) {
            if (out != null) out.append(new String(barray,0,bytes)); // since 3.0.0 for BUG 1357
            else if (M != null)  M.type(new String(barray,0,bytes));
          }
          state=1;
        }
        if ((bytes=osError.available()) > 0) {
          byte barray[] = new byte[bytes];
          bytes = osError.read(barray);
          if (bytes>0) {
            if (debug && !hasErrOutput && (M != null)) { // print more info for BUG 1312
              hasErrOutput = true;                       // only print the first time around
              M.warning("Got stderr running: "+cmdline);
            }

            if (err != null) err.append(new String(barray,0,bytes)); // since 3.0.0 for BUG 1357
            else if (M != null)  M.type(new String(barray,0,bytes)); // since 2.9.3
          }
          state=1;
        }
        try {
          status = osProcess.exitValue();
          // check for any output left in buffer after exit
          if (osOutput.available()>0 || osError.available()>0) state=0; else state=-1;
        }
        catch (IllegalThreadStateException e) {
          if (state==0) { Time.sleep(0.1); timeout--; }
          else state=0;
        }
      }
      osInput.close();
      osOutput.close();
      osError.close();
    }
    catch (Exception e) {
      throw new MidasException("Running OS cmd: " + cmdline, e);
    }
    return status;
  }

  /** Runs a command in "external" mode. All I/O will be to/from the OS's terminal.
      Note: this method is currently incompatible with nxm.sys.libg GTerminal.
      @param M       The midas context to use.
      @param cmdline The OS-specific command line.
   */
  public static void runExternal (Midas M, String cmdline) {
    String msf = M.results.getString("ENV.MSF");
    if (msf==null) {
      throw new MidasException("No ENV.MSF for external OS cmd: '" + cmdline + "'");
    }
    msf = msf+".bat";
    try {
      // JC4: Need to open the output stream in append mode to support builds
      //      which have many commands in the file. To support this we need to
      //      make sure that the file is removed once it has been run
      //      (this needs to be done in the NM shell script for each OS).
      FileOutputStream fos = new FileOutputStream(msf, true);
      PrintWriter pw = new PrintWriter(fos);
      if ("DOS".equals(Shell.ostype)) pw.println(cmdline);
      else if ("VMS".equals(Shell.ostype)) pw.println("$"+cmdline);
      else pw.println(cmdline);
      pw.close();
      M.status = EXIT;
    } catch (Exception e) {
      throw new MidasException("Error writing OS file to "+msf, e);
    }
  }

  public String toString() {
    return "O: "+args.cmdline;
  }

  /** Returns whether or not NeXtMidas debugging is enabled
      @return debug state true or false (on or off)
      @since 3.0.0
   */
  public static boolean getDebug () { return debug; }
  /** Enables or disables NeXtMidas debugging
      @param val new state for NeXtMidas debugging
      @since 2.9.3 */
  public static void setDebug (boolean val) { debug = val; }
}
