/*
 * WepsDBFile.java
 *
 * This class holds basic information about a weps crop or operation xml file. Each xml data file
 * loaded is bound to a new instance of this class.
 *
 *
 * Jim Frankenberger
 * USDA-ARS, West Lafayette IN
 * jrf@purdue.edu
 *
 * Created on August 27, 2004, 2:28 PM
 */
package ex1;

import de.schlichtherle.truezip.file.TFile;
import java.io.IOException;
import java.util.*;

import javax.swing.*;
import org.openide.util.Exceptions;

/**
 * This class holds basic information about a weps crop or operation xml file.
 * Each xml data file loaded is bound to a new instance of this class.
 *
 * @author jrf
 */
public class WepsDBFile {

   private String file;        // name of the file
   private final ArrayList<ActionValue> actions;  // for operations, list of actions that make up the operation
   private final boolean isCropFile;  // true if a CROP file, false for an operation file
   private final DefnFileParser parms; // link to 
   private TFile theFile;       // Java File instance of the file to access properties
   private HashMap<ParamDef, ParameterVal> cropParms;
   private boolean modified;
   private ActionValue currentActionNode;  // when building an operation from a file this is the current action/process that
   // data is being added to.
   private boolean actionSequenceChanged;  // true if an operations process order is changed in any way
   private boolean outOfOrder;   // true if this operation has a process order that does not look good
   private boolean nameMisMatched;//True if this operation's name does not match the file name.
   private ArrayList<String> outHard; //Stores the names of all actions outside the limits.
   private InputLimits.TableStatus status; //Stores the TableStatus for crops
   private boolean decomp;
   private String dbDir;

   private final String cropDevNotes = "crop_devnotes";
   private final HashMap<OpAction, String> opDevNotes;

   /**
    * Sets the decomposition flag, which determines whether this parameter is in
    * the crop subset of decomposition.
    * @param input flag
    */
   public void setDecomp(boolean input) {
      decomp = input;
   }

   /**
    * Retrieves the decomposition flag, which determines whether this parameter
    * is in the crop subset of decomposition.
    * @return decomp flag
    */
   public boolean getDecomp() {
      return decomp;
   }

   /**
    * Creates a new instance of WepsDBFile
    *
    * @param f name of file to load
    * @param isCrop true if crop file
    * @param p structure holding all parameter info
    * @param dbBase Database base-directory 
    */
   public WepsDBFile(String f, boolean isCrop, DefnFileParser p, String dbBase) {
      file = (f);
      theFile = new TFile(f);
      actions = new ArrayList<>();
      isCropFile = isCrop;
      parms = p;
      if (isCrop) {
         cropParms = new HashMap<>();
      }
      modified = false;
      actionSequenceChanged = false;
      outOfOrder = false;
      outHard = new ArrayList<>();
      status = InputLimits.TableStatus.OKAY;
      opDevNotes = initDevNotes();
      dbDir = dbBase;
   }

   private HashMap<OpAction, String> initDevNotes() {
      HashMap<OpAction, String> returnCase = new HashMap<>();
      returnCase.put(new OpAction("Initialization", 'O', 0), "op_devnotes000");
      returnCase.put(new OpAction("Direction and Speed", 'O', 1), "op_devnotes101");
      returnCase.put(new OpAction("Others", 'O', 2), "op_devnotes202");
      returnCase.put(new OpAction("Energy STIR Direction Speed", 'O', 3), "op_devnotes303");
      returnCase.put(new OpAction("Energy STIR Others", 'O', 4), "op_devnotes404");
      return returnCase;
   }

   /**
    *
    * Basic test to see if the file can be written. Read-only files will be
    * displayed in gray in the user interface.
    *
    * @return true if read-only file, false if file writeable
    */
   public boolean isReadOnly() {
      return !theFile.canWrite();
   }

   /**
    * This starts off the first action node of an operation.
    *
    * @return a new ActionValue node
    */
   public ActionValue startActionNode() {
      currentActionNode = new ActionValue();

      return currentActionNode;
   }

