package nxm.sys.intr;

import java.util.ArrayList;

import nxm.sys.inc.AsciiMap;
import nxm.sys.lib.Args;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.Macro;
import nxm.sys.lib.Parser;
import nxm.sys.lib.Shell;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.TextFile;

/**
  Displays the EXPLAIN file for a given command

  @author Jeff Schoen
*/
public class Explain extends Intrinsic implements AsciiMap {

  /** reserved sections in explain file (should be in ALL UPPER CASE). */
  private static final String[] RESERVED_SECTIONS =
    { "FUNCTIONS", "FUNCTION", "SEEALSO", "SEE ALSO","MESSAGES", "EXAMPLES", "SWITCHES" };

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

  public int process () {

    // Is debugging turned on?
    boolean dbg = MA.getState("/DEBUG", ((M.debug&Debug.PRIMITIVE)!=0));
    verbose = MA.getState("/VERBOSE",false);

    // Get parameters
    String name       = MA.getU("NAME");              // Short command name
    String sect       = MA.getU("SECTION");           // Section in explain file
    int rowCount      = MA.getL("/NR",-1);            // -1=use default row count
    boolean generate  = MA.getState("/GEN", false);   // Generate an explain file
    boolean overWrite = MA.getState("/NEW", false);   // For /GEN option
    boolean check     = MA.getState("/CHECK", false); // Check the explain file
    boolean doWarn    = MA.getState("/WARN", true);   // Turn on warnings by default
    String outResName = MA.getU("/OUT", null);        // Output result parameter name

    Args args = Shell.parseCommand (M,name);

    if (args.name.equals("")) {
      M.warning ("'"+name+"' is not in the command dictionary. No explain file is available.");
      return FINISH;
    }
    String expName;
    if (args.name.startsWith("%")) {
      expName = M.io.name("HOMEPATH",args.name.substring(1),"EXP","AUX");
    } else {
      expName = M.io.name("EXP",args.name,"EXP",args.option);
    }
    // JC4: Do not do this here, it causes unnecessary warnings when just displaying the
    //      explain file for an OS (support='O') command.
    // String sourceName = getFullSourceName(args);

    if (generate) {          // Generate the explain file
      String sourceName = getFullSourceName(args);
      int temp = nxm.sys.lib.Generate.genExplainFromSource (sourceName, args.name, expName, overWrite, doWarn);
      if      (temp == -2) M.warning(expName+" already exists, use /NEW to overwrite");
      else if (temp == -1) M.warning("Unable to overwrite existing file "+expName+" check permissions.");
    }
    else if (check) {          // Check the explain file
      String sourceName = getFullSourceName(args);
      doWarn |= (outResName==null);
      int numMissing = checkExplainFile(args.name, expName, sourceName, doWarn, dbg);
      if (outResName != null) MR.put(outResName, numMissing);
    }
    else {            // Show the explain file
      if ((sect!=null) && sect.equalsIgnoreCase("SeeAlso")) sect = "See Also";
      showExplainFile(name, expName, sect, rowCount);
    }
    return FINISH;
  }

