package org.opengroupware.jope.appserver;

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

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opengroupware.jope.eoaccess.EODatabaseDataSource;
import org.opengroupware.jope.eocontrol.EOAndQualifier;
import org.opengroupware.jope.eocontrol.EODataSource;
import org.opengroupware.jope.eocontrol.EODetailDataSource;
import org.opengroupware.jope.eocontrol.EOFetchSpecification;
import org.opengroupware.jope.eocontrol.EOKeyValueQualifier;
import org.opengroupware.jope.eocontrol.EOQualifier;
import org.opengroupware.jope.eocontrol.EOSortOrdering;
import org.opengroupware.jope.eocontrol.EOQualifier.ComparisonOperation;
import org.opengroupware.jope.foundation.NSJavaRuntime;
import org.opengroupware.jope.foundation.NSObject;

/*
 * WODisplayGroup
 * 
 * TODO: document
 */
public class WODisplayGroup extends NSObject {
  protected final static Log log = LogFactory.getLog("WODisplayGroup");

  protected EODataSource        dataSource;
  protected EOQualifier         qualifier;
  protected EOSortOrdering[]    sortOrderings;
  
  protected Map<String, Object> insertedObjectDefaultValues;
  
  protected int                 numberOfObjectsPerBatch;
  protected int                 currentBatchIndex;
  protected List<Integer>       selectionIndexes;
  protected boolean             fetchesOnLoad;
  protected boolean             selectsFirstObjectAfterFetch;
  protected boolean             validatesChangesImmediatly;
  protected boolean             inQueryMode;
  
  protected List<Object>        objects;
  protected List<Object>        displayObjects;
  
  protected Map<String, Object> queryBindings;
  protected Map<String, Object> queryMatch;
  protected Map<String, Object> queryMin;
  protected Map<String, Object> queryMax;
  protected Map<String, Object> queryOperator;
  protected String              defaultStringMatchFormat;
  protected String              defaultStringMatchOperator;
  protected static final String globalDefaultStringMatchFormat = "%@*";
  protected static final String globalDefaultStringMatchOperator =
    "caseInsensitiveLike";
  
  /* query parameters */
  protected String queryParameterNamePrefix    = "dg_";
  protected String indexQueryParameterName     = "batchindex";
  protected String batchSizeQueryParameterName = "batchsize";
  protected String orderKeyQueryParameterName  = "sort";

  
  /* construction */
  
  public WODisplayGroup() {
    this.currentBatchIndex = 1;
  }
  
  /* accessors */
  
  public void setFetchesOnLoad(boolean _flag) {
    this.fetchesOnLoad = _flag;
  }
  public boolean fetchesOnLoad() {
    return this.fetchesOnLoad;
  }
  
  public void setInsertedObjectDefaultValues(Map<String, Object> _values) {
    this.insertedObjectDefaultValues = _values;
  }
  public Map<String, Object> insertedObjectDefaultValues() {
    return this.insertedObjectDefaultValues;
  }
  
  public void setNumberOfObjectsPerBatch(int _value) {
    if (_value == this.numberOfObjectsPerBatch)
      return;
    
    this.numberOfObjectsPerBatch = _value;
    this.displayObjects = null; /* needs a recalculation */
  }
  public int numberOfObjectsPerBatch() {
    return this.numberOfObjectsPerBatch;
  }
  
  public void setSelectsFirstObjectAfterFetch(boolean _flag) {
    this.selectsFirstObjectAfterFetch = _flag;
  }
  public boolean selectsFirstObjectAfterFetch() {
    return this.selectsFirstObjectAfterFetch;
  }
  
  public void setValidatesChangesImmediatly(boolean _flag) {
    this.validatesChangesImmediatly = _flag;
  }
  public boolean validatesChangesImmediatly() {
    return this.validatesChangesImmediatly;
  }
  
  public void setSortOrderings(EOSortOrdering[] _sos) {
    this.sortOrderings = _sos;
  }
  public EOSortOrdering[] sortOrderings() {
    return this.sortOrderings;
  }
  
  /* datasource */
  
