package org.opengroupware.jope.weextensions;

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

import org.opengroupware.jope.appserver.WOActionResults;
import org.opengroupware.jope.appserver.WOContext;
import org.opengroupware.jope.appserver.WODisplayGroup;
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.EOQualifier.ComparisonOperation;
import org.opengroupware.jope.foundation.NSJavaRuntime;

/*
 * DisplayGroup
 * 
 * Small controller object which manages database fetches, especially
 * display ranges.
 */
public class WEDatabaseDisplayGroup extends WODisplayGroup {
  /* fetch results */
  protected Integer count;
  
  /* 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);
      }
    }
  }

  /* derived accessors */
  
  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 batchCount() {
    /* this differs to the superclass because it only needs the count */
    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);
  }
  
  /* 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.objects != null)
      return this.objects.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);
    // this does not work (yet):
    // fs.setFetchAttributeNames(new String[] { "COUNT(*)" });
    
    EOFetchSpecification old = this.dataSource.fetchSpecification();
    this.dataSource.setFetchSpecification(fs);
    List rows = this.dataSource.fetchObjects();
    this.dataSource.setFetchSpecification(old);
    
    if (rows == null) {
      log.error("error fetching object count!",
                     this.dataSource.lastException());
      return -1;
    }
    if (rows.size() < 1) {
      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 */
  
  @SuppressWarnings("unchecked")
  public List<Object> displayedObjects() {
    /* This differs to WODisplayGroup because it treats the count as a separate
     * data item.
     */
    if (this.displayObjects != null)
      return this.displayObjects;
    
    if (this.numberOfObjectsPerBatch < 1) /* display all objects */
      return this.allObjects();
    
    if (log.isDebugEnabled())
      log.debug("fetch displayed, limit: " + this.numberOfObjectsPerBatch);
    
    if (this.objects != null) {
      int startIdx = this.indexOfFirstDisplayedObject();
      int endIdx   = this.indexOfLastDisplayedObject();
      int size     = this.objects.size();
      
      if (startIdx >= size || endIdx < startIdx) {
        log.info("got an out-of-range batch for displayed objects: " +
                      startIdx + "/" + endIdx + ", count " + size);
        return null;
      }
      
      this.displayObjects = this.objects.subList(startIdx, endIdx + 1);
    }
    else {
      EOFetchSpecification fs  = this.fetchSpecificationForDisplayFetch();
      EOFetchSpecification old = this.dataSource.fetchSpecification();
      this.dataSource.setFetchSpecification(fs);
      this.displayObjects = this.dataSource.fetchObjects();
      this.dataSource.setFetchSpecification(old);
  
      if (this.displayObjects == null) {
        log.error("error fetching display objects!",
                       this.dataSource.lastException());
      }
    }

    if (log.isDebugEnabled())
      log.debug("fetched displayed objects: " + this.displayObjects);
    
    return this.displayObjects;
  }
  
  @SuppressWarnings("unchecked")
  public List<Object> allObjects() {
    if (this.objects != null)
      return this.objects;

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

    if (this.objects == null) {
      log.error("error fetching all objects!",
                     this.dataSource.lastException());
    }
    return this.objects;
  }
  
  /* processing query parameters */

  protected String countQueryParameterName   = "count";
  
  public void takeValuesFromRequest(WORequest _rq, WOContext _ctx) {
    /* Preserve object count so that we don't need to refetch it.
     * IMPORTANT: run first, other code might rely on it (and refetch if it
     *            isn't available
     */
    String cs = _rq.stringFormValueForKey
      (this.queryParameterNamePrefix + this.countQueryParameterName);
    if (cs != null)
      this.count = NSJavaRuntime.intValueForObject(cs);
    
    /* now call parent */
    super.takeValuesFromRequest(_rq, _ctx);
  }
  
  public void appendStateToQueryDictionary(Map<String, Object> _qd) {
    if (_qd == null)
      return;
    
    if (this.count != null) {
      _qd.put(this.queryParameterNamePrefix + this.countQueryParameterName,
              this.count);
    }
    
    /* now call parent */
    super.appendStateToQueryDictionary(_qd);
  }
  
  /* actions */
  
  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() {
    // TODO: check whether this is superflous
    
    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);
  }
  
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    /* we intentionally do NOT call super, could fetch the count */
    
    _d.append(" batch=" + this.currentBatchIndex + "/" + 
              this.numberOfObjectsPerBatch);
    
    if (this.count != null)
      _d.append(" has-count=" + this.count);
    
    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);
  }
}