   /**
    * This returns the subIndex of this action within the operation. This occurs
    * if an operation contains the same process/action more than once.
    *
    * @param a the structure to search for
    * @return the occurrence of this specific type of process in the operation (0
    * is first)
    */
   public int getSub(ActionValue a) {
      OpAction opa = a.getAction();
      int sub = 0;
      for (ActionValue av : actions) {
         if (av != null) {
            if (av == a) {
               return sub;
            }
            if (av.getAction() == opa) {
               sub++;
            }
         }
      }
      return 0;
   }

   /**
    * This closes building an action node by setting its type and then adding it
    * to the list.
    *
    * @param act type of process this node is to represent
    * @return
    */
   public ActionValue endActionNode(OpAction act) {

      currentActionNode.setAction(act);

      actions.add(currentActionNode);

      evaluate();

      return currentActionNode;
   }

   /**
    * Finds the first action of a specific type in the operation. If there is no
    * action of the requested type than null is returned.
    *
    * @param a type of process to search for
    * @return an instance of this process type if found
    */
   public ActionValue find(OpAction a) {

      for (ActionValue av : actions) {
         if (av != null) {
            if (av.getAction() == a) {
               return av;
            }
         }
      }
      return null;
   }

   /**
    * Adds a parameter and its value to the list for crops or if this is an
    * operation to the currrent action node being built.
    *
    * @param name parameter name
    * @param val value of the parameter
    * @return
    */
   public boolean addParm(String name, String val) {
      ParamDef p = parms.getParamDef(name);
      if (p != null) {
         if (isCropFile) {
            ParameterVal pv = new ParameterVal(p, val);
            cropParms.put(p, pv);
            if (name.equals("idc")) {
               decomp = val.equals("0") || val.equals("-1");
            }
            for (ParamDef key : cropParms.keySet()) {
               InputLimits il = key.il;
               ParameterVal curVal = cropParms.get(key);
               if (curVal == null) {
                  continue;
               }
               try {
                  double hardVal = Double.parseDouble(curVal.getVal(false));
                  InputLimits.TableStatus attempt;
                  if (decomp && !key.getDecomp()) {
                     attempt = InputLimits.TableStatus.OKAY;
                  }
                  else {
                     attempt = il.evaluateInput(hardVal);
                  }
                  if (status.lessThan(attempt)) {
                     status = attempt;
                  }
               }
               catch (NumberFormatException notDouble) {
               }
            }
         }
         else {
            currentActionNode.addParm(p, val);
            if (currentActionNode.operationOverwrite) {
               ErrorSink.add(file + "\nParameter overwrite:  " + p.name);
            }
         }
         return true;
      }
      return false;
   }

   /**
    * This method is needed to update a crop file so it now has a developer
    * notes category.
    */
   public void updateCropNotes() {
      String name = cropDevNotes;
      ParamDef p = parms.getParamDef(name);
      if (getValue(p, false).equals("???")) {
         ErrorSink.outDate("Outdated File:  " + getFileName());
         String val = "Initializing to this, hopefully this will never display";
         ParameterVal pv = new ParameterVal(p, val);
         cropParms.put(p, pv);
         val = "Enter dev notes here.";
         setValue(p, val, false);
      }
   }

   /**
    * This method is designed to check if the operation has developer notes for
    * each operation level parameter it has. If it doesn't, this method should
    * add them.
    */
   public void updateOpNotes() {
      ActionValue act = getAction(0);
      String name = opDevNotes.get(act.getAction());
      ParamDef p = parms.getParamDef(name);
      if (act.getVal(p) == null) {
         ErrorSink.outDate("Outdated File:  " + getFileName());
         String val = "Initializing to this, hopefully this will never display";
         act.addParm(p, val);
         val = "Enter dev notes here.";
         ParameterVal pVal = act.getVal(p);
         pVal.setVal(val, false);
         modified = pVal.isModified();
      }
   }

   /**
    * This determines how many rows in a detail operations table this file
    * occupies for the indicated code and type, like P24, P32, etc. If an
    * operation has 3 P24 processes and we are checking for 24 and 'P' this will
    * return 3. If the operation has 0 or 1 of the actions then one is returned
    * because we at least display the file name.
    *
    * @param code numeric part of process to search for
    * @param ty character (P,G,O) type of process to search for
    * @return number of rows this process will occupy
    */
   public int getRowsSpanned(int code, char ty) {
      int count = 0;
      for (ActionValue a : actions) {
         count += a.actionCounts(code, ty);
      }
      if (count == 0) {
         count = 1;
      }
      return count;
   }

