package nxm.sys.lib;

import java.util.Arrays;
import java.util.List;
import java.util.Map;

import nxm.sys.inc.AsciiMap;
import nxm.sys.inc.DataTypes;
import nxm.sys.inc.Debug;
import nxm.sys.inc.InternalUseOnly;
import nxm.sys.lib.Table.Function;

/**
 * Base class for macros including the just-in-time translator.
 * @author Jeff Schoen
 */
public class Macro extends Command implements Cloneable {

  /** Table of jump indices for procedures and labels. */
  @InternalUseOnly
  public Table jumps;

  /** Array of all commands in the macro. */
  @InternalUseOnly
  public Args[] lines;

  /** Control flow last command index for Intrinsic use only (i.e. index of very last command in macro). */
  @InternalUseOnly
  public int lastIndex = 0;

  /** Control flow next command index (to execute) for Intrinsic use only. */
  @InternalUseOnly
  public int nextIndex;

  /** Optional command to hang completion on. */
  @InternalUseOnly
  public String waitFor = null;

  /**
   * Full macro file name. Since 3.3.0: protected vs public, it was deprecated in 2.7.1 use {@link #getFileName()}
   * instead.
   */
  protected String filename;

  /** Procedure stack level. */
  @InternalUseOnly
  public int level, returns[];

  /** Try/Trap stack level. */
  private int traps[];

  private Message trapMsg;

  private boolean trapQuiet;

  /** Reference to local results tables. */
  @InternalUseOnly
  public Results results[];

  /** Reference to table of controls. */
  @InternalUseOnly
  public Table controls;

  private int closeLevel;

  private int maxIndex = 1024, maxLevel = 64;

  private int processMessageIndex, processExceptionIndex;

  private String textNext = null;

  private Midas midas1, midas2;

  private KeyVector primitives = new KeyVector(16); // Primitives run by processing engine

  private boolean hasMessageProcedures;

  private Shell suspendShell;

  /** suspend */
  @InternalUseOnly
  public int suspend;

  // Txt macro support
  private boolean txt;

  private int staticCount = 1;

  private Table sysctags;

  private int nTimers = 0;

  private double timeLast;
  private double[] timer, timerinit;

  /** The message processing flag. */
  private boolean processingMessage = false;

  /** The message exception processing flag. */
  private boolean processingException = false;

  /** Is message debug enabled via /MSGDEBUG=mask (since 2.7.1 for BUG 1875) */
  private boolean msgDebug;

  /** Message debug level via /MSGDEBUG=mask (since 2.7.1 for BUG 1875) */
  private String debugLevel = "DEFAULT";

  /** List of items that child macro can inherit from parent macro via /ShareMask= */
  public static final String shareList = "PIPES,RESULTS"; // Other options are TBD

  /** Default of what is shared between parent and child macro */
  public static final int SHARE_DEFAULT = 0x0;

  /** Share pipes between parent and child macro (since 2.7.3) */
  public static final int SHARE_PIPES = 0x1;

  /**
   * <b>INTERNAL USA ONLY:</b> Share results (and hence registry and pipes) between parent and child macro. It is used
   * to allow loop commands like FOREACH, DO, and WHILE from the NeXtMidas shell to use the same results table.
   */
  @InternalUseOnly
  public static final int SHARE_RESULTS = 0x2; // since 3.1.0 for BUG 2454

  /** What is shared between parent and child macros. Set via the /ShareMask switch. */
  private int shareMask = SHARE_DEFAULT; // since 3.1.2: added for Bug 2586 so PIPE command can check value of switch

  /** default constructor */
  public Macro () {}

  /**
   * Load macro text and initialize the macro context. Always returns {@link #INIT}.
   * @param midas The Midas context to use.
   */
  public int checkin (Midas midas) {
    super.checkin(midas);

    shareMask = MA.getOptionMask("/SHAREMASK", shareList, SHARE_DEFAULT); // since 2.7.3 (BUG 767)

    midas1 = midas;
    // midas2 = cloneContext(midas1,false, sharePipes); // in 3.1.0: changed to line below to use new method to pass in
    // multiple flags (Bug 2454)
    midas2 = cloneContext(midas1, false, shareMask);
    midas2.macro = this;
    midas2.results.put("THIS", this);

    lines = new Args[maxIndex];
    returns = new int[maxLevel];
    traps = new int[maxLevel];
    results = new Results[maxLevel];
    jumps = new Table();
    controls = new Table();
    lastIndex = 0;

    if (MA.find("/MFN")) {
      filename = args.getCS("/MFN");
      args.remove("/MFN");
    }
    else {
      filename = getFileName(M, name, option);
    }

    setTimers(1);
    return INIT;
  }

  /** Execute commands up to first pipe run. Always returns {@link #NORMAL}. */
  public int open () {
    super.checkin(midas2); // switch to macro context
    // 2011-03-11 NTN: same verbose behavior for Macro commands (e.g. path.mm) run from
    // interactive shell (to be true) as other command types
    verbose = MA.getState("/VERBOSE", (verbose || M.parent.macro == null));

    msgDebug = MA.isPresent("/MSGDEBUG"); // get the debug flag
    if (msgDebug) {
      String dlvl = MA.getS("/MSGDEBUG", debugLevel);
      if (!(dlvl.equals(""))) debugLevel = dlvl;
    }

    level = 0; // set to root level
    returns[0] = -1; // root level jump back for procedures
    results[0] = MR; // root level returns back for procedures
    if (lastIndex == 0) loadText(); // read in commands
    if (lastIndex <= 0) return ABORT; // error loading, all done
    primitives.clear(); // init processing engine

    argsToResults(M, this.args, lines[1], 0); // calling args to results entries

    processMessageIndex = lookupIndex("PROCESSMESSAGE");
    processExceptionIndex = lookupIndex("PROCESSEXCEPTION");

    for (Table.Iterator ti = jumps.iterator(); ti.getNext();) {
      hasMessageProcedures |= (ti.key.startsWith("PROCESS") &&
          ti.key.endsWith("MESSAGE") && !ti.key.equals("PROCESSMESSAGE"));
    }

    nextIndex = 1; // start at StartMacro
    int index = execBranch(nextIndex, 0); // run the setup code
    if (index < 0) return FINISH; // no piping, all done
    return NORMAL;
  }