  /** Displays the explain file.
      @param fname    The file name.
      @param section  The name of the section to display.
      @param rowCount The number of rows (-1 for default).
  */
  @SuppressWarnings("deprecation")  // for M.terminal - since 3.7.1 for NXM-2813
  private void showExplainFile (String name, String fname, String section, int rowCount) {
    int      start     = 0;
    int      end       = 0;
    String   syntax    = M.dictionary.getSyntax(name);

    TextFile file = new TextFile(M,fname);
    if (!file.open(BaseFile.INPUT|BaseFile.NOABORT)) {
      M.println ("No Explain file found for command: '"+name+"'"); // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      return;
    }

    ArrayList<String> fileLines = new ArrayList<>();
    String text;
    while ((text=file.read())!=null) {
      fileLines.add(text);
    }
    fileLines.add("");
    fileLines.add("Syntax: ");
    fileLines.add("  "+syntax);
    fileLines.add("");
    file.close();

    String[] lines = fileLines.toArray(StringUtil.EMPTY_STRING_ARRAY);

    if ((section == null) || (section.length() == 0)) {
      // Display entire file
      start = 0;
      end   = lines.length-1;
    } else {
      // Save the section name in case the user wants the FUNCTION name in the
      // FUNCTION: section.  Make sure section is capitalized and ends with a colon.
      boolean inFunction = false;
      String  funcName = section.trim().toUpperCase();
      section = funcName;
      if (!section.endsWith(":")) { section += ":"; }

      for (String reservedSection : RESERVED_SECTIONS) {
        if (funcName.equals(reservedSection)) {
          funcName = null;
          break;
        }
      }

      // Find start
      for (start = 0; start < lines.length; start++) {
        String line = lines[start].toUpperCase();
        if (inFunction) {
          if (!(line.startsWith(" ") || line.startsWith("\t") || line.equals(""))) {
            inFunction = false;
          }
          else if (line.startsWith(" ")) {
            line = line.trim();
            int index = line.indexOf("<");
            if (index > 0) line=line.substring(0,index).trim(); // Special case for FUNC <P1> ...
            index = line.indexOf(":");
            if (index > 0) line=line.substring(0,index).trim(); // Special case for FUNC: ...
            index = line.indexOf("-");
            if (index > 0) line=line.substring(0,index).trim(); // Special case for FUNC: ...
            if (line.equals(funcName)) break;
          }
        } else {
          if (line.startsWith(section)) {
            break; // Start found
          } else if (line.startsWith("FUNCTION:") || line.startsWith("FUNCTIONS:")) {
            inFunction = (funcName != null);
          }
        }
      }

      // Find end
      for (end = start+1; end < lines.length; end++) {
        String line = lines[end].toUpperCase();
        // JC4: Subsequent lines in section may also start with "Section:" as is the case
        //      with some of the "See Also:" sections.
        if (!(line.startsWith(" ") || line.startsWith("\t") || line.equals("")
                                   || line.startsWith(section))) {
          end--;
          break; // Start of next section found
        }
      }

      boolean found  = (start < lines.length-1);
      boolean inLine = found && (lines[start].trim().length() > section.length()); //In-line SeeAlso

      if (found && ((end > start) || inLine)) {
        // start and end are valid
        end   = Math.min(end, lines.length-1); // handle section ending at EOF
      } else {
        // start and end are NOT valid
        start = 0;
        end   = lines.length-1;
      }
    }

    // Display the file
    int oldRowCount = M.terminal.getRows();
    M.terminal.setRows(rowCount);
    M.moreString(lines, start, end);
    M.terminal.setRows(oldRowCount);
  }

  /** Check an explain file for mission parameters or switches.
    @param cmdName The short command name
    @param fullExpName The short command name
    @param fullSourceName The full name of the source file associated with command
    @param doWarn To turn on warnings
    @param dbg To turn on debugging

    @return returns a count parameters, switches, msgs not referenced in the explain file
  */
  public int checkExplainFile (String cmdName, String fullExpName, String fullSourceName, boolean doWarn, boolean dbg) {

    if (dbg) System.out.println("EXPLAIN: Checking explain file "+fullExpName+" in source file "+fullSourceName+" warn? "+doWarn);

    SourceParser sp = new SourceParser(fullSourceName, doWarn, dbg);
    TextFile tf = new TextFile(M,fullExpName); // fullExpName is LC at this point

    ArrayList<String> expList = new ArrayList<>();
    if (tf.open(BaseFile.INPUT|BaseFile.NOABORT)) {
      String line = tf.read();
      while (line != null) {
        expList.add(line);
        line = tf.read();
      }
    } else {
      M.warning("could not open file: "+fullExpName);
    }
    tf.close();

    if (dbg) System.out.println("EXPLAIN CHECK: Number of lines in explain file = "+expList.size());

    // Check for parameters
    int missingParms = 0;
    String [] params = sp.getParameters();
    if (params != null) {
      for (int p = 0; p < params.length; p++) {
        if (dbg) System.out.println("EXPLAIN CHECK: PARAMETER["+p+"] "+params[p]);
        String line = "";
        boolean foundReference = false;
        int l = 0;
        for (l = 0; l < expList.size(); l++) {
          line = ""+expList.get(l);
          foundReference = lineStartsWithParam(line, params[p]);
          if (foundReference) break;
        }
        if (foundReference) {
          if (dbg) System.out.println("EXPLAIN CHECK: found reference to "+params[p]+" at line "+l+" in file");
        } else {
          if (doWarn) M.warning("Did not find reference to parameter "+params[p]);
          missingParms++;
        }
      }
    } else {
      if (verbose) M.info("No command line parameters found in source");
    }

    // Check for receive messages
    int missingRxMsgs = 0;
    String [] rxMsgs = sp.getReceiveMessages();
    if (rxMsgs != null) {
      for (int p = 0; p < rxMsgs.length; p++) {
        if (dbg) System.out.println("EXPLAIN CHECK: PARAMETER["+p+"] "+rxMsgs[p]);
        String line = "";
        boolean foundReference = false;
        int l = 0;
        for (l = 0; l < expList.size(); l++) {
          line = ""+expList.get(l);
          foundReference = line.trim().toUpperCase().startsWith(rxMsgs[p].toUpperCase());
          if (foundReference) break;
        }
        if (foundReference) {
          if (dbg) System.out.println("EXPLAIN CHECK: found reference to "+rxMsgs[p]+" at line "+l+" in file");
        } else {
          if (doWarn) M.warning("Did not find reference to RX message "+rxMsgs[p]);
          missingRxMsgs++;
        }
      }
    } else {
      if (verbose) M.info("No receive messages found in source");
    }

    // Check for send messages
    int missingSendMsgs = 0;
    String [] sendMsgs = sp.getSendMessages();
    if (sendMsgs != null) {
      for (int p = 0; p < sendMsgs.length; p++) {
        if (dbg) System.out.println("EXPLAIN CHECK: PARAMETER["+p+"] "+sendMsgs[p]);
        String line = "";
        boolean foundReference = false;
        int l = 0;
        for (l = 0; l < expList.size(); l++) {
          line = ""+expList.get(l);
          foundReference = line.trim().toUpperCase().startsWith(sendMsgs[p].toUpperCase());
          if (foundReference) break;
        }
        if (foundReference) {
          if (dbg) System.out.println("EXPLAIN CHECK: found reference to "+sendMsgs[p]+" at line "+l+" in file");
        } else {
          if (doWarn) M.warning("Did not find reference to TX message "+sendMsgs[p]);
          missingSendMsgs++;
        }
      }
    } else {
      if (verbose) M.info("No send messages found in source");
    }

    // Check for switches
    int missingSwitches = 0;
    String [] switches = sp.getSwitches();
    if (switches != null) {
      for (int s = 0; s < switches.length; s++) {
        if (dbg) System.out.println("EXPLAIN CHECK: Switch["+s+"] "+switches[s]);
        String line = "";
        boolean foundReference = false;
        int l = 0;
        for (l = 0; l < expList.size(); l++) {
          line = ""+expList.get(l);
          foundReference = line.trim().toUpperCase().startsWith(switches[s].toUpperCase());
          if (foundReference) break;
        }
        if (foundReference) {
          // Found a reference to this switch/parameter
          if (dbg) System.out.println("EXPLAIN CHECK: found reference to "+switches[s]+" at line "+l+" in file");
        } else {
          if (doWarn) M.warning("Did not find reference to switch "+switches[s]);
          missingSwitches++;
        }
      }
    } else {
      if (verbose) M.info("No SWITCHES found in source");
    }
    return (missingParms+missingRxMsgs+missingSendMsgs+missingSwitches);
  }

