package org.opengroupware.jope.weextensions;

import java.util.ArrayList;
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.appserver.WOActionResults;
import org.opengroupware.jope.appserver.WOContext;
import org.opengroupware.jope.appserver.WORequest;
import org.opengroupware.jope.eoaccess.EODatabase;
import org.opengroupware.jope.eoaccess.EOEntity;
import org.opengroupware.jope.eocontrol.EOAndQualifier;
import org.opengroupware.jope.eocontrol.EODataSource;
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;

/*
 * DisplayGroup
 * 
 * Small controller object which manages database fetches, especially
 * display ranges.
 */
public class WEDatabaseDisplayGroup extends NSObject {
  protected final Log log = LogFactory.getLog("DisplayGroup");

  protected EODataSource     dataSource;
  protected EOQualifier      qualifier;
  protected EOSortOrdering[] sortOrderings;

  /* fetch results */
  protected Integer      count;
  protected List         allObjects;
  protected List         displayedObjects;
  
  /* batching */
  protected int          numberOfObjectsPerBatch;
  protected int          currentBatchIndex; // Note: starts at 1
  
  /* query parameters */
  protected String queryParameterNamePrefix    = "dg_";
  protected String indexQueryParameterName     = "batchindex";
  protected String batchSizeQueryParameterName = "batchsize";
  protected String orderKeyQueryParameterName  = "sort";
  protected String orderOpQueryParameterName   = "sortdir";
  
  /* query dictionary */
  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 WEDatabaseDisplayGroup(EODataSource _ds) {
    this.dataSource        = _ds;
    this.currentBatchIndex = 1;
  }
  
  public WEDatabaseDisplayGroup
    (EODatabase _db, String _entityName, String _fspec)
  {
    if (_db != null) {
      EOEntity entity = _db.entityNamed(_entityName);
      if (entity != null) {
        this.dataSource = _db.dataSourceForEntity(entity);
        
        EOFetchSpecification fs = entity.fetchSpecificationNamed(_fspec);
        this.dataSource.setFetchSpecification(fs);
      }
    }
  }

  /* accessors */
  
  public void setQualifier(EOQualifier _q) {
    this.qualifier = _q;
  }
  public EOQualifier qualifier() {
    return this.qualifier;
  }
  
  public void setNumberOfObjectsPerBatch(int _value) {
    if (_value == this.numberOfObjectsPerBatch)
      return;
    
    this.numberOfObjectsPerBatch = _value;
    this.displayedObjects = null; /* needs a recalculation */
  }
  public int numberOfObjectsPerBatch() {
    return this.numberOfObjectsPerBatch;
  }
  
  public void setCurrentBatchIndex(int _idx) {
    this.currentBatchIndex = _idx;
    this.displayedObjects = null; /* needs a recalculation */
  }
  public int currentBatchIndex() {
    return this.currentBatchIndex;
  }
  
  /* derived accessors */
  
  public int indexOfFirstDisplayedObject() {
    if (this.currentBatchIndex < 1) {
      this.log.warn("invalid batch index: " + this.currentBatchIndex);
      return 0;
    }
    if (this.numberOfObjectsPerBatch < 1)
      return 0;
    
    return (this.currentBatchIndex - 1) * this.numberOfObjectsPerBatch;
  }
  
  public int indexOfLastDisplayedObject() {
    int idx = (this.currentBatchIndex - 1) * this.numberOfObjectsPerBatch;
    idx += this.numberOfObjectsPerBatch;
    
    int size = this.count();
    if (size == 0) return -1;
    if (idx > size) idx = size; /* last batch can be smaller */
    return idx - 1;
  }
  
  public int indexOfFirstDisplayedObjectPlusOne() {
    return this.indexOfFirstDisplayedObject() + 1;
  }
  public int indexOfLastDisplayedObjectPlusOne() {
    return this.indexOfLastDisplayedObject() + 1;
  }
  
  public int batchCount() {
    int size = this.count();
    if (size < 1) return 0;
    if (this.numberOfObjectsPerBatch < 1) return 1;
    if (size < this.numberOfObjectsPerBatch) return 1;
    return (size / this.numberOfObjectsPerBatch) + 
           (size % this.numberOfObjectsPerBatch > 0 ? 1 : 0);
  }
  public boolean hasMultipleBatches() {
    return this.batchCount() > 1;
  }
  
  public boolean isFirstBatch() {
    return this.currentBatchIndex < 2;
  }
  public boolean isLastBatch() {
    return this.currentBatchIndex >= this.batchCount();
  }
  public boolean isInnerBatch() {
    return this.currentBatchIndex > 1 && !this.isLastBatch();
  }
  