  public void setDataSource(EODataSource _ds) {
    if (this.dataSource == _ds)
      return;
    
    if (this.dataSource != null) {
      // unregister with old editing context
    }
    
    this.dataSource = _ds;
    
    if (this.dataSource != null) {
      // register with new editing context
    }
    
    /* reset state */
    this.objects        = null;
    this.displayObjects = null;
  }
  public EODataSource dataSource() {
    return this.dataSource;
  }
  
  /* batches */
  
  public boolean hasMultipleBatches() {
    return this.batchCount() > 1;
  }
  
  public int batchCount() {
    List<Object> objs = this.allObjects();
    if (objs == null) return 0;
    
    int doc = objs.size();
    int nob = this.numberOfObjectsPerBatch();
    return (nob == 0) ? 1 : (doc / nob + ((doc % nob) != 0 ? 1 : 0));
  }
  
  public void setCurrentBatchIndex(int _idx) {
    if (_idx == this.currentBatchIndex) /* same batch */
      return;
    
    if (_idx > this.batchCount()) {
      log.info("batch index was bigger than the count: " + _idx + " vs " + 
               this.batchCount());
      
      /* We do not adjust the index, because the fetch might happen after the
       * index was set.
       */
      // Don't: _idx = 1;
    }
    
    this.currentBatchIndex = _idx;
    this.displayObjects = null; /* needs a recalculation */
  }
  public int currentBatchIndex() {
    // Don't: we might be asked before a fetch (which manages the count ...)
    // if (this.currentBatchIndex > this.batchCount())
    //   this.currentBatchIndex = 1;
    return this.currentBatchIndex;
  }

  public boolean isFirstBatch() { /* provide only 'next' buttons */
    return this.currentBatchIndex < 2;
  }
  public boolean isLastBatch() { /* provide only 'previous' buttons */
    return this.currentBatchIndex >= this.batchCount();
  }
  public boolean isInnerBatch() { /* provide 'next' and 'previous' buttons */
    return this.currentBatchIndex > 1 && !this.isLastBatch();
  }
  
  public int nextBatchIndex() {
    return (this.isLastBatch() ? 1 : this.currentBatchIndex + 1);
  }
  public int previousBatchIndex() {
    return (this.isFirstBatch() ? this.batchCount() : this.currentBatchIndex-1);
  }
  
  /* displayed objects */
  
  public int indexOfFirstDisplayedObject() {
    if (this.currentBatchIndex < 1) {
      log.warn("invalid batch index: " + this.currentBatchIndex);
      return 0;
    }
    if (this.numberOfObjectsPerBatch < 1)
      return 0;
    
    return (this.currentBatchIndex - 1) * this.numberOfObjectsPerBatch;
  }
  
  public int indexOfLastDisplayedObject() {
    List<Object> objs = this.allObjects();
    int doc = objs != null ? objs.size() : 0;
    int nob = this.numberOfObjectsPerBatch();
    
    if (nob == 0)
      return doc - 1;
    
    int fdo = this.indexOfFirstDisplayedObject();
    if ((fdo + nob) < doc)
      return (fdo + nob - 1);
    
    return (doc - 1);
  }

  public int indexOfFirstDisplayedObjectPlusOne() { /* useful for output */
    return this.indexOfFirstDisplayedObject() + 1;
  }
  public int indexOfLastDisplayedObjectPlusOne() { /* useful for output */
    return this.indexOfLastDisplayedObject() + 1;
  }
  
  public WOActionResults displayNextBatch() {
    this.clearSelection();
    
    this.currentBatchIndex++;
    if (this.currentBatchIndex > this.batchCount())
      this.currentBatchIndex = 1;
    
    this.updateDisplayedObjects();    
    return null; /* stay on page */
  }
  
  public WOActionResults displayPreviousPatch() {
    this.clearSelection();
    
    this.currentBatchIndex--;
    if (this.currentBatchIndex() <= 0)
      this.currentBatchIndex = this.batchCount();
    
    this.updateDisplayedObjects();    
    return null; /* stay on page */
  }
  