  private static boolean lineStartsWithParam (String line, String parm) {
    String cleanLine = cleanExpLine(line);
    String str = "<"+parm+">".toUpperCase(); // Check for positional parameter <NAME>
    boolean status = cleanLine.startsWith(str);
    if (!status) {
      str = "["+parm+"]".toUpperCase(); // Check for TAG-ONLY parameter [NAME]
      status = cleanLine.startsWith(str);
    }
    if (!status && cleanLine.startsWith("[")) {
      int i1 = cleanLine.indexOf(',');
      int i2 = cleanLine.indexOf(']');

      if ((i2 > 0) && (i1 > 0) && (i1 < i2)) {
        Parser parser = new Parser(cleanLine.substring(1,i2).trim());
        for (int i = 1; (!status) && (i <= parser.elements()); i++) {
          status = parm.equalsIgnoreCase(parser.get(i));
        }
      }
    }
    return status;
  }

  private static String cleanExpLine (String line) {
    return line.trim().toUpperCase();
  }

  private String getFullSourceName (Args args) {
    String fullSourceName = null;
    char support = args.support;
    String name = args.name;
    String option = args.option;
    if (support=='M') {
      name = name.toLowerCase();
      fullSourceName = Macro.getFileName(M,name,option);
    }
    else if (support=='P') {
      name = name.toLowerCase();
      fullSourceName = M.io.name("prim",name,"java",option);
    }
    else if (support=='I') {
      name = name.substring(0,1)+name.substring(1).toLowerCase();
      fullSourceName = M.io.name("intr",name,"java",option);
    }
    else if (support=='N') {
      name = name.toLowerCase();
      fullSourceName = M.io.name("nat",name,"c",option);
    }
    else {
      M.warning("Command support "+support+" not supported");
    }
    return fullSourceName;
  }

  private class SourceParser {
    private String [] tags     = StringUtil.EMPTY_STRING_ARRAY;
    private String [] rxMsgs   = StringUtil.EMPTY_STRING_ARRAY;
    private String [] sendMsgs = StringUtil.EMPTY_STRING_ARRAY;
    private String [] switches = StringUtil.EMPTY_STRING_ARRAY;
    private boolean warn = true;
    private boolean dbg = false;

    public SourceParser(String fullSourceName) {
      this(fullSourceName, true, false);
    }
    public SourceParser(String fullSourceName, boolean warn, boolean dbg) {
      this.dbg = dbg;
      this.warn = warn;
      load(fullSourceName);
    }

