package nxm.sys.lib;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import nxm.sys.inc.InternalUseOnly;

/**
  A class to perform Dictionary functions.

  This is initialized at startup and contains the execution description of
  all of the commands that are available within the environment. Each entry
  within this member is a line.

  The table classes are not used because command abbreviations produce
  few table hits.  It is faster to lookup entries in an alphabetically
  arranged array.

  @author Jeff Schoen
*/
public class Dictionary {
  /** array containing alphabetized entries */
  public String[] entry;

  /** number of entries */
  public int size;

  /** hash table for fast access to all previous lookups */
  public Table hotlist;

  private int[] alpha;
  private int inc=128;

  /** Initialize the Dictionary Table */
  public Dictionary () {
    entry = new String[1024];
    alpha = new int[27];
    hotlist = new Table();
    size = 0;
  }

  /** Add the following specified string into the entries array.
   *  @param string string to add to the entries array
   */
  public void append (String string) {
    if (size>=entry.length) expand(inc);
    entry[size++] = string;
  }

  /**
     Find the place for the command in the dictionary.

     @param string - command to be inserted into dictionary
     @return - desired index to insert this command (index could be out of entry buffer range)
   */
  public int place (String string) {
    String tmpa,tmpb; int lena,lenb;
    lena = string.indexOf(';');
    tmpa=string.substring(0,lena);
    for (int i=0; i<size; i++) {
      tmpb = entry[i];
      lenb = tmpb.indexOf(';');
      tmpb=tmpb.substring(0,lenb);
      if (tmpb.compareTo(tmpa)>=0) return i;
    }

    // insert location is at end and my be out of entry buffer range
    return size;
  }

  /**
     Insert a command at specified index

     @param string - command to be inserted into dictionary
     @param desiredIndex - location to insert it (may be beyond entry buffer range, but MUST be less
            than (entry.length + inc - 1))
   */
  public void insert (String string, int desiredIndex) {
    String cmdName = string.substring(0, string.indexOf(':')+1); // Gets "<CMD>;<OPT>:"

    // Ensure adequate entry array length, as desired index may be beyond buffer(Since 3.5.2 Bug_2844)
    if (desiredIndex>=entry.length) expand(inc);

    // Bug 369 If cmd matches, skip this, just update
    if ((entry == null) || (entry[desiredIndex] == null) || (!entry[desiredIndex].startsWith(cmdName))) {
      if (size>=entry.length) expand(inc);
      for (int i=size; i>desiredIndex; i--) entry[i] = entry[i-1];
      size++;
    }
    if (desiredIndex==size) size++; // last non-SYS entry needs to be accounted for (Bug 2358)

    entry[desiredIndex] = string;
  }


  /**
     Insert a command at index found by place method

     @param string - command to be inserted into dictionary
   */
  public void insert (String string) {
    int desiredIndex = place(string);
    insert (string,desiredIndex);
  }

  /** Expand the array containing alphabetized entries by specified increment.
   *  @param inc increment */
  public void expand (int inc) {
    int newlen = entry.length + inc;
    String[] newentry = new String[newlen];
    for (int i=0; i<size; i++) newentry[i]=entry[i];
    entry = newentry;
  }

  /** Clear the hotlist and reset the number of entries to zero. */
  public void clear() {
    hotlist.clear();
    size=0;
  }

  /** Finds the percent macros in the current home path.
      @param midas The midas context to use.
      @return The list of all macros in the current HOMEPATH, listed and sorted in the form
              <code>{ "%MACRO1", "%MACRO2", ... }</code>.
              Since NeXtMidas 2.8.3: returns empty array if none is found or if HOMEPATH
              points to a non-existent directory (instead of returning null) and prints a warning.
   */
  public static String[] findPercentMacros (Midas midas) {
    final String homepath = midas.getResults().getS("AUX.HOMEPATH");
    String[] files = midas.io.listFiles(homepath, "*", "mm");
    if (files == null) {  // BUG 2281 (catch null case for invalid homepath and print warning)
      files = StringUtil.EMPTY_STRING_ARRAY;
      midas.warning("Unable to find percent macros at invalid HOMEPATH: "+homepath);
    }
    else {
      for (int i = 0; i < files.length; i++) {
        files[i] = "%" + files[i].substring(0,files[i].indexOf(".mm")).toUpperCase();
      }
      Arrays.sort(files);
    }
    return files;
  }


  /** Finds all of the entries that start with a given prefix. This is used by the tab-complete
      functions.
      @param match The prefix.
      @return The list of entries that match.
   */
  List<String> findMatches (Midas midas, String match) {
    ArrayList<String> matches = new ArrayList<>();

    if (match.startsWith("%")) {
      String[] macros = findPercentMacros(midas);
      for (int i = 0; i < macros.length; i++) {
        if (macros[i].startsWith(match)) {
          matches.add(macros[i]);
        }
      }
    } else {
      int len = match.length();
      if (len<=0) return matches;
      int ia = match.charAt(0) - 'A';
      if (ia<0 || ia>25) return matches;
      for (int i=alpha[ia]; i<alpha[ia+1]; i++) {
        int lgth = entry[i].indexOf(':');
        if (entry[i].startsWith(match) && (lgth > 0)) {
          matches.add(entry[i].substring(0,lgth));
        }
      }
    }
    return matches;
  }