  public WOActionResults displayBatchContainingSelectedObject() {
    // TODO: implement me
    log.error("not implemented: displayBatchContainingSelectedObject");
    
    this.updateDisplayedObjects();    
    return null; /* stay on page */
  }
  
  /* selection */
  
  public boolean setSelectionIndexes(List<Integer> _selection) {
    // only required for delegate:
    // Set before = this.selectionIndexes != null
    //  ? new HashSet(this.selectionIndexes) : new HashSet();
    // Set after = _selection != null ? new HashSet(_selection) : new HashSet();
    
    this.selectionIndexes = _selection;
    return true;
  }
  public List selectionIndexes() {
    return this.selectionIndexes;
  }
  
  protected static final List<Integer> emptyList = new ArrayList<Integer>(0);
  protected static final List<Integer> int0Array =
    Arrays.asList(new Integer[] { 0 });
  
  public void clearSelection() {
    this.setSelectionIndexes(emptyList);
  }
  
  public WOActionResults selectNext() {
    if (this.displayObjects == null || this.displayObjects.size() == 0)
      return null;
    
    if (this.selectionIndexes == null || this.selectionIndexes.size() == 0) {
      this.setSelectionIndexes(int0Array);
      return null;
    }
    
    int idx = this.selectionIndexes.get(this.selectionIndexes.size() - 1);
    if (idx >= (this.displayObjects.size() - 1)) {
      /* last object is already selected, select first one */
      this.setSelectionIndexes(int0Array);
      return null;
    }
    
    /* select next object */
    List<Integer> list = new ArrayList<Integer>(1);
    list.add(idx + 1);
    this.setSelectionIndexes(list);
    return null;
  }
  
  public WOActionResults selectPrevious() {
    if (this.displayObjects == null || this.displayObjects.size() == 0)
      return null;
    
    if (this.selectionIndexes == null || this.selectionIndexes.size() == 0) {
      this.setSelectionIndexes(int0Array);
      return null;
    }
    
    List<Integer> list = new ArrayList<Integer>(1);
    int idx = this.selectionIndexes.get(this.selectionIndexes.size() - 1);
    
    if (idx <= 0) {
      /* first object is selected, now select last one */
      list.add(this.displayObjects.size() - 1);
    }
    else {
      /* select previous object .. */
      list.add(idx - 1);
    }
    
    this.setSelectionIndexes(list);
    return null;
  }
  
  public void setSelectedObject(Object _obj) {
    // TODO: implement me
    log.error("setSelectedObject is not implemented");
  }
  
  public Object selectedObject() {
    if (this.objects == null)
      return null;
    if (this.selectionIndexes == null || this.selectionIndexes.size() == 0)
      return null;
    
    int idx = this.selectionIndexes.get(0);
    if (idx >= this.objects.size())
      return null;
    
    // TODO: need to ensure that selection is in displayedObjects?
    return this.objects.get(idx);
  }
  
  public void setSelectedObjects(List<Object> _objs) {
    // TODO: implement me
    log.error("setSelectedObjects is not implemented");
  }
  
  public List<Object> selectedObjects() {
    if (this.objects == null)
      return null;
    if (this.selectionIndexes == null || this.selectionIndexes.size() == 0)
      return null;

    int sCount = this.selectionIndexes.size();
    int oCount = this.objects.size();
    
    List<Object> result = new ArrayList<Object>(sCount);
    for (int i = 0; i < sCount; i++) {
      int idx = this.selectionIndexes.get(i);
      if (idx < oCount)
        result.add(this.objects.get(idx));
    }
    return result;
  }
  
  public boolean selectObject(Object _object) {
    /* returns true if displayedObjects contains _obj, otherwise false */
    if (_object == null)
      return false;
    
    int idx = this.objects.indexOf(_object);
    if (idx != -1) {
      List<Integer> list = new ArrayList<Integer>(1);
      list.add(idx);
      this.setSelectionIndexes(list);
    }
    else
      this.setSelectionIndexes(emptyList);
    return true;
  }
  