    public String[] getParameters() {
      return tags;
    }
    public String[] getReceiveMessages() {
      return rxMsgs;
    }
    public String[] getSendMessages() {
      return sendMsgs;
    }
    public String[] getSwitches() {
      return switches;
    }
    private int load (String fullSourceName) {
      String switchesString = null;
      String tagsString = null;
      String rxMsgString = null;
      String sendMsgString = null;
      int status = -1;
      // Generate the explain file from source code
      TextFile tfi = new TextFile(M,fullSourceName);
      if (tfi.open(TextFile.INPUT|TextFile.NOABORT)) {
        int linenum = 0; // line number (one-based)
        String line;
        while ((line=tfi.read()) != null) {
          linenum++;
          int iMA=-1,iMSG=-1,iMSGSend=-1;
          //if (dbg) System.out.println("SP: LINE "+ linenum + " '"+line+"'");
          if (line.trim().startsWith("//") || line.trim().startsWith("/*")) {
            //M.warning("================== COMMENT ================");
            //M.warning(line);
          } else {
            iMA = paramIndex(line);  // Look for parameters
            iMSG = rxMsgIndex(line); // Look for receive messages
            iMSGSend = sendMsgIndex(line); // Look for send messages
          }
          if (iMA >= 0) {
            // Found a MA reference, should be an argument.
            String mastring = line.substring(iMA);
            String tagName = nxm.sys.lib.Generate.getTagNameFromString(mastring);
            if (tagName == null) {
              if (warn) {
                M.warning("could not find param tag in string '"+mastring+"'");
                M.warning("Line "+linenum+": "+line);
              }
            } else if (tagName.startsWith("/")) {
              if (dbg) System.out.println("SP: Found switch '"+tagName+"'");
              if (switchesString == null) switchesString = tagName;
              else {
                if (switchesString.indexOf(tagName) < 0) {
                  switchesString += ","+tagName;
                }
              }
            } else {
              if (dbg) System.out.println("SP: Found param '"+tagName+"'");
              if (tagsString == null) tagsString = tagName;
              else {
                if (tagsString.indexOf(tagName) < 0) {
                  tagsString += ","+tagName;
                }
              }
            }

          }
          if (iMSG >= 0) {
            String msgstr = line.substring(iMSG);
            String tagName = nxm.sys.lib.Generate.getTagNameFromString (msgstr);
            if (tagName == null) {
              if (warn) {
                M.warning("WARN: could not find msg tag in string '"+msgstr+"'");
                M.warning("Line "+linenum+": "+line);
              }
            } else if (rxMsgString == null) {
              if (dbg) System.out.println("SP: Found RX msg '"+tagName+"'");
              rxMsgString = tagName;
            } else {
              if (dbg) System.out.println("SP: Found RX msg '"+tagName+"'");
              if (rxMsgString.indexOf(tagName+",") < 0) {
                rxMsgString += ","+tagName;
              }
            }

          }
          if (iMSGSend >= 0) {
            String msgstr = line.substring(iMSGSend);
            String tagName = nxm.sys.lib.Generate.getTagNameFromString (msgstr);
            if (tagName == null) {
              if (warn) {
                M.warning("could not find msg tag in string '"+msgstr+"'");
                M.warning("Line "+linenum+": "+line);
              }
            } else if (sendMsgString == null) {
              if (dbg) System.out.println("SP: Found TX msg '"+tagName+"'");
              sendMsgString = tagName;
            } else {
              if (sendMsgString.indexOf(tagName+",") < 0) {
                if (dbg) System.out.println("SP: Found TX msg '"+tagName+"'");
                sendMsgString += ","+tagName;
              }
            }

          }
        }  // end of WHILE

        tags = StringUtil.cleanSplit(tagsString, ",");
        rxMsgs = StringUtil.cleanSplit(rxMsgString, ",");
        sendMsgs = StringUtil.cleanSplit(sendMsgString, ",");
        switches = StringUtil.cleanSplit(switchesString, ",");
        status = 1;
        tfi.close();
      } else {
        // Input source file could not be opened.
      }
      return status;
    }
    public int paramIndex(String line) {
      return line.indexOf("MA"+".");  // Look for parameters
    }
    public int rxMsgIndex(String line) {
      int index = line.indexOf("msg.name.equals"+LPAREN);
      if (index >= 0) return index;
      index = line.indexOf("msg.name.equals "+LPAREN);
      if (index >= 0) return index;
      index = line.indexOf("msg.name.startsWith"+LPAREN);
      if (index >= 0) return index;
      index = line.indexOf("msg.name.startsWith "+LPAREN);
      return index;
    }
    public int sendMsgIndex(String line) {
      int index = line.indexOf ("MQ.put"+LPAREN);
      if (index >= 0) return index;
      index = line.indexOf ("MQ.put "+LPAREN);
      return index;
    }
  }
}
