package nxm.sys.lib;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.Hashtable;

import nxm.sys.inc.Keyable;

/**
  A class to implement a query into an Object.

  This introspects an object and makes its set and get methods
  available for status and control.  A method is considered a property
  if it has a getXXX method with no arguments.  If a setXXX method of
  the same name with one argument of the getXXX methods return type,
  that property is considered controllable and will show "=&gt;" rather than
  just "=" between the property name and its short string value in the display.

  Objects that wish to hide some of their getXXX methods from display
  can include a String propertyList="Prop1,Prop2,...PropN" in the class
  to override the dynamically created list.

  TBD: If a getPropN() method cannot be found, it will look for just propN().
  Thus, non-standard property methods can be made accessible.

  @author Jeff Schoen

*/
public class Query {

  private Object obj;
  private int items, maxitems;
  private String[] keys,vals;
  private Method[] sets,gets,setS;
  private String propertyList;
  /** parent Query object */
  public Query parent;

  /** Create a Query
   *
   *  @param obj the object to be queried
   *  @param list  property list
   *  @param maxitems maximum size of setters, getters, etc. arrays
   */
  public Query (Object obj, String list, int maxitems) {
    this.obj = obj;
    this.maxitems = maxitems;
    if (list!=null && list.length()>0) propertyList = list;
    keys = new String[maxitems];
    vals = new String[maxitems];
    sets = new Method[maxitems];
    gets = new Method[maxitems];
    setS = new Method[maxitems];
    parse();
  }

  /** parse the Object to be introspected */
  public void parse() {
    items=0;
    if (obj instanceof Table) parseT();
    else if (obj instanceof Hashtable) parseHT();
    else if (obj instanceof KeyVector) parseKV();
    else if (propertyList!=null) parseList(propertyList);
    else try {
      Field field = obj.getClass().getField("propertyList");
      String list = (String)field.get(obj);
      if (list==null) parseClass();
      else parseList("Class,"+list);
    } catch (Exception e) { parseClass(); }
  }

  private void parseClass() {
    Class<?> clas = obj.getClass();
    Method[] methods = clas.getMethods();
    for (int i=0; i<methods.length && items<maxitems; i++) {
      String name = methods[i].getName();
      if (name.length()>3 && name.startsWith("get")) {
        String key = name.substring(3);
        if (methods[i].getParameterTypes().length>0) continue;
        if (key.equals("Key") && obj instanceof Keyable) continue;
        gets[items] = methods[i];
        sets[items] = findSetMethod(key,i,methods);
        setS[items] = findSetStringMethod(key,i,methods);
        keys[items++] = key;
      }
    }
    if (obj instanceof Keyable) {
      String[] keyables = ((Keyable)obj).getKeys();
      for (int i=0; i<keyables.length; i++) {
        String keyable = keyables[i];
        for (int j=0; j<items && keyables!=null; j++) {
          if (keyable.equalsIgnoreCase(keys[i])) keyables=null;
        }
        if (keyables!=null && items<maxitems) keys[items++] = keyable;
      }
    }
  }

  private void parseList (String list) {
    Class<?> clas = obj.getClass();
    Method[] methods = clas.getMethods();
    Parser p = new Parser(list);
    for (int i=1; p.more() && items<maxitems; i++) {
      String key = p.get(i);
      String getname = "get"+key;
      for (int j=0; j<methods.length; j++) {
        if (getname.equals(methods[j].getName()) &&
            methods[j].getParameterTypes().length==0) {
          gets[items] = methods[j];
          sets[items] = findSetMethod(key,j,methods);
          keys[items++] = key;
          break;
        }
      }
    }
  }

  private void parseT() {
    Table t = (Table)obj;
    Enumeration<String> e = t.keys();
    items = Math.min(maxitems,t.size());
    for (int i=0; i<items; i++) keys[i] = e.nextElement();
    Table tp = t.getPrevious();
    if (tp!=null && items<maxitems) keys[items++] = "getPrev";
  }

  @SuppressWarnings("unchecked")
  private void parseHT() {
    Hashtable<String,?> ht = (Hashtable<String,?>)obj;
    Enumeration<String> e = ht.keys();
    items = Math.min(maxitems,ht.size());
    for (int i=0; i<items; i++) keys[i] = e.nextElement();
  }

  private void parseKV() {
    KeyVector kv = (KeyVector)obj;
    items = Math.min(maxitems,kv.size());
    for (int i=0; i<items; i++) keys[i] = kv.getKey(i);
  }