  public boolean selectObjectsIdenticalTo(List<Object> _objs) {
    // return true if t least one obj matches
    // TODO: implement me
    log.error("selectObjectsIdenticalTo is not implemented");
    return false;
  }
  public boolean selectObjectsIdenticalTo
    (List<Object> _objs, boolean _firstOnMiss)
  {
    if (this.selectObjectsIdenticalTo(_objs))
      return true;
    
    if (!_firstOnMiss)
      return false;
    
    if (this.displayObjects == null || this.displayObjects.size() == 0)
      return this.selectObject(null);
    
    return this.selectObject(this.displayObjects.get(0));
  }
  
  /* objects */
  
  public void setObjectArray(List<Object> _objects) {
    if (this.objects == _objects)
      return;
    
    this.objects = _objects;
    
    this.clearSelection();
    if (this.objects != null && this.objects.size() > 0) {
      if (this.selectsFirstObjectAfterFetch())
        this.setSelectionIndexes(int0Array);
    }
  }
  
  public List<Object> allObjects() {
    return this.objects;
  }
  
  public List<Object> displayedObjects() {
    return this.displayObjects;
  }
  
  @SuppressWarnings("unchecked")
  public WOActionResults fetch() {
    List<Object> objs = null;
    EODataSource ds = this.dataSource();
    if (ds != null) objs = ds.fetchObjects();
    
    this.setObjectArray(objs);
    this.updateDisplayedObjects();
    
    if (this.selectsFirstObjectAfterFetch()) {
      this.clearSelection();
      
      if (objs != null && objs.size() > 0)
        this.setSelectedObject(objs.get(0));
    }
    
    return null; /* stay on page */
  }
  
  public void updateDisplayedObjects() {
    if (this.numberOfObjectsPerBatch < 1) { /* display all objects */
      this.displayObjects = this.objects;
      return;
    }
    
    int startIdx = this.indexOfFirstDisplayedObject();
    int endIdx   = this.indexOfLastDisplayedObject();
    int size     = this.objects != null ? this.objects.size() : 0;
    
    if (startIdx >= size || endIdx < startIdx) {
      log.info("got an out-of-range batch for displayed objects: " +
               startIdx + "/" + endIdx + ", count " + size);
      return;
    }
    
    // TODO: implement me
    this.displayObjects = this.objects != null
      ? this.objects.subList(startIdx, endIdx + 1)
      : null;
  }
  
  /* query */
  
  public void setInQueryMode(boolean _flag) {
    this.inQueryMode = _flag;
  }
  public boolean inQueryMode() {
    return this.inQueryMode;
  }
  
  public EOQualifier qualifierFromQueryValues() {
    List<EOQualifier> quals = new ArrayList<EOQualifier>(4);
    
    /* construct qualifier for all query-match entries */
    
    if (this.queryMatch != null) {
      Map<String, Object> opsMap = this.queryOperator();
      
      for (String key: this.queryMatch.keySet()) {
        Object value = this.queryMatch.get(key);
        
        /* determine operator */
        
        ComparisonOperation ops;
        String op = opsMap.get(key).toString();
        
        if (op == null) {
          /* default operator is equality */
          op  = "=";
          ops = ComparisonOperation.EQUAL_TO;
        }
        else if (value instanceof String) {
          op = this.defaultStringMatchOperator();
          ops = EOQualifier.operationForString(op);
          
          if (ops == ComparisonOperation.CASE_INSENSITIVE_LIKE ||
              ops == ComparisonOperation.LIKE) {
            String fmt = this.defaultStringMatchFormat();
            if (fmt == null) fmt = globalDefaultStringMatchFormat;
            value = fmt.replace("%@", value.toString());
          }
        }
        else {
          ops = EOQualifier.operationForString(op);
        }
        
        /* add qualifier */
        
        quals.add(new EOKeyValueQualifier(key, op, value));
      }
    }
    
    /* construct min qualifiers */
    
    if (this.queryMin != null) {
      for (String key: this.queryMin.keySet()) {
        Object value = this.queryMin.get(key);
        quals.add(new EOKeyValueQualifier
            (key, EOQualifier.ComparisonOperation.GREATER_THAN, value));
      }
    }

    /* construct max qualifiers */
    
    if (this.queryMax != null) {
      for (String key: this.queryMax.keySet()) {
        Object value = this.queryMax.get(key);
        quals.add(new EOKeyValueQualifier
            (key, EOQualifier.ComparisonOperation.LESS_THAN, value));
      }
    }
    
    /* conjoin qualifiers */
    
    if (quals.size() == 0)
      return null;
    
    if (quals.size() == 1)
      return quals.get(0);
    
    return new EOAndQualifier(quals);
  }
  
