package nxm.sys.intr;

import java.util.Arrays;
import java.util.Iterator;
import java.util.Map;

import nxm.sys.inc.Keyable;
import nxm.sys.lib.BaseFile;
import nxm.sys.lib.Command;
import nxm.sys.lib.Convert;
import nxm.sys.lib.Data;
import nxm.sys.lib.DataFile;
import nxm.sys.lib.FileName;
import nxm.sys.lib.Intrinsic;
import nxm.sys.lib.KeyVector;
import nxm.sys.lib.Keywords;
import nxm.sys.lib.Midas;
import nxm.sys.lib.MidasException;
import nxm.sys.lib.StringUtil;
import nxm.sys.lib.Table;
import nxm.sys.lib.TextFile;

/** Handles the creation and display of results parameters.
    As of NeXtMidas 2.5.0 this is a superset of both the {@link Set SET} and {@link Get GET} commands.

    @author  Jeff Schoen
    @author  Jay Hallam
    @author  J. Czechowski IV
  */
public class Results extends Intrinsic {

  /** Delimiter used when showing 1) empty strings or 2) strings beginning or ending with space(s) */
  public static char SHOW_DELIM = '"';

  /** Default filename for /save and /load switch, if not given */
  public static final String DEF_FILENAME = "results.tmp";

  // These are package-private, only RESULTS, SET, and GET should have access to them.
  String          label,value,outRes;
  FileName        inFile,outFile;
  boolean         all,isSet;

  // the global results table reference
  nxm.sys.lib.Results rt;

  /** List of restricted result keywords (i.e. they cannot be overwritten) (must be in alphabetical order!) */
  String[]  restrictedSets = {"AUX", "ENV", "OPT", "RAM", "REG"};

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

  /** Gets the command-line parameters, allows SET and GET to override. */
  public int open () {
    int len;
    rt = MR;

    label   = MA.getU("LABEL");
    value   = MA.getUQ("VALUE");
    all     = MA.getZ("/ALL");
    outRes  = MA.getU("/RES",null);
    isSet   = (value != null);

    // load results from a file
    if ((len=MA.getLength("/LOAD"))>=0) {        // added for Bug 1452
      inFile = (len==0)? new FileName(DEF_FILENAME) : MA.getFileName("/LOAD");
      BaseFile bf = BaseFile.getInstanceFor(this, inFile);    // get file instance
      if      (bf instanceof TextFile) loadFromTextFile(bf);
      else if (bf instanceof DataFile) loadFromDataFile(bf);
      return FINISH;
    }

    // save results to a file
    if ((len=MA.getLength("/SAVE"))>=0) {        // added for Bug 1452
      outFile = (len==0)? new FileName(DEF_FILENAME) : MA.getFileName("/SAVE");
      if (rt.get(label) != null) outRes = rt.get(label).toString();  // Get sub-results
      BaseFile bf = BaseFile.getInstanceFor(this, outFile);          // get file instance
      if      (bf instanceof TextFile) saveToTextFile(bf, outRes);
      else if (bf instanceof DataFile) saveToDataFile(bf, outRes);
      else throw new MidasException("Unsupported file type "+bf.getClass());
      return FINISH;
    }

    return NORMAL;
  }

  public int process () {

    // Find the global results table if change/search is to occur there
    rt = MR;
    boolean sp = MA.getState("/PARENT"), sg = MA.getState("/GLOBAL");
    if (sp&&sg) M.warning("/PARENT and /GLOBAL switches both present. Using /GLOBAL.");
         if (sg) rt = (nxm.sys.lib.Results)MR.getRoot();
    else if (sp) rt = (nxm.sys.lib.Results)MR.getPrevious();

    if (isSet) {                                               // Set a result
      int attr = 0; // Get special table arguments to allow overriding of protected values
      if (value==null && label.indexOf('(')<0) M.error("No value specified for "+label);
      if (MA.getState("/FORCE"))   attr |= Table.FORCE;
      if (MA.getState("/ADDROOT")) attr |= Table.ADDROOT;
      if (isRestricted(label)) M.error("Forbidden to overwrite result: "+label); // for BUG 2443
      if (label.length() < 2 || label.charAt(1)!=':') label = "_:"+label;
      rt.put(label,value,attr,args);
      return FINISH;
    }

    // Radix to use with GET (-1=NormalOutput)
    int radix = -1;
    if (MA.getState("/HEX")) radix=16;
    if (MA.getState("/OCT")) radix=8;
    if (MA.getState("/BIN")) radix=2;
    radix = MA.getL("/RADIX",radix);

    // support for /COUNT added in 3.1.0 (Bug 2506)
    int count = 0;
    boolean doCount = false;
    String countID = MA.get("/COUNT");
    if (countID != null) { countID = countID.toUpperCase(); doCount = true; }

    if (outRes != null) {                                      // Output to a result
      Object val = Data.toRadix(radix, rt.get(label));
      if (val == null) M.println("Result ["+label+"] not found");  // since 3.7.1 using M.println instead of M.terminal.writeln to allow logging - NXM-1813
      MR.put(outRes, val);
    }
    else if (label.length()==0) {                              // Display all results
      count = listMultipleResult(M, rt, label, 0, all, radix, doCount, verbose);
    }
    else if (StringUtil.isWildcardString(label)) {             // Display matching results
      count = listMultipleResult(M, rt, label, 0, all, radix, doCount, verbose);
    }
    else {                                                     // Display a single entry
      count = listSingleResult(M, rt, label, 0, all, radix, doCount, verbose);
    }

    if (doCount) {
      MR.put(countID, count);  // add the new result for the count
      doCount=false; count=0;  // reset
      listSingleResult(M,rt,countID,0,all,radix,true,true);  // show the count
    }

    return FINISH;
  }