   /**
    * This reads a particular value from an operations record.
    *
    * @param tab what process type, corresponds to table index of detail tables
    * @param col what column(parameter) to get
    * @param sub if there are more than 1 process of this type selects which on
    * @param altUnits true if the value should be returned in the alternate
    * units
    * @return
    */
   public String getValueAt(int tab, int col, int sub, boolean altUnits) {
      OpAction a = parms.getAction(tab);
      int numFound = 0;

      for (ActionValue aa : actions) {
         if (aa.sameOpAction(a)) {
            if (numFound == sub) {
               // this is the one
               ParameterVal pv = aa.getVal(col - 3);
               if (pv != null) {
                  return pv.getVal(altUnits);
               }
               else {
                  return "???";
               }
            }
            else {
               numFound++;
            }
         }
      }
      return "--";
   }

   /**
    * The matching function to setValueAt() for operations data.
    *
    * @param tab what process type, corresponds to table index of detail tables
    * @param col what column(parameter) to get
    * @param sub if there are more than 1 process of this type selects which on
    * @param val string to save
    * @param altUnits true if the value is in alternate units and needs to be
    * converted before storing
    * @return true if value set okay
    */
   public boolean setValueAt(int tab, int col, int sub, String val, boolean altUnits) {
      OpAction a = parms.getAction(tab);
      int numFound = 0;

      for (ActionValue aa : actions) {
         if (aa.sameOpAction(a)) {
            if (numFound == sub) {
               // this is the one
               ParameterVal pv = aa.getVal(col - 3);
               if (pv != null) {
                  pv.setVal(val, altUnits);
                  if (pv.isModified()) {
                     modified = true;
                     updateStatus();
                     outHard = null;
                     return true;
                  }
               }
               else {
                  return false;
               }
            }
            else {
               numFound++;
            }
         }
      }
      return false;
   }

   /**
    * Returns a list of all the parameters. Only valid for a crop file because
    * the operations data has a hierarchy.
    *
    * @return a Collection (ParameterVal) of all parms
    */
   public Collection<ParameterVal> getAllParms() {
      if (isCropFile) {
         return cropParms.values();
      }
      else {
         return null;
      }
   }

   /**
    * Get the value of a specific crop parameter. The definition passed in
    * indicates what to search for If altUnits is true then the value is
    * converted before it is returned.
    *
    * @param p Info about parameter
    * @param altUnits true if alternate(english) conversion should be done
    * @return
    */
   public String getValue(ParamDef p, boolean altUnits) {
      if (isCropFile == true) {
         ParameterVal pv = cropParms.get(p);
         if (pv != null) {
            return pv.getVal(altUnits);
         }
      }

      return "???";
   }

   /**
    * Sets the value of a specific crop parameter.
    *
    * @param p the parameter info about what to set
    * @param val the value that is to be saved
    * @param altUnits true if the value is in English units
    * @return
    */
   public boolean setValue(ParamDef p, String val, boolean altUnits) {
      if (isCropFile == true) {
         ParameterVal pv = cropParms.get(p);
         if (pv != null) {
            pv.setVal(val, altUnits);
            if (pv.isModified()) {
               modified = true;
               updateStatus();
            }
         }
      }
      return true;
   }

   /**
    * For operations return the number of processes/actions that make up this
    * operation
    *
    * @return number of processes in this operation file.
    */
   public int getActionCount() {
      return actions.size();
   }

   /**
    * Return the ith action in the list or null if we don't have it.
    *
    * @param inx the index of the process to get
    * @return the ActionValue structure of the process or null if the operation
    * does not have process at the requested index
    */
   public ActionValue getAction(int inx) {
      if (inx < actions.size()) {
         return actions.get(inx);
      }
      else {
         return null;
      }
   }