  /** find match in Dictionary (abbreviations allowed)
   *
   *  @param match String to match against
   *  @return index of value
   */
  public int findMatch (String match) {
    int len = match.length();
    if (len<=0) return -1;
    int ia = match.charAt(0) - 'A';
    if (ia<0 || ia>25) return -1;
    for (int i=alpha[ia]; i<alpha[ia+1]; i++) {
      if (entry[i].startsWith(match)) {
        int i2 = entry[i].indexOf(',');
        int i1 = entry[i].indexOf(':');
        int la = Integer.valueOf(entry[i].substring(i1+1,i2)).intValue();
        if (len>=la) return i; // abbreviation allowed
      }
    }
    return -1;
  }

  /** find match in Dictionary (only exact match allowed)
  *
  *  @param match String to match against
  *  @return index of value
  */
  @InternalUseOnly
 public boolean isDictionaryEntry (String match) {
   int len = match.length();
   if (len<=0) return false;
   int ia = match.charAt(0) - 'A';
   if (ia<0 || ia>25) return false;
   for (int i=alpha[ia]; i<alpha[ia+1]; i++) {
     if (entry[i].startsWith(match)) {
       int lengthCmdName = entry[i].indexOf(";");
       if (lengthCmdName ==len) return true;

     }
   }
   return false;
 }

  /** find Parser
   *
   *  @param match String to match against
   *  @return parser
   */
  public Parser findParser (String match) {
    Object obj = hotlist.get(match);
    if (obj!=null) return (Parser)obj;
    int i = findMatch(match);
    if (i<0) return null;
    Parser parser = new Parser (entry[i]);
    hotlist.put(match,parser);
    return parser;
  }

  /** find name
   *
   *  @param match String to match against
   *  @return name
   */
  public String findName (String match) {
    Parser parser = findParser(match);
    if (parser == null) return null;
    String name = parser.get(1);
    int i = name.indexOf(';');
    name = name.substring(0, i);
    return name;
  }

  /** Return the entry contained at the specified index.
   *  @param i index
   *  @return entry at the specified index
   */
  public final String get (int i) {
    return entry[i];
  }

  /** Gets one of the entries in a table where the keys match the parameters used with
      the UPDATE command. Note that only the default arguments given are included.
      @param i The entry number.
      @return The table representing the dictionary entry.
      @see #get(int)
      @see #getSize()
      @since NeXtMidas 2.3.1
   */
  public final Table getEntry (int i) {
    Table  tbl    = new Table();
    String entry  = get(i);
    Parser parser = new Parser(entry);

    String nameOptAbbr = parser.get(1); // COMMAND;OPT:ABBR
    int indexColon = nameOptAbbr.indexOf(':');
    int indexSemi  = nameOptAbbr.indexOf(';');
    int abbrLen    = Convert.s2l(nameOptAbbr.substring(indexColon+1));
    String name = nameOptAbbr.substring(0,indexSemi);
    String abbr = nameOptAbbr.substring(0,abbrLen);
    tbl.put("NAME",  name);
    tbl.put("OPT",   nameOptAbbr.substring(indexSemi+1,indexColon));
    tbl.put("ABBR",  abbr);
    tbl.put("CMD",   abbr+"*"+name.substring(abbrLen)); // COM*MAND
    tbl.put("SUP",   parser.get(2));
    tbl.put("NARGS", parser.get(3));
    for (int arg = 1; (arg+3) <= parser.elements(); arg++) {
      tbl.put("DEF"+arg, parser.get(arg+3));
    }
    return tbl;
  }

  /** Gets the syntax for a command.
      @param name The command name
      @return The table representing the dictionary entry.
      @since NeXtMidas 2.5.4
   */
  public final String getSyntax (String name) {
    int    iname  = findMatch(name);
    String syntax = null;

    if (iname >= 0) {
      String entry  = get(iname);
      Parser parser = new Parser(entry);

      String nameOptAbbr = parser.get(1); // COMMAND;OPT:ABBR
      int indexColon = nameOptAbbr.indexOf(':');
      String nargs = parser.get(3);

      syntax = nameOptAbbr.substring(0,indexColon);
      for (int arg = 1; (arg+3) <= parser.elements(); arg++) {
        String param = parser.get(arg+3);
	int    eq    = param.indexOf("=");

	if (eq < 0) syntax += " <P"+arg+">";
	else        syntax += " <"+param.substring(0,eq)+">";
      }
      if (nargs.indexOf("+") > 0) {
        syntax += " ...";
      }
    }
    return syntax;
  }

  /** Return the number of entries.
   *  @return number of entries */
  public final int getSize () {
    return size;
  }


  /** generate alpha (set values) */
  public void genAlpha () {
    int ia;
    for (ia=0; ia<27; ia++) alpha[ia] = size;
    for (int i=0; i<size; i++) {
      ia = entry[i].charAt(0)-'A';
      if (ia<0||ia>26) continue;
      if (i<alpha[ia]) alpha[ia]=i;
    }
    for (ia=25; ia>=0; ia--) if (alpha[ia]==size) alpha[ia]=alpha[ia+1];
  }

  /** Set the entries within the instance to the values that are contained in
      the given array. The size of the instance will be set to the number of
      non null elements in the array.

      This completely overwrites any pre-existing entries.
      @param entry entries
  */
  public void fromArray (String[] entry) {
    this.entry = entry;
    for (size=0; size<entry.length && entry[size]!=null; size++);
    genAlpha();
  }

  /** Return a string containing the current entries, one per line.
   *  @return string containing the current entries */
  public String getList () {
    StringBuilder sb = new StringBuilder(1024);
    for (int i = 0; i < size; i++) {
      sb.append(entry[i]);
      sb.append("\n");
    }
    return sb.toString();
  }
}