  /**
   * Process waits for pipe to finish and continues executing from there.
   * @return {@link #FINISH} if the macro is done, {@link #NORMAL} if the macro is still in the process of executing and
   *         {@link #NOOP} if no commands in the macro are executed.
   */
  public int process () {

    int work = 0; // any work for this processing engine ?
    for (int i = 0; i < primitives.size(); i++) {
      Command cmd = (Command) primitives.get(i);
      if (cmd.state == Command.DONE || cmd.state == Command.EXIT) continue;
      int status = cmd.runSingle();
      if (status == NORMAL) work++;
      if (M.pipeMode == Midas.POFF && cmd.state != DONE) {
        cmd.close();
        cmd.state = DONE;
      }
      if (cmd.state == DONE || cmd.state == Command.EXIT) {
        cmd.runExit();
        cmd.checkout();
      }
    }

    if (M.pipeMode == Midas.POFF) {
      int index = execBranch(nextIndex, 0); // run the setup code
      if (index < 0)
        return FINISH; // no piping, all done
      else
        return NORMAL;
    }
    else {
      if (waitFor != null && M.registry.get(waitFor) == null) setPipeMode(Midas.PSTOP);
      if (waitFor == null && M.registry.table.size() <= staticCount) setPipeMode(Midas.PSTOP);
    }

    // Any timers ?
    if (nTimers > 0 && !(nTimers == 1 && timer[0] == 0)) processTimers();

    return (work > 0) ? NORMAL : NOOP;
  }

  private void processTimers () {
    double time = Time.current();
    double tdiff = time-timeLast; // processTimers is called about 0.01 seconds
    boolean trst = (tdiff>10 || tdiff<-10); // a diff this big indicates a system clock slip
    // send timer message with timer number in the info field
    for (int i = 0; i < nTimers; i++) {
      if (trst) timerinit[i] = time;    // reset timer assuming system clock NTP slips
      else if (timer[i] != 0 && (time-timerinit[i]) > Math.abs(timer[i])) {
        MQ.put("TIMER", i, Double.valueOf(time), this, this);
        if (timer[i] > 0)
          timerinit[i] = time; // renew interval
        else
          timer[i] = 0; // terminate countdown
      }
    }
    timeLast = time;
  }

  /**
   * If processingMessage, any ERROR messages are subject to trapping. We never put an ERROR message on the queue.
   */
  public int processMessage (Message msg) {
    MessageQueue safeMQ = MQ; // since 3.1.1: cached MQ ref, to make thread safe during macro shutdown

    if (msg.name.equals("ERROR")) {
      if (processingException)
        M.printStackTrace((Exception) msg.getData());
      else
        if (processingMessage)
          trapMsg = msg;
        else
          if (thisIsMe())
            processTrap(msg);
          else
            if (safeMQ != null)
              safeMQ.put(msg);
            else { // since 3.1.1: print warning. Using System.out since M is probably null too (BUG 2537)
              System.out.println("WARN: Macro.processMessage(ERROR msg) - MQ is null, cannot queue/process " + msg);
            }
      return NORMAL;
    }
    else
      if (!thisIsMe() || processingMessage) {
        if (safeMQ != null) {
          safeMQ.put(msg);
        }
        else { // since 3.1.1: print warning. Using System.out since M is probably null too (BUG 2537)
          System.out.println("WARN: Macro.processMessage() - MQ is null, cannot queue/process " + msg);
        }
        return NORMAL;
      }

    if (msgDebug) M.info(getName() + " received " + msg.toString(debugLevel));
    int handlerIndex = hasMessageProcedures ? lookupIndex("PROCESS" + msg.name + "MESSAGE") : 0;
    processingMessage = true;

    if (msg.name.equals("EXEC")) {
      execBranch(msg.info, 1);
    }
    else
      if (handlerIndex > 0) {
        M.results.put("MSG", msg);
        execBranch(handlerIndex + 1, 1); // skip PROCEDURE cmd for speed
      }
      else
        if (msg.name.equals("EXIT")) {
          setPipeMode(Midas.PSTOP);
        }
        else
          if (processMessageIndex > 0) {
            M.results.put("MSG", msg);
            execBranch(processMessageIndex + 1, 1); // skip PROCEDURE cmd for speed
          }
          else
            if (msg.name.equals("MACRO")) {
              String text = (String) msg.data;
              if (text.equals("INIT"))
                M.pipeMode = Midas.PINIT;
              else
                if (text.equals("RUN"))
                  M.pipeMode = Midas.PRUN;
                else
                  if (text.equals("PAUSE"))
                    M.pipeMode = Midas.PPAUSE;
                  else
                    if (text.equals("STOP"))
                      M.pipeMode = Midas.PSTOP;
                    else
                      if (text.equals("EXIT")) M.pipeMode = Midas.POFF;
            }
    processingMessage = false;
    return NORMAL;
  }

  /** set trap handler
   *
   *  @param label trap handler label
   *  @param quiet true for trap quiet
   */
  public void setTrapHandler (String label, boolean quiet) {
    int index = label.equals("OFF") ? 0 : lookupIndex(label);
    if (index < 0) M.warning("Unresolved trap handler: " + label);
    traps[level] = index;
    trapQuiet = quiet;
  }