  /** Save the Results table (or subset of it) to a Text file
      @param bf The type of file specified by the user
      @param myRes The results sub-set
   */
  private void saveToTextFile (BaseFile bf, String myRes) {
    TextFile tf = (TextFile)bf;
    tf.open(TextFile.OUTPUT|TextFile.FORCEABORT);
    if (label.equals("*") || label.equals("")) {      // save all of Results table
      tf.write( rt.toString() );
    } else if (myRes != null) {                       // Just save Results specified
      String outString = label+ "=" +myRes;
      tf.write(outString);
    } else {                                          // Results not found
      M.warning("Results " +label+ " not found");
    }
    tf.close();
  }

  /** Save the Results table (or subset of it) to a MIDAS file.
      NOTE: This is the default behaviour. If you wish to save the results to a
      TextFile, use {@link #saveToTextFile(BaseFile, Results, String)}.
      @param bf The type of file specified by the user
      @param myRes The results sub-set
   */
  private void saveToDataFile (BaseFile bf, String myRes) {
    DataFile df = (DataFile)bf;
    df.open(DataFile.OUTPUT);
    df.setType(1000);   // matches X-Midas
    df.setFormat("TA"); // matches X-Midas
    Keywords kw = df.getKeywordsObject();
    kw.add("RESULTS_DUMP","START");  // matches X-Midas
    if (label.equals("*") || label.equals("")) {       // save all of Results table
      String[] keys = rt.getKeys();
      for (String key : keys) {
        if (((rt.get(key) instanceof Data)     ||  //Make sure is appropriate data type
             (rt.get(key) instanceof Table)    ||
             (rt.get(key) instanceof String))) {
          kw.add(key, rt.get(key));
        }
      }
    } else if (myRes != null) {                       // Just save Results specified
      kw.add(label, myRes);
    } else {                                          // Results not found
      M.warning("Results " +label+ " not found");
    }
    kw.add("RESULTS_DUMP","END");  // matches X-Midas
    df.close();
  }

  /** Load the Results table with data from a Text file.
      Presumably, the file was created using <code>res /save</code>.
      @param bf The file
   */
  private void loadFromTextFile (BaseFile bf) {
    Table myInput = new Table ((TextFile)bf);  // convert to table
    loadFromTable(myInput);
  }

  /** Load the Results table with data from a MIDAS file. Presumably, the file
      was created using <code>res /save</code>. By X-MIDAS precedent, the data
      is found in the extended header section using the keyword <code>RESULTS_DUMP
      </code> with values of <code>START</code> prior to the data and <code>
      END</code> after the data.
      @param bf The file
   */
  private void loadFromDataFile (BaseFile bf) {
    DataFile df = (DataFile)bf;   // Convert to DataFile
    df.open(DataFile.INPUT|DataFile.FORCEABORT);  // Open the file
    Keywords myKeys = df.getKeywordsObject();     // Get the keywords
    Keywords.Iterator kwi = myKeys.iterator();
    Keywords.Key      key = null;
    boolean loadIt = false;                      // Flag for section of file we want
    Table myRes = new Table();
    while(kwi.hasNext()) {                       // Iterate through data
      key = kwi.next();
      if ((key.name.equals("RESULTS_DUMP")) && (key.value.toString().equals("START"))) {
        loadIt = true;
      } else if ((key.name.equals("RESULTS_DUMP")) && (key.value.toString().equals("END"))) {
        loadIt = false;
      }
      if (loadIt && (!key.name.equals("RESULTS_DUMP") && (!isRestricted(key.name)))) {
        Object myObj =Convert.s2o(key.value.toString(), null);
        myRes.put(key.name, myObj);
      } else if (isRestricted(key.name)) M.warning("Forbidden to overwrite " +key.name);
    }
    loadFromTable(myRes);
    df.close();
  }