  /* display group fetch specification */
  
  protected EOFetchSpecification fetchSpecificationForFetch() {
    /* Note: this MUST return a copy of the fetchspec */
    EOFetchSpecification fs = this.dataSource.fetchSpecification();
    
    EOQualifier qv = this.qualifierFromQueryValues();
    EOQualifier q;
    
    if (qv == null)
      q = this.qualifier;
    else if (this.qualifier == null)
      q = qv;
    else
      q = new EOAndQualifier(this.qualifier, qv);
    
    if (fs == null) {
      fs = new EOFetchSpecification(null, q, this.sortOrderings);
    }
    else {
      fs = new EOFetchSpecification(fs);
      
      if (this.sortOrderings != null)
        fs.setSortOrderings(this.sortOrderings);
      
      qv = fs.qualifier();
      if (qv == null && q == null)
        fs.setQualifier(null);
      else if (q == null)
        fs.setQualifier(qv);
      else if (qv == null)
        fs.setQualifier(q);
      else
        fs.setQualifier(new EOAndQualifier(qv, q));
    }
    
    // TODO: apply qualifier bindings
    
    return fs;
  }

  protected EOFetchSpecification fetchSpecificationForDisplayFetch() {
    EOFetchSpecification fs = this.fetchSpecificationForFetch();
    
    /* apply offset/limit */
    
    if (this.currentBatchIndex > 1)
      fs.setFetchOffset(this.indexOfFirstDisplayedObject());
    
    if (this.numberOfObjectsPerBatch > 0)
      fs.setFetchLimit(this.numberOfObjectsPerBatch);
    return fs;
  }
  
  /* count */
  
  public int count() {
    if (this.count != null)
      return this.count;
    
    if (this.allObjects != null)
      return this.allObjects.size();
    
    this.count = this.fetchCount();
    return this.count;
  }
  public boolean hasNoEntries() {
    return this.count() == 0;
  }
  public boolean hasManyEntries() {
    return this.count() > 1;
  }
  public boolean hasOneEntry() {
    return this.count() == 1;
  }
  
  protected static String countPattern =
    "%(select)s COUNT(*) FROM %(tables)s %(where)s";
  
  public int fetchCount() {
    EOFetchSpecification fs = this.fetchSpecificationForFetch();
    fs.setHint("EOCustomQueryExpressionHintKey", countPattern);
    fs.setFetchesRawRows(true);
    
    EOFetchSpecification old = this.dataSource.fetchSpecification();
    this.dataSource.setFetchSpecification(fs);
    List rows = this.dataSource.fetchObjects();
    this.dataSource.setFetchSpecification(old);
    
    if (rows == null) {
      this.log.error("error fetching object count!",
                     this.dataSource.lastException());
      return -1;
    }
    if (rows.size() < 1) {
      this.log.error("fetch succeeded, but no object count was returned?!");
      return -1;
    }
    
    Map row = (Map)rows.get(0);
    return ((Number)(row.values().iterator().next())).intValue();
  }
  
  /* fetching objects */
  
  public List displayedObjects() {
    if (this.displayedObjects != null)
      return this.displayedObjects;
    
    if (this.numberOfObjectsPerBatch < 1) /* display all objects */
      return this.allObjects();
    
    if (this.log.isDebugEnabled())
      this.log.debug("fetch displayed, limit: " + this.numberOfObjectsPerBatch);
    
    if (this.allObjects != null) {
      int startIdx = this.indexOfFirstDisplayedObject();
      int endIdx   = this.indexOfLastDisplayedObject();
      int size     = this.allObjects.size();
      
      if (startIdx >= size || endIdx < startIdx) {
        this.log.info("got an out-of-range batch for displayed objects: " +
                      startIdx + "/" + endIdx + ", count " + size);
        return null;
      }
      
      this.displayedObjects = this.allObjects.subList(startIdx, endIdx + 1);
    }
    else {
      EOFetchSpecification fs  = this.fetchSpecificationForDisplayFetch();
      EOFetchSpecification old = this.dataSource.fetchSpecification();
      this.dataSource.setFetchSpecification(fs);
      this.displayedObjects = this.dataSource.fetchObjects();
      this.dataSource.setFetchSpecification(old);
  
      if (this.displayedObjects == null) {
        this.log.error("error fetching display objects!",
                       this.dataSource.lastException());
      }
    }

    if (this.log.isDebugEnabled())
      this.log.debug("fetched displayed objects: " + this.displayedObjects);
    
    return this.displayedObjects;
  }
  