  private void processTrap (Message msg) {
    M.results.put("EMSG", msg);
    if (processExceptionIndex > 0) {
      processingException = true;
      execBranch(processExceptionIndex + 1, 1);
      processingException = false;
      if (msg.name.equals("CAUGHT")) return; // handled by processException
    }
    else {
      M.printStackTrace((Exception) msg.getData());
    }
    if (processingMessage && traps[level] > 0) {
      if (!trapQuiet) M.println("Trapped ERROR to line: " + traps[level]); // since 3.7.1 using M.println to allow
                                                                           // logging of interactive commands NXM-2813
      nextIndex = traps[level] + 1;
    }
  }

  public void processException (Exception e) {
    processingMessage = false;
    if (midas1.macro != null) { // let the parent macro decide
      midas1.macro.MQ.put("ERROR", 0, e, midas1.macro, this);
      try {
        close();
      } catch (Exception ec) {
        /* Do Nothing */ }
    }
    else
      super.processException(e);
  }

  /** Finish processing. Always returns {@link #NORMAL} */
  public int close () {
    if (M.logger != null) M.logger.close();
    M.macro = null;
    super.checkin(midas1); // back to parent context
    return NORMAL;
  }

  public void setState (int state) {
    switch (state) {
      case START:
      case RESUME:
        M.pipeMode = Midas.PRUN;
        break;
      case STOP:
      case PAUSE:
        M.pipeMode = Midas.PPAUSE;
        break;
      case FINISH:
      case ABORT:
        M.pipeMode = Midas.PSTOP;
        break;
    }
  }

  /** Finish processing (after pipe off). */
  public void checkout () {
    for (int i = 1; i < lastIndex; i++) {
      if (lines[i].support == 'I' && lines[i].cmd.state != PROCESS) lines[i].cmd.checkout();
    }
    lines = null;
    super.checkout();
  }

  /**
   * Sets the pipe mode.
   * @param mode The pipe mode. This should be one of: {@link Midas#PINIT},{@link Midas#PRUN},{@link Midas#PWAIT},
   *          {@link Midas#PSTOP} or {@link Midas#POFF}.
   */
  public void setPipeMode (int mode) {
    if (mode == Midas.PINIT) {
      int index = lookupIndex("INIT");
      if (index > 0) execBranch(index, 1);
    }
    else
      if (mode == Midas.PRUN) {
      }
      else
        if (mode == Midas.PWAIT) {
          int index = lookupIndex("OPEN");
          if (index > 0) execBranch(index, 1);
        }
        else
          if (mode == Midas.PSTOP) {
            int index = lookupIndex("CLOSE");
            if (index > 0) execBranch(index, 1);
            nextIndex = returns[closeLevel];
            level = closeLevel - 1;
            mode = Midas.POFF;
          }
          else
            if (mode == Midas.POFF) {
            }
    M.pipeMode = mode;
  }

  /**
   * Prepare a new results table without sharing pipes.
   * @param midas1 The Midas context with the results table to clone.
   * @param sub If <code>true</code> indicates that the new Midas context created should simply point to
   *          <code>midas1.registry.table</code> and <code>midas1.queue</code>; if <code>false</code> the new Midas
   *          context will use a new registry table which has <code>midas1.registry.table.getRoot()</code> as its root.
   * @return A clone of <code>midas1</code> with its context cloned.
   */
  public static Midas cloneContext (Midas midas1, boolean sub) {
    return cloneContext(midas1, sub, false);
  }

  /**
   * Prepare a new results table with an option to share pipes..
   * @param midas1 The Midas context with the results table to clone.
   * @param sub If <code>true</code> indicates that the new Midas context created should simply point to
   *          <code>midas1.registry.table</code> and <code>midas1.queue</code>; if <code>false</code> the new Midas
   *          context will use a new registry table which has <code>midas1.registry.table.getRoot()</code> as its root.
   * @param sharePipes true if share pipes
   * @return A clone of <code>midas1</code> with its context cloned.
   * @since NeXtMidas 2.7.3
   * @deprecated Since NeXtMidas 3.1.2 use {@link #cloneContext(Midas, boolean, int)}
   */
  @Deprecated
  @InternalUseOnly
  public static Midas cloneContext (Midas midas1, boolean sub, boolean sharePipes) {
    int flags = sharePipes ? (SHARE_DEFAULT | SHARE_PIPES) : (SHARE_DEFAULT & ~SHARE_PIPES);
    return cloneContext(midas1, sub, flags);
  }

  /**
   * Prepare a new results table with an option to share pipes..
   * @param midas1 The Midas context with the results table to clone.
   * @param sub If <code>true</code> indicates that the new Midas context created should simply point to
   *          <code>midas1.registry.table</code> and <code>midas1.queue</code>; if <code>false</code> the new Midas
   *          context will use a new registry table which has <code>midas1.registry.table.getRoot()</code> as its root.
   * @param flags mask value of the SHARE values. See {@link #shareList}. If sharing pipes, the cloned Midas context
   *          shares the pipes of the parent. If sharing results, the cloned Midas context shares the results
   * @return A clone of <code>midas1</code> with its context cloned.
   * @since NeXtMidas 3.1.0
   */
  @InternalUseOnly
  public static Midas cloneContext (Midas midas1, boolean sub, int flags) {
    // flags param added for Bug 2454
    Results results1 = midas1.results;
    Midas midas2 = midas1.cloneOf();
    Results results2 = new Results(midas2);
    results2.setPrevious(results1);
    if ((flags & SHARE_RESULTS) != 0) {
      midas2.results = midas1.results; // since 3.1.0 for BUG 2454
    }
    else
      midas2.results = results2;
    if (sub) {
      midas2.registry.table = midas1.registry.table;
      midas2.queue = midas1.queue;
    }
    else {
      midas2.pipeMode = Midas.POFF;
      midas2.registry.table = new Table();
      midas2.registry.table.setPrevious(midas1.registry.table.getRoot()); // <--- TODO: Ask J why set previous? See
                                                                          // issue in Bug 2687
      if ((flags & SHARE_PIPES) != 0) {
        midas2.pipes = midas1.pipes; // since 2.7.3 for BUG 767
      }
      else {
        midas2.pipes = new Table();
        midas2.pipes.setPrevious(midas1.pipes);
        midas2.pipes.setFlags(0); // since 3.1.2: to NOT allow child macros see parent's pipes (by default)
                                  // that was unintentionally enabled in 2.9.2 by fix to BUG 2333 &
                                  // BUG 2334 when Table.containsKey(.) method was fixed.
      }
      results2.setFlags(0); // don't see parent results
    }
    results2.put("ENV", results1.get("ENV"));
    results2.put("AUX", results1.get("AUX"));
    results2.put("OPT", results1.get("OPT"));
    results2.put("RAM", midas2.pipes);
    results2.put("REG", midas2.registry.table);
    return midas2;
  }