  /** Load the Results table from a given table. Prior to writing to the Results
      table, each key is checked to ensure it is not a restricted (system) key.
      See {@link #restrictedSets} for list of restricted keys.
      @param myRes The given table
   */
  private void loadFromTable (Table myRes) {
    String[] keys = myRes.getKeys();
    for (String key : keys) {
      if (isRestricted(key)) M.warning("Forbidden to overwrite " +key);
      else rt.put(key, myRes.get(key));
    }
  }

  /** Check a given string for restricted Results key words
      @param key The given String
      @return <code>True</code> if the String is restricted
   */
  private boolean isRestricted (String key) {
    return Arrays.binarySearch(restrictedSets, key) >= 0;
  }

  /** List a single entry in the current table that matches to the Midas
      terminal screen.
      @param M The Midas context
      @param map to search
      @param key to be matched
      @param level The current level of recursion. Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
  */
  static void listSingleResult (Midas M, Map<?,?> map, String key, int level, boolean recurse) {
    listSingleResult(M,map,key,level,recurse,-1,false,false);
  }

  /** List a single entry in the current table that matches to the Midas
      terminal screen.
      @param M The Midas context
      @param map to search
      @param key to be matched
      @param level The current level of recursion. Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
      @param doCount Whether or not to keep track of and display the count of matches to a filter (Since 3.1.0)
      @param verbose With doCount, whether or not to display all matches of filter as well as count (Since 3.1.0)
  */
  private static int listSingleResult (Midas M, Map<?,?> map, String key,
                                        int level, boolean recurse, int radix, boolean doCount, boolean verbose) {
    int count = 0;
    Object result = map.get(key);
    if (result==null) M.println("Result ["+key+"] not found");
    else count = doMultiple (M,key,result,"*",level,recurse,level,radix,doCount,verbose);
    return count;
  }

  /** List all the entries in the current table that match the
      requested wildcard pattern to the Midas terminal screen.
      @param M The Midas context
      @param map to search
      @param match to be matched ("*" for all, or chars without "*"
      @param level The current level of recursion.  Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
  */
  static void listMultipleResult (Midas M, Map<?,?> map, String match, int level, boolean recurse) {
    listMultipleResult(M,map,match,level,recurse,-1,false,false);
  }

  /** List all the entries in the current table that match the
      requested wildcard pattern to the Midas terminal screen.
      @param M The Midas context
      @param map to search
      @param match to be matched ("*" for all, or chars without "*"
      @param level The current level of recursion.  Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
      @param doCount Whether or not to keep track of and display the count of matches to a filter (Since 3.1.0)
      @param verbose With doCount, whether or not to display all matches of filter as well as count (Since 3.1.0)
  */
  private static int listMultipleResult (Midas M, Map<?,?> map, String match,
                          int level, boolean recurse, int radix, boolean doCount, boolean verbose) {
    // if we are processing sub-levels, then change matching to * so that variables under map are printed
    if (level > 0) {
      match = "*";
    }
    int count = 0;
    for (Iterator<?> mi=map.entrySet().iterator(); mi.hasNext(); ) {
      Map.Entry<?,?> entry = (Map.Entry<?,?>)mi.next();
      String key = entry.getKey().toString(); if (key==null) continue;
      Object value = entry.getValue(); if (value==null) continue;
      count += doMultiple (M, key, value, match, level, recurse, -1, radix, doCount, verbose);
    }
    return count;
  }

  /** This method is public only to used by nxm.sys.prim.system (otherwise it should be package-private).
      @see #listMultipleResult(Midas, Map, String, int, boolean)
      @param M The Midas context
      @param map to search
      @param match to be matched ("*" for all, or chars without "*"
      @param level The current level of recursion.  Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
   */
  public static void listMultiple (Midas M, Map<?,?> map, String match, int level, boolean recurse) {
    listMultipleResult(M, map, match, level, recurse);
  }

  /** List all the entries in the current keyable that match the
      @param M       The Midas context
      @param keyable The keyable.
      @param match   To be matched ("*", "", null for all; or a wildcard string).
      @param level   The current level of recursion. Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local item.
  */
  private static void listMultipleKeyable (Midas M, Keyable keyable, String match,
                                           int level, boolean recurse, int radix) {
    String keys[] = keyable.getKeys();
    for (String key : keys) {
      if (key == null) continue;
      Object value = keyable.getKey(key);
      doMultiple(M, key, value, match, level, recurse, -1, radix, false, false);
    }
  }

  /** List all the entries in the current key vector that match the
      @param M       The Midas context
      @param kv      The key vector.
      @param match   To be matched ("*", "", null for all; or a wildcard string).
      @param level   The current level of recursion. Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local item.
  */
  private static void listMultipleKeyVector (Midas M, KeyVector kv, String match,
                                             int level, boolean recurse, int radix) {
    for (int i=0; i<kv.size(); i++) {
      String key    = kv.getKey(i);
      Object value  = kv.get(i);
      doMultiple(M, key, value, match, level, recurse, -1, radix, false, false);
    }
  }