  public List allObjects() {
    if (this.allObjects != null)
      return this.allObjects;

    EOFetchSpecification fs  = this.fetchSpecificationForFetch();
    EOFetchSpecification old = this.dataSource.fetchSpecification();
    this.dataSource.setFetchSpecification(fs);
    this.allObjects = this.dataSource.fetchObjects();
    this.dataSource.setFetchSpecification(old);

    if (this.allObjects == null) {
      this.log.error("error fetching all objects!",
                     this.dataSource.lastException());
    }
    return this.allObjects;
  }
  
  /* selection */
  
  public void clearSelection () {
  }
  
  /* processing query parameters */
  
  public boolean isAttributeAllowedForSorting(String _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 (lBatchIndex > 0) this.setCurrentBatchIndex(lBatchIndex);
    if (lBatchSize  > 0) this.setNumberOfObjectsPerBatch(lBatchSize);
    
    String sOrderings = 
      _rq.stringFormValueForKey(prefix + this.orderKeyQueryParameterName);
    if (sOrderings != null && sOrderings.length() > 0) {
      String[] ops = sOrderings.split(",");
      String   dir =
        _rq.stringFormValueForKey(prefix + this.orderOpQueryParameterName);
      
      this.sortOrderings = new EOSortOrdering[ops.length];
      for (int i = 0; i < ops.length; i++) {
        String key = ops[i];
        Object sel = EOSortOrdering.EOCompareAscending;
        
        if (!this.isAttributeAllowedForSorting(key))
          continue;
        
        if ("A".equals(dir))
          sel = EOSortOrdering.EOCompareAscending;
        else if ("D".equals(dir))
          sel = EOSortOrdering.EOCompareDescending;
        else if ("AI".equals(dir))
          sel = EOSortOrdering.EOCompareCaseInsensitiveAscending;
        else if ("DI".equals(dir))
          sel = EOSortOrdering.EOCompareCaseInsensitiveDescending;
        else
          sel = dir;
                
        this.sortOrderings[i] = new EOSortOrdering(key, sel);
      }
      
      // TODO: maybe we should compact empty cells in the array
    }
    
    // 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 */
    
    if (this.sortOrderings != null && this.sortOrderings.length > 0) {
      StringBuffer sb = new StringBuffer(128);
      String orderOp = null;
      
      for (int i = 0; i < this.sortOrderings.length; i++) {
        if (i != 0) sb.append(",");
        sb.append(this.sortOrderings[i].key());
        
        if (i == 0) {
          Object sel = this.sortOrderings[i].selector();
          if (sel != null && sel != EOSortOrdering.EOCompareAscending)
            orderOp = this.opKeyForSortOrdering(this.sortOrderings[i]);
        }
      }
      _qd.put(prefix + this.orderKeyQueryParameterName, sb.toString());
      if (orderOp != null)
        _qd.put(prefix + this.orderOpQueryParameterName, orderOp);
    }
    
    /* add qualifier if we have one ... */
    // TODO
  }
  
  /* sorting */
  
  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 sortDirectionForKey(String _key) {
    if (_key == null)
      return null;
    if (this.sortOrderings == null || this.sortOrderings.length < 1)
      return null;
    
    if (!_key.equals(this.sortOrderings[0].key()))
      return null;

    String op = this.opKeyForSortOrdering(this.sortOrderings[0]);
    return op != null ? op : "A";
  }

  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;
  }
  
  /* actions */
  
  public int nextBatchIndex() {
    return (this.isLastBatch() ? 1 : this.currentBatchIndex + 1);
  }
  public int previousBatchIndex() {
    return (this.isFirstBatch() ? this.batchCount() : this.currentBatchIndex-1);
  }
  
  public WOActionResults displayNextBatch() {
    this.clearSelection();
    this.setCurrentBatchIndex(this.nextBatchIndex());
    return null;
  }
  
  public WOActionResults displayPreviousBatch() {
    this.clearSelection();
    this.setCurrentBatchIndex(this.previousBatchIndex());
    return null;
  }
  
  
  /* query dictionaries */
  
  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;
        Object opv = opsMap.get(key);
        String op  = opv != null ? opv.toString() : null;
        
        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 = NSKeyValueStringFormatter.format
//              (fmt, new Object[] { value });
            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;
  }

  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    _d.append(" batch=" + this.currentBatchIndex + "/" + 
              this.numberOfObjectsPerBatch);
    
    if (this.count != null)
      _d.append(" has-count=" + this.count);
    
    if (this.allObjects != null)
      _d.append(" has-all=#" + this.allObjects.size());
    if (this.displayedObjects != null)
      _d.append(" has-displayed=#" + this.displayedObjects.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);
  }
}