  /**
   * Execute a branch of commands.
   * @param index The branch index to begin execution with.
   * @param mode The execution mode. If this is <code>1</code> then move up one level without any execution.
   * @return The new <code>index</code> value.
   */
  public int execBranch (int index, int mode) {
    int curlevel = level;
    if (mode == 1) {
      level++;
      results[level] = M.results;
      returns[level] = nextIndex;
      traps[level] = 0;
    }
    while (index > 0 && index < maxIndex) {
      nextIndex = index + 1;
      Args targs = lines[index];
      // NXM-2626 Check if state is abort to stop macro
      if (this.state == Command.ABORT) {
        this.setState(ABORT);
        break;
      }
      // NXM-2626 If targs is null break out the while loop, no more args to process
      if (targs != null) {
        char sup = targs.support;

        if (sup == 'I') {
          Shell.execCommand(M, targs.cmd);
        }
        else {
          // evaluate carets and load
          if (sup == 'U' || targs.cmdline.indexOf('^') >= 0) {
            String cmdline = targs.evaluateCarets(targs.cmdline);
            targs = Shell.parseCommand(M, cmdline);
          }
          targs.cmd = Shell.loadCommand(M, targs);
          Shell.execCommand(M, targs.cmd);
        }
        if (suspend != 0) {
          if (suspend > 0)
            M.info("Entering macro suspend mode at line=" + index);
          else
            suspend = -suspend;
          if (suspendShell == null) suspendShell = new Shell();
          suspendShell.M = this.M;
          suspendShell.runCommand("MACROFUNC,,LIST,5");
          suspendShell.process();
        }
        // if we trapped a message while executing, handle and branch
        if (trapMsg != null && !processingException) { // check for processingException added for Bug 1897
          processTrap(trapMsg);
          trapMsg = null;
        }
        index = nextIndex;
        if (mode == 1 && level == curlevel) break;

        if (mode == 0 && M.pipeMode == Midas.PWAIT) {
          closeLevel = ++level; // add fake level to hold nextIndex after WAIT
          results[level] = M.results;
          returns[level] = nextIndex;
          break;
        }
      }
      else {
        break;
      }
    }
    return index;
  }

  /**
   * Add a command for the processing engine.
   * @param cmd A command to be run by the processing engine.
   */
  void addCommand (Command cmd) {
    cmd.setCmdProcEngine(M.macro, Thread.currentThread());
    cmd.runEnter();
    cmd.runSingle();
    primitives.add(cmd.getID(), cmd);
  }

  /**
   * Prep context of a subroutine branch.
   * @param index The branch index to prep.
   * @param subM The subset Midas context to use.
   * @return The value of <code>index</code>.
   */
  public int prepBranch (int index, Midas subM) {
    for (int i = index; i < lastIndex; i++) {
      Args args = lines[i];
      args.M = subM;

      if (args.support == 'I') args.cmd.checkin(subM);

      if (i == index)
        ; /* Do Nothing */
      else
        if (args.name.equals("SUBROUTINE"))
          break;
        else
          if (args.name.equals("PROCEDURE")) break;
    }
    return index;
  }