  public Map<String, Object> queryBindings() {
    if (this.queryBindings == null)
      this.queryBindings = new HashMap<String, Object>(8);
    return this.queryBindings;
  }
  public Map<String, Object> queryMatch() {
    if (this.queryMatch == null)
      this.queryMatch = new HashMap<String, Object>(8);
    return this.queryMatch;
  }
  public Map<String, Object> queryMin() {
    if (this.queryMin == null)
      this.queryMin = new HashMap<String, Object>(2);
    return this.queryMin;
  }
  public Map<String, Object> queryMax() {
    if (this.queryMax == null)
      this.queryMax = new HashMap<String, Object>(2);
    return this.queryMax;
  }
  public Map<String, Object> queryOperator() {
    if (this.queryOperator == null)
      this.queryOperator = new HashMap<String, Object>(8);
    return this.queryOperator;
  }

  public void setDefaultStringMatchFormat(String _value) {
    this.defaultStringMatchFormat = _value;
  }
  public String defaultStringMatchFormat() {
    return this.defaultStringMatchFormat;
  }

  public void setDefaultStringMatchOperator(String _value) {
    this.defaultStringMatchOperator = _value;
  }
  public String defaultStringMatchOperator() {
    return this.defaultStringMatchOperator;
  }
  
  /* qualifiers */
  
  public void setQualifier(EOQualifier _q) {
    this.qualifier = _q;
  }
  public EOQualifier qualifier() {
    return this.qualifier;
  }
  
  // TODO: allQualifierOperators
  // TODO: stringQualifierOperators
  // TODO: relationalQualifierOperators
  
  public void qualifyDisplayGroup() {
    EOQualifier q = this.qualifierFromQueryValues();
    if (q != null)
      this.setQualifier(q);
    
    this.updateDisplayedObjects();
    
    if (this.inQueryMode())
      this.setInQueryMode(false);
  }
  
  public void qualifyDataSource() {
    /* This method does filtering/sorting in the datasource while
     * qualifyDisplayGroups() just does it in-memory.
     */
    EODataSource ds = this.dataSource();
    if (ds == null) {
      log.warn("no datasource set.");
      return;
    }
    
    /* build qualifier */
    
    EOQualifier q = this.qualifierFromQueryValues();
    if (q != null)
      this.setQualifier(q);

    Map<String, Object> bindings = this.queryBindings();
    if (bindings != null && bindings.size() == 0) bindings = null;
    
    /* set qualifier in datasource */
    
    // TODO: do something better here ...
    if (ds instanceof EODatabaseDataSource) {
      EODatabaseDataSource dbds = (EODatabaseDataSource)ds;
      dbds.setAuxiliaryQualifier(q);
      dbds.setQualifierBindings(bindings);
      
      if (this.sortOrderings != null) {
        EOFetchSpecification fs = dbds.fetchSpecification();
        if (fs != null) {
          fs = new EOFetchSpecification(fs);
          fs.setSortOrderings(this.sortOrderings);
        }
        else {
          fs = new EOFetchSpecification();
          fs.setSortOrderings(this.sortOrderings);
        }
        
        dbds.setFetchSpecification(fs);
      }
    }
    else {
      EOFetchSpecification fs = ds.fetchSpecification();
      fs = fs != null 
        ? new EOFetchSpecification(fs)
        : new EOFetchSpecification();
      
      fs.setQualifier(q);
      fs.setSortOrderings(this.sortOrderings);
      
      /* apply */
      ds.setFetchSpecification(fs);
    }
    
    /* perform fetch */
    
    this.fetch();
    
    if (this.inQueryMode())
      this.setInQueryMode(false);
  }
  
  /* object creation */
  