  /** List the current result if the key matches the given pattern and recurse as needed.
      @param M       The Midas context
      @param key     The current result name.
      @param result  The current result value.
      @param match   To be matched ("*", "", null for all; or a wildcard string).
      @param level   The current level of recursion. Should be set to zero unless being called recursively.
      @param recurse TRUE recurses into tables, FALSE only checks local table
      @param rlevel  Force recurse on this level
      @param radix   The radix
      @param doCount Whether or not to keep track of and display the count of matches to a filter (Since 3.1.0)
      @param verbose With doCount, whether or not to display all matches of filter as well as count (Since 3.1.0)
  */
  static int doMultiple (Midas M, String key, Object result, String match,
                                 int level, boolean recurse, int rlevel, int radix, boolean doCount, boolean verbose) {
    int count = 0;
    if (match==null || match.length()==0 || StringUtil.isWildcardMatch(match,key)) {
      // doCount and verbose checks added in 3.1.0 to support the /COUNT switch (Bug 2506)
      count++;
      if (!doCount || verbose) M.println(formatEntry(key,result,level,radix));
    }
    else return count;

    if (result instanceof nxm.sys.lib.Results) {
      M.println(formatEntry(key,result,level,radix)); // global table reference
    }
    else if (recurse || level==rlevel) {
      if (result instanceof Map) {
        count += listMultipleResult(M,(Map< ? , ? >)result,match,level+1,recurse,radix, doCount,verbose);
      }
      else if (result instanceof KeyVector) {
        listMultipleKeyVector(M,(KeyVector)result,match,level+1,recurse,radix);
      }
      else if (result instanceof Keyable && !(result instanceof Command)) { // Keyable but not Table or KeyVector
        listMultipleKeyable(M,(Keyable)result,match,level+1,recurse,radix);
      }
    }
    return count;
  }

  /** @see Results#formatEntry(String, Object, int, int) */
  private static String showEntry (String key, Object object, int level) {
    return formatEntry(key,object,level,-1);
  }

  /**
    Returns a string containing the key/value pair formatted for output to the screen.
    Each entry is formatted:
      [stringsize][T,S,K,M,O]: [keyname] = [keyvalue]
    where,
      S=String, T=Table, K=KeyVector, M=Message, O=OtherObject, etc.
    @param key
    @param object associated with the key
    @param level
    @return string formatted text describing the table entry
  */
  static String formatEntry (String key, Object object, int level, int radix) {
    String spec;
    String obj;
    if (object instanceof Data) {
      spec = ((Data)object).getSpecs();
    }
    else {
      char type = Data.getDataTypeLetter(object);
      int  mult = Data.getMultiplier(object);
      if (mult < 0) {
        spec = String.valueOf(type);
      } else {
        spec = String.valueOf(mult) + String.valueOf(type);
      }
    }

    object = Data.toRadix(radix, object);

    if (object instanceof Map) {
      String name = object.getClass().getName();
      int i=name.lastIndexOf('$'); if (i<0) i=name.lastIndexOf('.');
      obj = name.substring(i+1)+" of "+((Map<?,?>)object).size()+" entries";
    }
    else if (object instanceof Message) {
      obj = "A "+((Message)object).name+" message";
    }
    else if (object instanceof Keyable && !(object instanceof Command)) {
      String name = object.getClass().getName();
      int i=name.lastIndexOf('$'); if (i<0) i=name.lastIndexOf('.');
      obj = name.substring(i+1)+" with "+((Keyable)object).getKeys().length +" keys";
    }
    else if (object instanceof String[]) {
      obj = "Array of "+((String[])object).length+" strings";
    }
    else if (object instanceof String) {
      String str = obj = object.toString();
      if (str.startsWith(" ") || str.endsWith(" ") || str.equals("")) obj = SHOW_DELIM+str+SHOW_DELIM;
    }
    else if (object == null) {
      obj = "null";
    }
    else {
      obj = object.toString();
    }
    return format(key, obj, spec, level);
  }

  /** Formats a key+obj+spec for output. */
  private static String format (String key, String obj, String spec, int level) {
    StringBuilder str = new StringBuilder();

    // Type specifier
    for (int i = spec.length(); i < 3; i++) str.append(' ');
    str.append(spec).append(": ");

    // Indentation + Key
    for (int i = 0; i < level; i++) str.append(' ');
    str.append(key);

    // Value
    for (int i = str.length(); i < 20; i++) str.append(' ');
    str.append(" = ").append(obj);

    return str.toString();
  }


  @Override
  public String getStatus() {
    Object obj = rt.get(label);
    return (obj==null)? "NULL" : obj.toString();
  }
}
