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.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.NSObject;

/*
 * WODisplayGroup
 * 
 * TODO: document
 */
public class WODisplayGroup extends NSObject {
  protected final 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";
  
  
  /* construction */
  
  public WODisplayGroup() {
  }
  
  /* 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) {
    this.numberOfObjectsPerBatch = _value;
  }
  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;
  }
  
  /* 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
    }
  }
  public EODataSource dataSource() {
    return this.dataSource;
  }
  
  /* batches */
  
  public boolean hasMultipleBatches() {
    return this.batchCount() > 1;
  }
  
  public int batchCount() {
    List<Object> objs = this.allObjects();
    int doc = objs != null ? objs.size() : 0;
    int nob = this.numberOfObjectsPerBatch();
    return (nob == 0) ? 1 : (doc / nob + ((doc % nob) != 0 ? 1 : 0));
  }
  
  public void setCurrentBatchIndex(int _idx) {
    this.currentBatchIndex = (_idx <= this.batchCount()) ? _idx : 1;
  }
  public int currentBatchIndex() {
    if (this.currentBatchIndex > this.batchCount())
      this.currentBatchIndex = 1;
    return this.currentBatchIndex;
  }
  
  public int indexOfFirstDisplayedObject() {
    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 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
    
    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
    this.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
    this.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
    this.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() {
    // TODO: implement me
    this.log.error("updateDisplayedObjects is not implemented");
  }
  
  /* 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() {
    EODataSource ds = this.dataSource();
    if (ds == null) {
      this.log.warn("no datasource set.");
      return;
    }
    
    /* build qualifier */
    
    EOQualifier q = this.qualifierFromQueryValues();
    if (q != null)
      this.setQualifier(q);
    
    /* set qualifier in datasource */
    
    // TODO: do something better here ...
    if (ds instanceof EODatabaseDataSource)
      ((EODatabaseDataSource)ds).setAuxiliaryQualifier(q);
    else
      this.log.error("could not qualify datasource: " + ds);

    /* set bindings in datasource */
    
    Map<String, Object> bindings = this.queryBindings();
    if (bindings != null && bindings.size() > 0) {
      // TODO: do something better here ...
      if (ds instanceof EODatabaseDataSource)
        ((EODatabaseDataSource)ds).setQualifierBindings(bindings);
      else
        this.log.error("could not set bindings in datasource: " + ds);
    }
    
    /* 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) {
      this.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) {
        this.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);
  }
  
  /* description */

  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.dataSource != null) _d.append(" ds=" + this.dataSource);
  }
}