  /**
   * Translate text file into an array of commandable objects.
   * @return The number of commandable objects translated or <code>-1</code> if the file can not be opened or if the
   *         file has a syntax error.
   */
  public int loadText () {
    String text;

    int iflevel = 0;
    int dolevel = 0;
    int looplevel = 0;
    int forlevel = 0;
    int whilelevel = 0;
    int macrolevel = 0;

    int ifindex[] = new int[100];
    int doindex[] = new int[100];
    int loopindex[] = new int[100];
    int forindex[] = new int[100];
    int whileindex[] = new int[100];

    int i = 0;
    lines[i++] = args;

    Args largs = args; // last command
    boolean autoInitialEntry = false;

    TextFile file = new TextFile(M, filename);

    // if no .mm, look for .txt
    if (!file.exists()) {
      file.setExt("txt");
      txt = file.exists();
      if (!txt) file.setExt("mm");
    }

    if (txt) {
      cwidgets = new KeyVector();
      TextFile tf = new TextFile(this, "nxm.sys.cfg.ctags.tbl");
      sysctags = new Table(tf);
    }

    if (!file.open(TextFile.INPUT | TextFile.FORCEABORT)) return -1;

    file.setIncludeString("include");

    // try/finally block added to the code below to ensure the file gets closed (Bug 2405)
    try {

      while ((text = readFiltered(file)) != null) {

        if ((M.debug & Debug.MACRO) != 0) M.info("" + i + " " + text);

        Args targs = Shell.parseCommand(M, text);
        if (targs.name.equals("STARTMACRO") && !filename.contains("/mcr/")) {
          if (M.isAutoDetectingBehavior()) { // NXM-4097 Java vs Macro defaults
            M.setIfJavaBehavior(false);
            midas2.setIfJavaBehavior(false);
          }
          if (Midas.isJavaVsMidasDebug()) {
            M.info("#JvsM Macro.loadText STARTMACRO setting Macro/Midas usage to true filename:"+filename);
          }
        }
        int linenum = file.getReadProperCount(); // "estimate" line number (since this will be different when files are
                                                 // included)
        targs.setLineNumber(linenum);

        if (targs.support == 'I') {
          targs.cmd = Shell.loadCommand(M, targs);
        }

        if (i == 1) { // handle /ID switch for startmacro line
          if (!targs.name.equals("STARTMACRO")) M.error("Macro must begin with STARTMACRO command");
          if (targs.cmd.id.length() <= 0) targs.cmd.id = "MAIN";
          M.registry.put(targs.cmd.id, this);
        }

        // handle control flow statements
        if (targs.name.equals("IF")) {
          if (noTestCase(targs)) M.error("No test condition specified for IF statement around line " + linenum);
          ifindex[++iflevel] = i;
        }
        else
          if (targs.name.equals("ELSEIF")) {
            if (noTestCase(targs)) M.error("No test condition specified for ELSEIF statement around line " + linenum);
            checkElseifStruct(targs); // added in 3.1.1 for Bug 2540
            if (iflevel < 1) M.error("Unexpected ELSEIF around line " + linenum + " text: " + text);
            ((Intrinsic) lines[ifindex[iflevel]].cmd).nextIndex = i;
            ifindex[iflevel] = i;
          }
          else
            if (targs.name.equals("ELSE")) {
              if (iflevel < 1) M.error("Unexpected ELSE around line " + linenum);
              ((Intrinsic) lines[ifindex[iflevel]].cmd).nextIndex = i;
              ifindex[iflevel] = i;
            }
            else
              if (targs.name.equals("ENDIF")) {
                if (iflevel < 1) M.error("Unexpected ENDIF around line " + linenum);
                ((Intrinsic) lines[ifindex[iflevel]].cmd).nextIndex = i;
                iflevel--;
              }
              else
                if (targs.name.equals("DO")) {
                  doindex[++dolevel] = i;
                }
                else
                  if (targs.name.equals("ENDDO")) {
                    if (dolevel < 1) M.error("Unexpected ENDDO around line " + linenum);
                    ((Intrinsic) lines[doindex[dolevel]].cmd).nextIndex = i;
                    ((Intrinsic) targs.cmd).nextIndex = doindex[dolevel];
                    dolevel--;
                  }
                  else
                    if (targs.name.equals("LOOP")) {
                      loopindex[++looplevel] = i;
                    }
                    else
                      if (targs.name.equals("ENDLOOP")) {
                        if (looplevel < 1) M.error("Unexpected ENDLOOP around line " + linenum);
                        ((Intrinsic) lines[loopindex[looplevel]].cmd).nextIndex = i;
                        ((Intrinsic) targs.cmd).nextIndex = loopindex[looplevel];
                        looplevel--;
                      }
                      else
                        if (targs.name.equals("FOREACH")) {
                          forindex[++forlevel] = i;
                        }
                        else
                          if (targs.name.equals("ENDFOR")) {
                            if (forlevel < 1) M.error("Unexpected ENDFOR around line " + linenum);
                            ((Intrinsic) lines[forindex[forlevel]].cmd).nextIndex = i;
                            ((Intrinsic) targs.cmd).nextIndex = forindex[forlevel];
                            forlevel--;
                          }
                          else
                            if (targs.name.equals("WHILE")) {
                              if (noTestCase(targs))
                                M.error("No test condition specified for WHILE statement around line " + linenum);
                              whileindex[++whilelevel] = i;
                            }
                            else
                              if (targs.name.equals("ENDWHILE")) {
                                if (whilelevel < 1) M.error("Unexpected ENDWHILE around line " + linenum);
                                ((Intrinsic) lines[whileindex[whilelevel]].cmd).nextIndex = i;
                                ((Intrinsic) targs.cmd).nextIndex = whileindex[whilelevel];
                                whilelevel--;
                              }
                              else
                                if (targs.name.equals("LABEL")) {
                                  jumps.put(targs.getU(1), Integer.valueOf(i));
                                }
                                else
                                  if (targs.name.equals("PROCEDURE")) {
                                    if (macrolevel != 0)
                                      M.error("PROCEDURE definition found inside STARTMACRO/ENDMACRO around line "
                                          + linenum + " text: " + text);
                                    jumps.put(targs.getU(1), Integer.valueOf(i));
                                  }
                                  else
                                    if (targs.name.equals("SUBROUTINE")) {
                                      if (macrolevel != 0)
                                        M.error("SUBROUTINE definition found inside STARTMACRO/ENDMACRO around line "
                                            + linenum + " text: " + text);
                                      jumps.put(targs.getU(1), Integer.valueOf(i));
                                    }
                                    else
                                      if (targs.name.equals("STARTMACRO")) {
                                        if (macrolevel != 0)
                                          M.error("Duplicate STARTMACRO found before ENDMACRO around line " + linenum
                                              + " text: " + text);
                                        macrolevel++;
                                      }
                                      else
                                        if (targs.name.equals("ENDMACRO")) {
                                          macrolevel--;
                                        }

        // handle special .TXT macro constructs
        if (txt) {
          if (targs.name.equals("XCONTROL") && largs.name.equals("STARTCONTROLS")) {
            autoInitialEntry = true;
            jumps.put("INITIALENTRY", Integer.valueOf(i));
          }
          if (autoInitialEntry && !targs.name.equals("XCONTROL")) {
            autoInitialEntry = false;
            Args dargs = Shell.parseCommand(M, "RETURN");
            dargs.cmd = Shell.loadCommand(M, dargs);
            lines[i++] = dargs;
          }
        }

        if (i == maxIndex) { // do we need a bigger array ?
          maxIndex *= 2;
          Args[] tmp = lines;
          lines = new Args[maxIndex];
          for (int ic = 0; ic < tmp.length; ic++)
            lines[ic] = tmp[ic];
        }
        lines[i++] = targs;
        largs = targs;
      } // end while loop
    } finally {
      file.close();
    }
    // file.close(); // moved to finally block for Bug 2405
    lastIndex = i;
    if (iflevel != 0) M.error("Unclosed IF/ELSEIF/ELSE/ENDIF block");
    if (dolevel != 0) M.error("Unclosed DO/ENDDO block");
    if (looplevel != 0) M.error("Unclosed LOOP/ENDLOOP block");
    if (forlevel != 0) M.error("Unclosed FOREACH/ENDFOR block");
    if (whilelevel != 0) M.error("Unclosed WHILE/ENDWHILE block");
    if (macrolevel != 0) M.error("Unbalanced STARTMACRO/ENDMACRO block");

    if ((M.debug & Debug.MACRO) != 0) jumps.dump(System.out);

    return i;
  }