   /**
    * For an operation file return the name of the ith action.
    *
    * @param index what process to return name for
    * @return name of the process at the index or "--" if nothing there
    */
   public String getActionName(int index) {
      if (isCropFile) {
         return "Crop";
      }
      else {
         if (index < actions.size()) {
            ActionValue a = actions.get(index);
            if (a != null) {
               return a.getName();
            }
            else {
               return "--";
            }
         }
         else {
            return "--";
         }
      }
   }

   /**
    *
    * @param index
    * @return
    */
   public String getShortActionName(int index) {
      if (isCropFile) {
         return "Crop";
      }
      else {
         if (index < actions.size()) {
            ActionValue a = actions.get(index);
            if (a != null) {
               return a.getCode() + a.getID();
            }
            else {
               return "--";
            }
         }
         else {
            return "--";
         }
      }
   }

   /**
    * Return the base name and extension of this file.
    *
    * @return the base and extension of the file
    */
   public String getFileName() {
      return theFile.getName();
   }

   /**
    * Return the full pathname for this file.
    *
    * @return full filename of this file.
    */
   public String getPathName() {
      return file;
   }

   /**
    * Get the path part of this file rooted at the main weps database directory.
    *
    * @return path part of name relative to dbdir
    */
   public String getPathOnly() {
      if (file.startsWith(dbDir)) {
         String path = theFile.getParent();
         return path.substring(dbDir.length());
      }
      else {
         return theFile.getParent();
      }
   }

   /**
    * The basename of the file without the .CROP, .UPGM or .OPRN extension
    *
    * @return the basename without the .CROP, .UPGM or .OPRN extension
    */
   public String getBaseName() {
      String str = (theFile.getName());
      String baseName = null;
      String[] legalExtensions = {".CROP", ".UPGM", ".OPRN"};
      int len = str.length();

      if (len > 5) {
         String extension = str.substring(len - 5, len);
         for (String legalExtension : legalExtensions) {
            if (extension.equalsIgnoreCase(legalExtension)) {
               baseName = str.substring(0, len - 5);
            }
         }
      }
      return baseName;
   }

   /**
    * Return true if any parameter has changed.
    *
    * @return true if any part of this file has changed.
    */
   public boolean isModified() {
      return modified;
   }