  public WOActionResults insert() {
    int idx;
    
    if (this.selectionIndexes != null && this.selectionIndexes.size() > 0)
      idx = this.selectionIndexes.get(0) + 1;
    else
      idx = this.objects != null ? this.objects.size() : 0;
      
    return this.insertObjectAtIndex(idx);
  }
  
  public WOActionResults insertObjectAtIndex(int _idx) {
    EODataSource ds = this.dataSource();
    if (ds == null) {
      log.warn("no datasource set for object insert.");
      return null;
    }
    
    Object newObject = ds.createObject();
    if (newObject == null) {
      // TODO: report some error (using delegate?)
      return null;
    }
    
    /* apply default values */
    
    // TODO: add KVC helper?
    // TODO: takeValuesFromDictionary(this.insertedObjectDefaultValues())
    
    /* insert */
    
    this.insertObjectAtIndex(newObject, _idx);
    
    return null /* stay on page */;
  }
  
  public void insertObjectAtIndex(Object _o, int _idx) {
    /* insert in datasource */
    
    EODataSource ds = this.dataSource();
    if (ds != null) {
      // TODO: error handling?
      ds.insertObject(_o);
    }
    
    /* update object-array (Note: ignores qualifier for new objects!) */
    
    if (this.objects == null)
      this.objects = new ArrayList<Object>(1);
    
    if (_idx <= this.objects.size())
      this.objects.set(_idx, _o); // TODO: is this correct? (does it _insert_?)
    else
      this.objects.add(_o);
    
    this.updateDisplayedObjects();
    
    /* select object */
    
    this.selectObject(_o);
  }
  
  /* object deletion */
  
  public WOActionResults delete() {
    this.deleteSelection();
    return null;
  }
  
  public boolean deleteSelection() {
    if (this.selectionIndexes == null || this.selectionIndexes.size() == 0)
      return true;
    if (this.objects == null || this.objects.size() == 0)
      return false;
    
    List<Object> objsToDelete = new ArrayList<Object>(this.selectedObjects());
    for (int i = 0; i < objsToDelete.size(); i++) {
      int idx = this.objects.indexOf(objsToDelete.get(i));
      
      if (idx == -1) {
        log.error("did not find object in selection: " + 
                  objsToDelete.get(i));
        return false;
      }
      
      if (!this.deleteObjectAtIndex(idx))
        return false;
    }
    return true;
  }
  
  public boolean deleteObjectAtIndex(int _idx) {
    if (this.objects == null || this.objects.size() == 0)
      return false;
    if (_idx >= this.objects.size())
      return false;
    
    Object object = this.objects.get(_idx);
    
    /* delete in datasource */
    
    EODataSource ds = this.dataSource();
    if (ds != null)
      ds.deleteObject(object);
    
    /* update array */
    
    this.objects.remove(_idx);
    this.updateDisplayedObjects();
    
    return true;
  }
  
  /* master details */
  
  public boolean hasDetailDataSource() {
    EODataSource ds = this.dataSource();
    return ds != null ? (ds instanceof EODetailDataSource) : false;
  }
  
  public void setDetailKey(String _key) {
    EODataSource ds = this.dataSource();
    if (ds != null && (ds instanceof EODetailDataSource))
      ((EODetailDataSource)ds).setDetailKey(_key);
  }
  public String detailKey() {
    EODataSource ds = this.dataSource();
    return (ds != null && (ds instanceof EODetailDataSource))
      ? ((EODetailDataSource)ds).detailKey() : null;
  }
  
  public void setMasterObject(Object _v) {
    EODataSource ds = this.dataSource();
    if (ds == null) return;
    if (!(ds instanceof EODetailDataSource)) return;
    
    ds.qualifyWithRelationshipKey(this.detailKey(), _v);
  }
  public Object masterObject() {
    EODataSource ds = this.dataSource();
    return (ds != null && (ds instanceof EODetailDataSource))
      ? ((EODetailDataSource)ds).masterObject()
      : null;
  }
  
  /* key/value coding */
  