  /** Checks to see that an IF or ELSEIF is missing a test case. */
  private boolean noTestCase (Args targs) {
    String arg = targs.getU(1);
    if (arg.equals("THEN")) return true; // No test between IF and THEN
    if (targs.getNArgs() > 1) return false; // Found a test case
    return arg.equals(""); // If this is not blank, then allow it for implicit isTrue test
  }

  /**
   * Matches a text label to an entry in {@link #jumps}.
   * @param label The text label to match.
   * @return The index associated with the label or <code>-1</code> if the label does not match any entries.
   */
  public int lookupIndex (String label) {
    Object entry = jumps.get(label);
    if (entry instanceof Integer) return ((Integer) entry).intValue();
    return -1;
  }

  /**
   * Generate the path to a macro text file. If the macro's name, <code>mname</code> has a prefix of <code>'%'</code>
   * then the user's <code>"HOMEPATH</code> will be searched rather then the <code>"MCR"</code> directory in the
   * specified option tree.
   * @param M The Midas context to use.
   * @param mname The macro's name.
   * @param option The option tree to search in.
   * @return The file name.
   */
  public static String getFileName (Midas M, String mname, String option) {
    String fname;
    if (mname == null || mname.length() == 0)
      fname = null;
    else
      if (mname.charAt(0) == '%')
        fname = M.io.name("HOMEPATH", mname.substring(1), "MM", "AUX");
      else
        fname = M.io.name("MCR", mname, "MM", option);
    return fname;
  }

  /**
   * Parse arguments into results parameters for macros, subroutines and procedures.
   * @param M The Midas context to place the arguments into (as results parameters).
   * @param ai The arguments set to get the values from.
   * @param ao The arguments containing commands in the Macro
   * @param offset into arguments containing commands in the Macro
   */
  public static void argsToResults (Midas M, Args ai, Args ao, int offset) {
    Object value;
    String key, resname, defValue;
    int ic, ib;
    String[] resnames  = new String[ao.nargs];
    String[] defValues = new String[ao.nargs];
    String[] macroRes  = new String[ao.nargs];
    int[]    iColon    = new int[ao.nargs];
    boolean  isCall    = offset!=0; // is this a procedure call?

    // preprocess args from macro, so know all ao macro result names
    int idx = 0;
    for (int i = offset + 1; i <= ao.nargs; i++,idx++) {
      resname = ao.getU(i);
      ib = resname.indexOf('[');

      if (ib > 0) { // parse off user macro defaults
        defValues[idx] = resname.substring(ib + 1, resname.length() - 1);
        resname = resname.substring(0, ib);
      }
      else {
        defValues[idx] = "";
      }
      resnames[idx] = resname;
      ic = resname.indexOf(':'); // for parsing off argument type
      iColon[idx] = ic;
      macroRes[idx] = (ic >= 0) ? resname.substring(ic+1) : resname;
    }
    List<String> macroResList = (isCall) ? null : Arrays.asList(macroRes);

    // Now get actual values, but can check if input arg key matches later macro result name
    idx = 0;
    for (int i = offset + 1; i <= ao.nargs; i++,idx++) {
      resname      = resnames[idx];
      defValue     = defValues[idx];
      String aoRes = macroRes[idx];
      ic           = iColon[idx];

      value = defValue;

      // Check if we have a value for the input key at this index
      // If not, key = UNDEFINED
      key = (isCall || ai.get(aoRes) == null) ? null : aoRes;
      if ((key==null) && (i <= ai.nargs)) {
        String iKey = ai.getKey(i);
        // grab by position if procedure call or input arg key does not match ao macro result name
        if (isCall || !macroResList.contains(iKey)) {
          key = iKey;
        }
      }
      if (key==null) key = "UNDEFINED";

      if (ic <= 0) {
        value = ai.getU(key, defValue);
      }
      else
        if (ic == 1) {
          switch (resname.charAt(0)) {
            case DataTypes.STRING:
              value = ai.getS(key, defValue);
              break;
            case 'U':
              value = ai.getU(key, defValue);
              break;
            // case 'T': value = ai.getTable(key); break; // 2.7.0 NTN: TODO: revert back to this when we support Java's
            // null in results
            case DataTypes.TABLE:
              value = ai.getTable(key, null, Args.NO_ERROR);
              break; // 2.7.1 NTN: need this otherwise macros ERROR when Java's null is converted to "NULL" string
                     // (which is an invalid Table)
            case DataTypes.DOUBLE:
              value = new Data(ai.getD(key, Convert.s2d(defValue)));
              break;
            case DataTypes.FLOAT:
              value = new Data(ai.getF(key, (float) Convert.s2d(defValue)));
              break;
            case DataTypes.LONG:
              value = new Data(ai.getL(key, (int) Convert.s2d(defValue)));
              break;
            case DataTypes.INT:
              value = new Data(ai.getI(key, (short) Convert.s2d(defValue)));
              break;
            case DataTypes.BYTE:
              value = new Data(ai.getB(key, (byte) Convert.s2d(defValue)));
              break;
            case DataTypes.NIBBLE:
              value = new Data(ai.getD(key, Convert.s2d(defValue)));
              break;
            case DataTypes.BOOL:
              value = Boolean.valueOf(ai.getZ(key, Convert.o2z(defValue)));
              break;
            case DataTypes.OBJECT:
              value = ai.getO(key);
              break;
            case 'Q':
              if (ao.nargs == 1)
                value = ai.getRawArgs();
              else
                value = ai.getQ(key, defValue);
              break;
            case DataTypes.ASCII:
              value = ai.getS(key, defValue);
              if (M.macro != null && M.macro.isTxt())
                ;
              else
                M.warning("Replacing A: with S: in macro/procedure command line " + "arg = " + resname);
              break;
            default:
              M.error("Unsupported argument type in " + resname);
          }
        }
        else
          if (resname.startsWith("CS:")) {
            value = ai.getCS(key, defValue);
          }
          else
            if (resname.startsWith("FN:")) {
              value = ai.getFileName(key, defValue);
            }
            else {
              M.error("Unsupported argument type in " + resname);
            }

      M.results.put(aoRes, value);
    }
  }