   /**
    * Save the database file as an XML file specified by the DTD's in
    * mcrewCfgDir
    *
    * @param mcrewCfgDir directory of mcrew config files
    * @return true if file saved ok
    */
   public boolean saveFile(SavePanelSingle parent, String mcrewCfgDir) {
      boolean rc;
      SaveXML xs = new SaveXML(mcrewCfgDir, parms, this, isCropFile);

      if (!isCropFile) {
         if (checkProcesses(parent, true) == false) {
            String msg = getFileName() + " does not have a valid process order. Save file anyway?";
            if (JOptionPane.showConfirmDialog(parent, msg, "Invalid Process Order",
                    JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
               return false;
            }
         }
      }

      // Check if we can only save the parms we changed
      if (actionSequenceChanged == false) {
         rc = xs.saveFile(file);
      }
      else {
         rc = xs.createFile(file);
      }    // needs to be built from scratch

      if (rc) {
         modified = false;
         actionSequenceChanged = false;
      }

      return rc;
   }

   /**
    * Save the database file as an XML file specified by the DTD's in
    * mcrewCfgDir
    *
    * @param mcrewCfgDir directory of mcrew config files
    * @return true if file saved ok
    */
   public boolean saveFile(String mcrewCfgDir) {
      boolean rc = false;
      SaveXML xs = new SaveXML(mcrewCfgDir, parms, this, isCropFile);

      if (!isCropFile) {
         if (checkProcesses(true) == false) {
            String msg = getFileName() + " does not have a valid process order. Save file anyway?";
            if (JOptionPane.showConfirmDialog(null, msg, "Invalid Process Order",
                    JOptionPane.YES_NO_OPTION) == JOptionPane.NO_OPTION) {
               return false;
            }
         }
      }

      // Check if we can only save the parms we changed
      if (actionSequenceChanged == false) {
         rc = xs.saveFile(file);
      }
      else {
         rc = xs.createFile(file);
      }    // needs to be built from scratch

      if (rc) {
         modified = false;
         actionSequenceChanged = false;
      }

      return rc;
   }

   /**
    * Create a copy of this file.
    *
    * @param mcrewCfgDir directory where the mcrew config xml and dtd files are
    * @param wf base file structure where data comes from
    * @param stripActions true to remove all actions
    */
   public void createClone(String mcrewCfgDir, WepsDBFile wf, boolean stripActions) {

      SaveXML xs = new SaveXML(mcrewCfgDir, parms, this, isCropFile);

      xs.cloneFile(wf.getPathName(), file, stripActions);

   }

   /**
    * Change the name of an existing file.
    *
    * @param mcrewCfgDir directory where mcrew config files are located
    * @param newName new name for file
    */
   public void changeName(String mcrewCfgDir, String newName) {
      try {
         String f = getFileName();

         int len = f.length();
         int len2 = file.length();

         String dir = file.substring(0, len2 - len);
         dir = dir.concat(newName);

         file = (dir);
         TFile nfile = new TFile(file);
         theFile.mv(nfile);
         theFile = nfile;
         String temp = nfile.getName();
         temp = temp.substring(0, temp.lastIndexOf("."));
         objectName = temp;
         nameMisMatched = false;

         SaveXML xs = new SaveXML(mcrewCfgDir, parms, this, isCropFile);

         xs.cloneFile(file, dir, false);
      }
      catch (IOException ex) {
         Exceptions.printStackTrace(ex);
      }
   }

   /**
    * Get rid of this file
    *
    * @return true if the file was deleted ok
    */
   public boolean deleteFile() {
      try {
         theFile.rm();
         return true;
      }
      catch (IOException ex) {
         return false;
      }
   }

   /**
    * This function changes a process to a different type.
    *
    * @param procNum process index in operation to change
    * @param newProc index of new process
    * @param av template to use to fill in parm values or null
    * @return
    */
   public boolean changeProcess(int procNum, int newProc, ActionValue av) {
      boolean rc = true;
      ActionValue act;

      if (av == null) {
         OpAction a = parms.getAction(newProc);
         act = new ActionValue();
         act.setAction(a);
         act.fillWithDefaultParms();
      }
      else {
         act = new ActionValue(av);
      }
      actions.set(procNum, act);
      modified = true;
      actionSequenceChanged = true;

      checkProcesses(true);

      return rc;
   }

   /**
    * This function inserts a new process before (or after) a given process.
    *
    * @param procNum index of current process in this operation
    * @param newProc type of new operation to insert
    * @param before true if this should be inserted before, false for after
    * @param av set of parameters to use as template or null
    * @return
    */
   public boolean insertProcess(int procNum, int newProc, boolean before, ActionValue av) {
      boolean rc = true;
      ActionValue act;

      if (av == null) {
         OpAction a = parms.getAction(newProc);
         act = new ActionValue();
         act.setAction(a);
         act.fillWithDefaultParms();
      }
      else {
         act = new ActionValue(av);
      }

      if (before) {
         actions.add(procNum, act);
      }
      else {
         procNum = procNum + 1;
         if (procNum >= actions.size()) {
            actions.add(act);
         }
         else {
            actions.add(procNum, act);
         }
      }

      modified = true;
      actionSequenceChanged = true;

      checkProcesses(true);

      return rc;
   }

   /**
    * This function deletes a process from an operation.
    *
    * @param index index in list of processes that is to be deleted
    * @return true if process could be deleted, false otherwise
    */
   public boolean deleteProcess(int index) {
      boolean rc = false;

      if (actions.size() > index) {
         actions.remove(index);
         rc = true;
      }

      modified = true;
      actionSequenceChanged = true;

      checkProcesses(true);

      return rc;
   }

   /**
    * This function determines if the WEPS operations process and group ordering
    * is valid.
    *
    * @param displayMessage true if a dialog is to be displayed
    * @return true of process order is ok, false if things need fixing
    */
   public boolean checkProcesses(boolean displayMessage) {
      String msg = null;
      ActionValue a;
      // Rule 1: first process must define the type of operation
      if (actions.size() > 0) {
         a = actions.get(0);
         if ((a.isCode('O') == false) ^ ((a.isCode('P') == true) && (!"100".equals(a.getID())))) {
            msg = "First entry in process list must be O0, O1, O2, O3, O4, or UPGM P100";
         }
         else {
            // Rule 2: check that any groupId links are being followed
            int count = actions.size();
            for (int i = 0; i < count; i++) {
               a = actions.get(i);
               int groups = a.getGroups();
               //System.out.println("Number of preceding groups: " + groups);
               if (groups > 0) {
                  boolean predOk = false;
                  for (int j = 0; j < groups; j++) {
                     if (a.getGroupId(j) != -1) {
                        if (hasPredecessor(a, i) == true) {
                           predOk = true;
                           break;
                        }
                     }
                  }
                  if (!predOk) {
                     StringBuilder sb = new StringBuilder();
                     for (int j = 0; j < groups; j++) {
                        String reqName = parms.getFullActionName(a.getGroupCode(j), Integer.toString(a.getGroupId(j)));
                        System.out.println("required prereq: " + reqName);
                        sb.append(reqName);
                        if ((groups > 1) && (j != (groups - 1))) {
                           sb.append(" or \n    ");
                        }
                     }
                     if (msg != null) {
                        msg = msg + "\n" + a.getName() + " requires:\n    "
                                + sb.toString() + "\nto appear before it in the process list.";
                     }
                     else {
                        msg = a.getName() + " requires:\n    " + sb.toString()
                                + "\nto appear before it in the process list.";
                     }
                  }
               }
            }

         }
      }
      else {
         msg = "Requires at least an O0, O1, O2, O3, O4, or UPGM P100";
      }
      if (msg != null) {
         msg = msg + "\n\nFor this to be a valid WEPS operation, the process ordering must be corrected.";
         String title = "Operation: " + getFileName() + " does not have a valid process order";
         if (displayMessage) {
            JOptionPane.showMessageDialog(null, msg, title,
                    JOptionPane.INFORMATION_MESSAGE);
         }
         outOfOrder = true;
         return false;
      }
      else {
         outOfOrder = false;
         return true;
      }
   }

   /**
    * This function determines if the WEPS operations process and group ordering
    * is valid.
    *
    * @param displayMessage true if a dialog is to be displayed
    * @return true of process order is ok, false if things need fixing
    */
   public boolean checkProcesses(SavePanelSingle parent, boolean displayMessage) {
      String msg = null;
      ActionValue a;
      // Rule 1: first process must define the type of operation
      if (actions.size() > 0) {
         a = actions.get(0);
         if ((a.isCode('O') == false) ^ ((a.isCode('P') == true) && (!"100".equals(a.getID())))) {
            msg = "First entry in process list must be O0, O1, O2, O3, O4, or UPGM P100";
         }
         else {
            // Rule 2: check that any groupId links are being followed
            int count = actions.size();
            for (int i = 0; i < count; i++) {
               a = actions.get(i);
               int groups = a.getGroups();
               if (groups > 0) {
                  boolean predOk = false;
                  for (int j = 0; j < groups; j++) {
                     if (a.getGroupId(j) != -1) {
                        if (hasPredecessor(a, i) == true) {
                           predOk = true;
                           break;
                        }
                     }
                  }
                  if (!predOk) {
                     StringBuilder sb = new StringBuilder();
                     for (int j = 0; j < groups; j++) {
                        sb.append(parms.getFullActionName(a.getGroupCode(j), Integer.toString(a.getGroupId(j))));
                        if ((groups > 1) && (j != (groups - 1))) {
                           sb.append(" or     ");
                        }
                     }
                     if (msg != null) {
                        msg = msg + "\n" + a.getName() + " requires:\n    "
                                + sb.toString() + " to appear before it in the process list";
                     }
                     else {
                        msg = a.getName() + " requires:\n    " + sb.toString()
                                + " to appear before it in the process list";
                     }
                  }
               }
            }

         }
      }
      else {
         msg = "Requires at least an O0, O1, O2, O3, O4, or UPGM P100";
      }
      if (msg != null) {
         msg = msg + "\n\nFor this to be a valid WEPS operation the process ordering must be corrected.";
         String title = "Operation: " + getFileName() + " does not have a valid process order";
         if (displayMessage) {
            JOptionPane.showMessageDialog(parent, msg, title,
                    JOptionPane.INFORMATION_MESSAGE);
         }
         outOfOrder = true;
         return false;
      }
      else {
         outOfOrder = false;
         return true;
      }
   }

   /**
    * checks to see if the name of the file is the same as the name of the
    * object.
    *
    * @return
    */
   public boolean checkName() {
      nameMisMatched = !theFile.getName().startsWith(objectName + ".");
      return !nameMisMatched;
   }

   /**
    * Return true if the groupId link requirement is satisfied, otherwise false
    *
    * @param a ActionValue to check, this is an instance of a process and all
    * its parameters
    * @param limit - How many to check, basically only check from 0 upto this
    * process
    * @return true if has correct predecessor, false if missing
    */
   private boolean hasPredecessor(ActionValue a, int limit) {
      //int gid = a.getGroupId();
      //int gid2=gid;
      //String gcode = a.getGroupCode();

      // This is somewhat of a hack to allow any appearance of group 1 to fullfill the obligations
      // for a group 2.
      //if (gid == 2)
      //    gid2 = 1;
      for (int i = 0; i < limit; i++) {
         ActionValue av = actions.get(i);
         for (int j = 0; j < a.getGroups(); j++) {
            if (av.getIdInt() == a.getGroupId(j)) {
               String gcode = a.getGroupCode(j);

               if (gcode.equals(av.getCode())) {
                  return true;
               }
            }
         }
      }

      return false;
   }

   /**
    * Shifts the process one to the right in the process list.
    *
    * @param col column that is to be logically shifted right
    */
   public void moveRight(int col) {
      if (col >= (actions.size() - 1)) {
         return;
      }        // already at end, can't move

      ActionValue av = actions.get(col);
      ActionValue avr = actions.get(col + 1);

      actions.set(col + 1, av);
      actions.set(col, avr);

      modified = true;
      actionSequenceChanged = true;
   }

   /**
    * Shifts the process one to the left in the process list.
    *
    * @param col column that is to be shifted logically left
    */
   public void moveLeft(int col) {
      if (col <= 0) {
         return;
      }      // already at start, can't move

      ActionValue av = actions.get(col);
      ActionValue avr = actions.get(col - 1);

      actions.set(col - 1, av);
      actions.set(col, avr);

      modified = true;
      actionSequenceChanged = true;
   }

   /**
    * Return true if the process sequence is incorrect, set previously as a
    * result of checking processes.
    *
    * @return true if process order is not correct
    */
   public boolean isOutOfOrder() {
      return outOfOrder;
   }

   /**
    * @return
    */
   public boolean isNameMismatched() {
      return nameMisMatched;
   }

   /**
    *
    */
   public String objectName;

   /**
    *
    */
   public void fixupOperation() {
      //add the ofuel parameter if it doesn't exist.
      ActionValue action = getAction(0);

      if (action.isCode('O') && (action.getIdInt() == 3 || action.getIdInt() == 4)) {
         //kludge, the action only has single character id, hashmap has two
         ParamDef def = parms.getParamDef("ofuel0" + action.getID());
         ParameterVal value = action.getVal(def);
         if (value == null) {
            action.addParm(def, " ");
         }
      }

   }

   /**
    * This method evaluates the input limits for the object as each node ends.
    */
   private void evaluate() {
      //We will always be looking at the values in the most recent action
      OpAction opAct = actions.get(actions.size() - 1).getAction();
      //The maximimum number of parameters we will be dealing with
      int maxInd = opAct.getNumChildParms();
      //Accumulate holds the highest Table Status of the loop.
      InputLimits.TableStatus accumulate = InputLimits.TableStatus.OKAY;
      for (int index = 0; index < maxInd; index++) {
         ParamDef pDef = opAct.getColumn(index);
         InputLimits il = pDef.il;
         ParameterVal curVal = currentActionNode.getVal(pDef);
         if (curVal == null) {
            continue;
         }
         try {
            double hardVal = Double.parseDouble(curVal.getVal(false));
            InputLimits.TableStatus attempt;
            if (decomp && !pDef.getDecomp()) {
               attempt = InputLimits.TableStatus.OKAY;
            }
            else {
               attempt = il.evaluateInput(hardVal);
            }
            if (accumulate.lessThan(attempt)) {
               accumulate = attempt;
            }
         }
         catch (NumberFormatException notDouble) {
         }
      }
      currentActionNode.setStatus(accumulate);
      if (accumulate == InputLimits.TableStatus.NOSAVE) {
         outHard.add(opAct.name);
      }
   }

   /**
    * This method updates the object status by tracing through each node in
    * turn.
    */
   private void updateStatus() {
      if (isCropFile) {
         for (ParamDef key : cropParms.keySet()) {
            InputLimits il = key.il;
            ParameterVal curVal = cropParms.get(key);
            if (curVal == null) {
               continue;
            }
            try {
               double hardVal = Double.parseDouble(curVal.getVal(false));
               InputLimits.TableStatus attempt;
               if (decomp && !key.getDecomp()) {
                  attempt = InputLimits.TableStatus.OKAY;
               }
               else {
                  attempt = il.evaluateInput(hardVal);
               }
               if (status.lessThan(attempt)) {
                  status = attempt;
               }
            }
            catch (NumberFormatException notDouble) {
            }
         }
      }
      else {
         for (ActionValue act : actions) {
            OpAction opAct = act.getAction();
            //The maximimum number of parameters we will be dealing with
            int maxInd = opAct.getNumChildParms();
            //Accumulate holds the highest Table Status of the loop.
            InputLimits.TableStatus accumulate = InputLimits.TableStatus.OKAY;
            for (int index = 0; index < maxInd; index++) {
               ParamDef pDef = opAct.getColumn(index);
               InputLimits il = pDef.il;
               ParameterVal curVal = act.getVal(pDef);
               if (curVal == null) {
                  continue;
               }
               try {
                  double hardVal = Double.parseDouble(curVal.getVal(false));
                  InputLimits.TableStatus attempt;
                  if (decomp && !pDef.getDecomp()) {
                     attempt = InputLimits.TableStatus.OKAY;
                  }
                  else {
                     attempt = il.evaluateInput(hardVal);
                  }
                  if (accumulate.lessThan(attempt)) {
                     accumulate = attempt;
                  }
               }
               catch (NumberFormatException notDouble) {
               }
            }
            act.setStatus(accumulate);
         }
      }
   }

   /**
    * Returns the highest table status out of all the nodes in the document.
    *
    * @return
    */
   public InputLimits.TableStatus getTotalStatus() {
      if (isCropFile) {
         return status;
      }
      //Accumulate holds the highest Table Status of the loop.
      InputLimits.TableStatus accumulate = InputLimits.TableStatus.OKAY;
      for (ActionValue act : actions) {
         OpAction opAct = act.getAction();
         //The maximimum number of parameters we will be dealing with
         int maxInd = opAct.getNumChildParms();
         for (int index = 0; index < maxInd; index++) {
            ParamDef pDef = opAct.getColumn(index);
            InputLimits il = pDef.il;
            ParameterVal curVal = act.getVal(pDef);
            if (curVal == null) {
               continue;
            }
            try {
               double hardVal = Double.parseDouble(curVal.getVal(false));
               InputLimits.TableStatus attempt;
               if (decomp && !pDef.getDecomp()) {
                  attempt = InputLimits.TableStatus.OKAY;
               }
               else {
                  attempt = il.evaluateInput(hardVal);
               }
               if (accumulate.lessThan(attempt)) {
                  accumulate = attempt;
               }
            }
            catch (NumberFormatException notDouble) {
            }
         }
      }
      return accumulate;
   }

   /**
    * Used to retrieve the input status of a given node from it's name.
    *
    * @param name
    * @return
    */
   public InputLimits.TableStatus getStatus(String name) {
      for (ActionValue act : actions) {
         if (act.getAction().name.equals(name)) {
            return act.getStatus();
         }
      }
      return InputLimits.TableStatus.OKAY;
   }

   /**
    * Used during initialization, the last link in the dive chain. returns the
    * names of the nodes that are out of the hard limits.
    *
    * @return
    */
   public ArrayList<String> dive() {
      return outHard;
   }
}