  public void takeValueForKeyPath(Object _value, String _keypath) {
    if (_keypath != null && _keypath.length() > 8 && _keypath.charAt(0)=='q') {
      if (_value != null) {
        if (_keypath.startsWith("queryMatch."))
          this.queryMatch().put(_keypath.substring(11), _value);
        else if (_keypath.startsWith("queryMax."))
          this.queryMax().put(_keypath.substring(9), _value);
        else if (_keypath.startsWith("queryMin."))
          this.queryMin().put(_keypath.substring(9), _value);
        else if (_keypath.startsWith("queryOperator."))
          this.queryOperator().put(_keypath.substring(14), _value);
      }
      else {
        if (_keypath.startsWith("queryMatch."))
          this.queryMatch().remove(_keypath.substring(11));
        else if (_keypath.startsWith("queryMax."))
          this.queryMax().remove(_keypath.substring(9));
        else if (_keypath.startsWith("queryMin."))
          this.queryMin().remove(_keypath.substring(9));
        else if (_keypath.startsWith("queryOperator."))
          this.queryOperator().remove(_keypath.substring(14));
      }
    }
    super.takeValueForKeyPath(_value, _keypath);
  }
  public Object valueForKeyPath(String _keypath) {
    if (_keypath != null && _keypath.length() > 8 && _keypath.charAt(0)=='q') {
      if (_keypath.startsWith("queryMatch."))
        return this.queryMatch().get(_keypath.substring(11));
      if (_keypath.startsWith("queryMax."))
        return this.queryMax().get(_keypath.substring(9));
      if (_keypath.startsWith("queryMin."))
        return this.queryMin().get(_keypath.substring(9));
      if (_keypath.startsWith("queryOperator."))
        return this.queryOperator().get(_keypath.substring(14));
    }
    return super.valueForKeyPath(_keypath);
  }
  
  
  /* processing query parameters */
  
  public boolean isAttributeAllowedForSorting(EOSortOrdering _key) {
    // we might want to restrict the allowed attributes for sorting
    if (_key == null) return false;
    return true;
  }
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    String prefix = this.queryParameterNamePrefix;
    
    int lBatchIndex = NSJavaRuntime.intValueForObject
      (_rq.stringFormValueForKey(prefix + this.indexQueryParameterName));
    int lBatchSize  = NSJavaRuntime.intValueForObject
      (_rq.stringFormValueForKey(prefix + this.batchSizeQueryParameterName));
    
    if (lBatchSize  > 0) this.setNumberOfObjectsPerBatch(lBatchSize);
    if (lBatchIndex > 0) this.setCurrentBatchIndex(lBatchIndex);
    
    /* sort orderings */
    
    String sOrderings = 
      _rq.stringFormValueForKey(prefix + this.orderKeyQueryParameterName);
    this.sortOrderings = this.sortOrderingsFromQueryValue(sOrderings);
    