  private String readFiltered (TextFile file) {
    int i;
    String op, text = textNext;
    textNext = null;
    if (text == null) text = file.readProper();
    if (text == null) return text;
    // check for in-line command separator character
    i = text.indexOf(AsciiMap.LF);
    if (i > 0) {
      textNext = text.substring(i + 1);
      return text.substring(0, i);
    }
    // check for one line IF statements and unfold
    if ((text.length() >= 3) &&
        (text.charAt(0) == 'i' || text.charAt(0) == 'I') &&
        (text.charAt(1) == 'f' || text.charAt(1) == 'F') &&
        (text.charAt(2) == ' ' || text.charAt(2) == ',')) {
      Parser p = new Parser(text);
      p.clean();
      for (i = 3;;) {
        op = p.get(i);
        if (op.length() == 0) break;
        if (op.charAt(0) == 'N') op = op.substring(1);
        if (Args.isUnaryTest(op))
          i += 1;
        else
          if (Args.isCompoundTest(op) || op.equals("THEN"))
            ;
          else
            i += 2;
        op = p.get(i);
        if (op.length() == 0) break;
        if (Args.isCompoundTest(op))
          i += 2;
        else
          break;
      }
      if (op.equals("THEN")) op = p.get(++i);
      if (op.length() > 0) {
        i = p.curoffset;
        textNext = new String(p.buffer, i, p.length - i);
        if (!textNext.endsWith("\nENDIF")) textNext += "\nENDIF";
        text = new String(p.buffer, 0, i - 1);
      }
    }

    return text;
  }

  /**
   * Check to see if user has incorrectly made ELSEIF statement. This method simply looks for the THEN keyword and
   * issues a warning that any text beyond the THEN will be disregarded.
   * @param targs Line from macro (starts with ELSEIF)
   * @since NeXtMidas 3.1.1
   */
  private void checkElseifStruct (Args targs) {
    int i;
    String tmpstr;
    for (i = 2;; i++) {
      tmpstr = targs.getU(i);
      if (tmpstr.length() == 0) break; // end of text
      if (tmpstr.toUpperCase().equals("THEN")) {
        tmpstr = targs.getU(++i);
        if (tmpstr.length() == 0)
          break;
        else { // there is text after the THEN
          StringBuilder sb = new StringBuilder();
          for (;; i++) {
            if (targs.getU(i).length() == 0)
              break; // end of text
            else
              sb.append(targs.getU(i)).append(" "); // rebuild String of ignored text
          }
          sb.deleteCharAt(sb.lastIndexOf(" ")); // remove trailing space
          M.warning("The following text beyond 'THEN' in ELSEIF statement will be ignored: [" + sb + "]");
        }
      }
    }
  }

  /**
   * Set the number of active timers. Any old timers will be discarded.
   * @param nTimers The number of active timers.
   */
  public void setTimers (int nTimers) {
    this.nTimers = nTimers;
    timer = new double[nTimers];
    timerinit = new double[nTimers];
  }

  /**
   * Get a timer's value by index.
   * @param i The index of the timer to get the value of.
   * @return The value for the timer at index <code><i>i</i></code>.
   * @see #setTimer(int,double)
   */
  public double getTimer (int i) {
    if (i < 0 || i >= nTimers) throw new MidasException("Invalid timer index = " + i);
    return timer[i];
  }

  /**
   * Set a timer's value by index.
   * @param i The index of the timer to set.
   * @param value The new value for the timer at index <code><i>i</i></code>.
   * @see #getTimer(int)
   */
  public void setTimer (int i, double value) {
    if (i < 0 || i >= nTimers) throw new MidasException("Invalid timer index = " + i + " to set value = " + value);
    timer[i] = value;
    timerinit[i] = Time.current();
  }

  /** Sets the table of controls
      @param controls table of controls
   */
  public void setControls (Table controls) {
    this.controls = controls;
  }

  /** Gets the table of controls
      @return table of controls
   */
  public Table getControls () {
    return controls;
  }