  private Method findSetMethod (String key, int i, Method[] methods) {
    Class<?> ret = methods[i].getReturnType();
    String setName = "set"+key;
    for (int j=0; j<methods.length; j++) {
      if (!setName.equals(methods[j].getName())) continue;
      Class<?>[] args = methods[j].getParameterTypes();
      if (args.length==1 && ret.equals(args[0])) return methods[j];
    }
    return null;
  }

  private Method findSetStringMethod (String key, int i, Method[] methods) {
    Class<?> ret = key.getClass();
    String setName = "set"+key;
    for (int j=0; j<methods.length; j++) {
      if (!setName.equals(methods[j].getName())) continue;
      Class<?>[] args = methods[j].getParameterTypes();
      if (args.length==1 && ret.equals(args[0])) return methods[j];
    }
    return null;
  }

  /** evaluate Objects to get their values */
  public void evaluate() {
    for (int i=0; i<items; i++) {
      String value = "";
      Object object = getObject(i);
      if (object instanceof Hashtable)
        value = "Hashtable of "+((Hashtable<?,?>)object).size()+" entries";
      else if (object instanceof Table)
        value = "Table of "+((Table)object).size()+" entries";
      else if (object instanceof KeyVector)
        value = "KeyVector of "+((KeyVector)object).size()+" entries";
      else if (object!=null)
        value = object.toString();
      vals[i] = value;
    }
  }

  /** get number of items
   *
   *  @return number of items
   */
  public int    getItems () { return items; }
  /** get a name
   *  @param i one-based index of value to select
   *  @return name
   */
  public String getName (int i) { return keys[i]; }
  /** get a value
   *  @param i one-based index of value to select
   *  @return value
   */
  public String getValue (int i) { return vals[i]; }
  /** get a get method by index
   *  @param i one-based index of get method to select
   *  @return getter method at that index
   */
  public Method getGetMethod (int i) { return gets[i]; }
  /** get a set method by index
   *  @param i one-based index of set method to select
   *  @return setter method at that index
   */
  public Method getSetMethod (int i) { return sets[i]; }
  /** get a set String method by index
   *  @param i one-based index of set String method to select
   *  @return set String method at that index
   */
  public Method getSetStringMethod (int i) { return setS[i]; }

  /** get Object by key
   *  @param key key
   *  @return Object matching the key
   */
  public Object getObject (String key) {
    for (int i=0; i<items; i++) {
      if (key.equalsIgnoreCase(keys[i])) return getObject(i);
    }
    return null;
  }

  /** get Object by index
   *
   *  @param i one-based index
   *  @return Object at that index
   */
  public Object getObject (int i) {
    Object object=null;
    try {
      String key = keys[i];
           if (obj instanceof Table) object = ((Table)obj).getKey(key);
      else if (obj instanceof KeyVector) object = ((KeyVector)obj).get(i);
      else if (obj instanceof Hashtable) object = ((Hashtable<?,?>)obj).get(key);
      else if (gets[i]!=null) object = gets[i].invoke(obj,(Object[])null);
      else if (obj instanceof Keyable) object = ((Keyable)obj).getKey(key);
      else object = null;
    }
    catch (Exception e) { }
    return object;
  }

  /** get Object on which to introspect
   *
   *  @return Object on which to introspect
   */
  public Object getObject() { return obj; }

  /** set Object by key
   *
   *  @param key   key to associate with object
   *  @param value value of oject
   */
  public void setObject (String key, Object value) {
    for (int i=0; i<items; i++) {
      if (key.equalsIgnoreCase(keys[i])) setObject(i,value);
    }
  }

  /** set Object by index
   *
   * @param i one-based index
   * @param value value of object
   */
  public void setObject (int i, Object value) {
    try {
      Object object = getObject(i); // to determine the type
           if (object instanceof String) value = Convert.o2s(value);
      else if (object instanceof Double) value = Double.valueOf(Convert.o2d(value));
      else if (object instanceof Float) value = Float.valueOf(Convert.o2f(value));
      else if (object instanceof Integer) value = Integer.valueOf(Convert.o2l(value));
      else if (object instanceof Short) value = Short.valueOf(Convert.o2i(value));
      else if (object instanceof Byte) value = Byte.valueOf(Convert.o2b(value));
      if (sets[i]!=null) {
        Object[] args = new Object[1];
        args[0] = value;
        if (value instanceof String && setS[i]!=null)
          setS[i].invoke(obj,args);
        else
          sets[i].invoke(obj,args);
      }
      else if (obj instanceof Keyable) {
        ((Keyable)obj).setKey(keys[i],value);
      }
    }
    catch (Exception e) { }
  }

}