    // TODO: qualifier
  }
  
  public void appendStateToQueryDictionary(Map<String, Object> _qd) {
    if (_qd == null)
      return;
    
    String prefix = this.queryParameterNamePrefix;
    
    if (this.currentBatchIndex > 1)
      _qd.put(prefix + this.indexQueryParameterName, this.currentBatchIndex);
    
    if (this.numberOfObjectsPerBatch > 0) {
      _qd.put(prefix + this.batchSizeQueryParameterName,
              this.numberOfObjectsPerBatch);
    }
    
    /* add sort orderings */
    
    String sos = this.queryValueForSortOrderings();
    if (sos != null)
      _qd.put(prefix + this.orderKeyQueryParameterName, sos);
    
    /* add qualifier if we have one ... */
    // TODO
  }
  
  /* sorting */
  
  public EOSortOrdering sortOrderingFromQueryValue(String key) {
    Object sel;
    
    if (key.startsWith("-")) {
      /* support -name for descending sorts */
      sel = EOSortOrdering.EOCompareDescending;
      key = key.substring(1);
    }
    else if (key.endsWith("-D")) { /* eg name-D */
      sel = EOSortOrdering.EOCompareDescending;
      key = key.substring(0, key.length() - 2);
    }
    else if (key.endsWith("-DI")) { /* eg lastname-DI */
      sel = EOSortOrdering.EOCompareCaseInsensitiveDescending;
      key = key.substring(0, key.length() - 3);
    }
    else if (key.endsWith("-AI")) { /* eg lastname-AI */
      sel = EOSortOrdering.EOCompareCaseInsensitiveAscending;
      key = key.substring(0, key.length() - 3);
    }
    else if (key.endsWith("-A")) {
      sel = EOSortOrdering.EOCompareAscending;
      key = key.substring(0, key.length() - 3);
    }
    else
      sel = EOSortOrdering.EOCompareAscending;
    
    return new EOSortOrdering(key, sel);
  }
  
  public EOSortOrdering[] sortOrderingsFromQueryValue(String _s) {
    if (_s == null || _s.length() == 0) return null;

    String[] ops = _s.split(",");
      
    EOSortOrdering[] qSortOrderings = new EOSortOrdering[ops.length];
    for (int i = 0; i < ops.length; i++) {
      /* reconstruct */
      qSortOrderings[i] = this.sortOrderingFromQueryValue(ops[i]);

      /* check permissions */
      if (!this.isAttributeAllowedForSorting(qSortOrderings[i]))
        qSortOrderings[i] = null;
    }
    
    // TODO: we should compact empty cells in the array
    return qSortOrderings;
  }
  
  public String queryValueForSortOrderings() {
    if (this.sortOrderings == null || this.sortOrderings.length == 0)
      return null;
    
    StringBuffer sb = new StringBuffer(128);
    
    for (int i = 0; i < this.sortOrderings.length; i++) {
      if (i != 0) sb.append(",");
      sb.append(this.sortOrderings[i].key());
      
      Object sel = this.sortOrderings[i].selector();
      if (sel != null && sel != EOSortOrdering.EOCompareAscending) {
        String orderOp = this.opKeyForSortOrdering(this.sortOrderings[i]);
        sb.append("-");
        sb.append(orderOp);
      }
    }
    return sb.toString();
  }
  
  public String opKeyForSortOrdering(EOSortOrdering _so) {
    if (_so == null) return null;
    Object sel = _so.selector();
    if (sel == null)
      return null;
    
    if (sel == EOSortOrdering.EOCompareAscending)
      return "A";
    if (sel.equals(EOSortOrdering.EOCompareDescending))
      return "D";
    if (sel.equals(EOSortOrdering.EOCompareCaseInsensitiveDescending))
      return "DI";
    if (sel.equals(EOSortOrdering.EOCompareCaseInsensitiveAscending))
      return "AI";
    return sel.toString();
  }
  
  public String currentSortDirection() {
    if (this.sortOrderings == null || this.sortOrderings.length < 1)
      return "A";
    
    String op = this.opKeyForSortOrdering(this.sortOrderings[0]);
    return op != null ? op : "A";
  }
  public String nextSortDirection() {
    String cs = this.currentSortDirection();
    if (cs == null || cs.length() == 0)
      return "D";
    
    if ("A".equals(cs))  return "D";
    if ("D".equals(cs))  return "A";
    if ("AI".equals(cs)) return "DI";
    if ("DI".equals(cs)) return "AI";
    return cs;
  }

  /* query parameters */
  
  public String queryParameterNamePrefix() {
    return this.queryParameterNamePrefix;
  }
  public String indexQueryParameterName() {
    return this.indexQueryParameterName;
  }
  public String batchSizeQueryParameterName() {
    return this.batchSizeQueryParameterName;
  }
  public String orderKeyQueryParameterName() {
    return this.orderKeyQueryParameterName;
  }
  
  /* description */

  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    _d.append(" batch=" + this.currentBatchIndex + "/" + 
              this.numberOfObjectsPerBatch);
    
    if (this.objects != null)
      _d.append(" has-all=#" + this.objects.size());
    if (this.displayObjects != null)
      _d.append(" has-displayed=#" + this.displayObjects.size());
    
    if (this.dataSource != null)
      _d.append(" ds=" + this.dataSource);
    
    if (this.qualifier != null)
      _d.append(" q=" + this.qualifier);
    
    if (this.sortOrderings != null)
      _d.append(" so=" + this.sortOrderings);
  }
}