  /**
   * Create a clone of this Macro ready for subroutine use.
   * @return A clone of this Macro ready for subroutine use.
   */
  public Macro cloneOf () {
    try {
      Macro macro = (Macro) this.clone();
      macro.returns = new int[maxLevel];
      macro.results = new Results[maxLevel];
      macro.level = 0;
      macro.returns[0] = -1;
      macro.results[0] = midas2.results;
      macro.nextIndex = -1;
      return macro;
    } catch (CloneNotSupportedException e) {
      throw new MidasException("Clone of Macro not supported", e);
    }
  }

  /**
   * Get the lines of this macro as {@link Args}
   * @return Get the lines of this macro as Args
   */
  public Args[] getLines () {
    return lines;
  }

  /**
   * Get Control flow next command index
   * @return Control flow next command index
   */
  public int getNextIndex () {
    return nextIndex;
  }

  /**
   * Gets the parent context.
   * @return the parent context
   */
  public Midas getParentContext () {
    return midas1;
  }

  /**
   * Get this macro's registry table.
   * @return this macro's registry table
   */
  public Table getRegistry () {
    return midas2.registry.table;
  }

  /**
   * Get this macro's results table.
   * @return this macro's results table
   */
  public Results getResults () {
    return midas2.results;
  }

  /**
   * Get this macro's piped Commands.
   * @return this macro's piped Commands.
   */
  public KeyVector getPrimitives () {
    return primitives;
  }

  /**
   * Get the full macro file name. This corresponds to the /MFN= switch.
   * @return full file name
   * @since NeXtMidas 2.7.1
   */
  public String getFileName () {
    return filename;
  }

  /**
   * Is specified option shared between parent and child macros. Set via the /ShareMask switch. Only used by the PIPE
   * command to check value of switch.
   * @param mask option
   * @return true if shared
   * @since NeXtMidas 3.1.2.
   */
  public boolean isShared (int mask) {
    return (shareMask & mask) != 0;
  }

  /**
   * Gets the registry name (i.e. ID) of the message handler being used for this macro. This will return
   * "PARENT.&lt;msgid&gt;" where &lt;msgid&gt; is value set via /MSGID= unless "GLOBAL.&lt;msgid&gt;" is specified.
   * @since NeXtMidas 3.3.1
   */
  @Override
  public synchronized String getMsgID () {
    String retMsgID = super.getMsgID();
    if (!retMsgID.isEmpty() && !retMsgID.startsWith("GLOBAL.") && !retMsgID.equals("<unknown>")) {
      retMsgID = "PARENT." + retMsgID; // the specified /MSGID= is in the parent's registry for a macro
    }
    return retMsgID;
  }

  /** Special code for .TXT macros */
  KeyVector cwidgets;

  /** Returns whether or not a text file is being read from
      @return using text file
   */
  public boolean isTxt () {
    return txt;
  }

  /** prep text command
   *  @param cmd Command
   */
  public void prepTxtCommand (Command cmd) {
    // Java SYS primitive acting as Host - pick up CTAGs
    if (cmd.support == 'P') {
      String ctags = sysctags.getS(cmd.name);
      if (ctags != null) {
        Parser p = new Parser(ctags);
        parseControlTagSwitch(cmd, p);
        for (int i = 1; i <= p.elements(); i++) {
          cwidgets.add(cmd.getID() + "-" + i, p.get(i));
          int wbid = cmd.args.getL("/WB") * 1000 + i;
          if (wbid > 0) MR.addFunction("W_" + wbid, new ControlTag("REG." + cmd.getID() + "." + p.get(i)));
        }
      }
    }
  }

  /** adjust static count
   *  @param inc increment
   */
  public synchronized void adjustStaticCount (int inc) {
    staticCount += inc;
  }

  /** add control widget
   *
   *  @param cmd  Command
   *  @param cid  Command ID
   *  @param name name
   */
  public void addControlWidget (Command cmd, int cid, String name) {
    if (!txt) return;
    // parse Control Tag switch on 1st widget only
    if (cid == 1) parseControlTagSwitch(cmd, null);
    cwidgets.add(cmd.getID() + "-" + cid, name);
    int wbid = cmd.args.getL("/WB") * 1000 + cid;
    if (wbid > 0) MR.addFunction("W_" + wbid, new ControlTag("REG." + cmd.getID() + ".W_" + cid));
  }

  /** parse control tag switch
   *
   *  @param cmd Command
   *  @param cp  Parser
   */
  private void parseControlTagSwitch (Command cmd, Parser cp) {
    String ctag = cmd.args.getS("/CTAG");
    if (ctag == null || ctag.length() == 0) return;
    if (ctag.charAt(0) == '(') ctag = ctag.substring(1, ctag.length() - 1);
    Parser p = new Parser(ctag);
    for (int i = 1; i <= p.elements(); i++) {
      String tag = p.get(i);
      int lc = tag.indexOf(':');
      int tid = Convert.s2l(tag.substring(0, lc));
      tag = tag.substring(lc + 1);
      String func = (cp != null) ? cp.get(tid) : "W_" + tid;
      MR.addFunction(tag, new ControlTag("REG." + cmd.getID() + "." + func));
    }
  }

  /** get control widgets
   *  @return map containing control widgets
   */
  public KeyVector getControlWidgets () {
    return cwidgets;
  }

  /** get control tags
   *  @return Map containing control tags
   */
  public Map<String, Function> getControlTags () {
    return MR.getFunctions();
  }

  /** Control Tag Class */
  public class ControlTag implements Results.Function {
    private String expression;

    /** ControlTag Constructor
        @param expr macro expression
     */
    public ControlTag (String expr) {
      expression = expr;
    }

    public Object exec (Object args) {
      return null;
    }

    public void set (Object value) {
      if (MR != null) MR.put(expression, value);
    }

    public Object get () {
      return (MR != null) ? MR.get(expression) : null;
    }

    public String toString () {
      return expression;
    }
  }

  public String getStatus () {
    Object s = results[0].get("STATUS");
    return (s == null) ? getState() : s.toString();
  }

}
