package org.opengroupware.jope.eocontrol;

import java.util.HashMap;
import java.util.Map;

import org.opengroupware.jope.foundation.NSKeyValueStringFormatter;
import org.opengroupware.jope.foundation.NSObject;

/*
 * EOFetchSpecification
 * 
 * Raw SQL hint: EOCustomQueryExpressionHintKey
 */
// TODO: finish me
public class EOFetchSpecification extends NSObject {

  protected String             entityName;
  protected String[]           attributeNames;
  protected EOQualifier        qualifier;
  protected EOSortOrdering[]   sortOrderings;
  protected int                fetchLimit;
  protected int                fetchOffset;
  protected Map                userInfo;
  protected Map<String,Object> hints;
  protected boolean            usesDistinct;
  protected boolean            locksObjects;
  protected boolean            deep;
  protected boolean            fetchesRawRows;
  protected boolean            fetchesReadOnly;
  protected boolean            requiresAllQualifierBindingVariables;
  
  /* construction */
  
  public EOFetchSpecification() {
    this(null, null, null, false, false, null);
  }

  public EOFetchSpecification
    (String _entityName, EOQualifier _qualifier, EOSortOrdering[] _orderings)
  {
    this(_entityName, _qualifier, _orderings,
         false, false, null);
  }

  public EOFetchSpecification
    (String _entityName, EOQualifier _qualifier, EOSortOrdering[] _orderings,
     boolean _usesDistinct, boolean _isDeep, Map<String, Object> _hints)
  {
    this.entityName    = _entityName;
    this.qualifier     = _qualifier;
    this.sortOrderings = _orderings;
    this.usesDistinct  = _usesDistinct;
    this.deep          = _isDeep;
    
    /* properly copy mutable deep structures */
    // TODO: should we copy the sort orderings array?
    // Note: qualifiers are immutable
    this.hints = _hints != null ? new HashMap<String, Object>(_hints) : null;
  }
  
  public EOFetchSpecification(EOFetchSpecification _src) {
    // Note: we can't use a null _src here
    this(_src.entityName(), _src.qualifier(), _src.sortOrderings(),
         _src.usesDistinct(), _src.isDeep(), _src.hints());
    
    if (_src != null) {
      this.locksObjects    = _src.locksObjects();
      this.fetchLimit      = _src.fetchLimit();
      this.fetchOffset     = _src.fetchOffset();
      this.fetchesRawRows  = _src.fetchesRawRows();
      this.fetchesReadOnly = _src.fetchesReadOnly();
      this.requiresAllQualifierBindingVariables = 
        _src.requiresAllQualifierBindingVariables();
      
      // TODO: should we copy that array?
      this.attributeNames = _src.fetchAttributeNames();
    }
  }
  
  /* accessors */

  public String entityName() {
    return this.entityName;
  }
  
  public void setQualifier(EOQualifier _q) {
    this.qualifier = _q;
  }
  public EOQualifier qualifier() {
    return this.qualifier;
  }
  
  public void setSortOrderings(EOSortOrdering[] _sos) {
    this.sortOrderings = _sos;
  }
  public EOSortOrdering[] sortOrderings() {
    return this.sortOrderings;
  }
  
  public void setFetchLimit(int _limit) {
    this.fetchLimit = _limit;
  }
  public int fetchLimit() {
    return this.fetchLimit;
  }
  
  public void setFetchOffset(int _skipCount) {
    this.fetchOffset = _skipCount;
  }
  public int fetchOffset() {
    return this.fetchOffset;
  }
  
  public void setUserInfo(Map _ui) {
    this.userInfo = _ui;
  }
  public Map userInfo() {
    return this.userInfo;
  }
  
  public void setHints(Map<String,Object> _ui) {
    this.hints = _ui;
  }
  public Map<String,Object> hints() {
    /* Note: we do not return null when possible to avoid checks */
    return this.hints != null ? this.hints : new HashMap<String,Object>();
  }
  public void setHint(String _key, Object _value) {
    if (this.hints == null)
      this.hints = new HashMap<String, Object>(1);
    this.hints.put(_key, _value);
  }
  public void removeHint(String _key) {
    if (this.hints != null && _key != null)
      this.hints.remove(_key);
  }
  
  public void setUsesDistinct(boolean _flag) {
    this.usesDistinct = _flag;
  }
  public boolean usesDistinct() {
    return this.usesDistinct;
  }
  
  public void setLocksObjects(boolean _flag) {
    this.locksObjects = _flag;
  }
  public boolean locksObjects() {
    return this.locksObjects;
  }
  
  public void setIsDeep(boolean _flag) {
    this.deep = _flag;
  }
  public boolean isDeep() {
    return this.deep;
  }
  
  public void setFetchAttributeNames(String[] _attrNames) {
    this.attributeNames = _attrNames;
  }
  public String[] fetchAttributeNames() {
    return this.attributeNames;
  }
  
  public void setFetchesRawRows(boolean _flag) {
    this.fetchesRawRows = _flag;
  }
  public boolean fetchesRawRows() {
    return this.fetchesRawRows;
  }
  
  public void setFetchesReadOnly(boolean _flag) {
    this.fetchesReadOnly = _flag;
  }
  public boolean fetchesReadOnly() {
    return this.fetchesReadOnly;
  }
  
  public void setRequiresAllQualifierBindingVariables(boolean _flag) {
    this.requiresAllQualifierBindingVariables = _flag;
  }
  public boolean requiresAllQualifierBindingVariables() {
    // TODO: I think prepared statements for fspecs which return true can be
    //       cached in the adaptor.
    return this.requiresAllQualifierBindingVariables;
  }
  
  /* hint patterns */
  
  public Map<String, Object> resolveHintBindPatterns(Object _b) {
    if (this.hints == null)
      return null;
    
    Map<String, Object> boundHints = null;
    for (String key: this.hints.keySet()) {
      if (!key.endsWith("BindPattern"))
        continue;
      
      if (boundHints == null)
        boundHints = new HashMap<String, Object>(this.hints);
      
      boundHints.remove(key); /* remove pattern key */
      String value = this.hints.get(key).toString();
      key = key.substring(0, key.length() - 11);
      
      value = NSKeyValueStringFormatter.format(value, _b);
      boundHints.put(key, value);
    }
    return boundHints != null ? boundHints : this.hints;
  }
  
  /* operations */
  
  public EOFetchSpecification fetchSpecificationWithQualifierBindings
    (Object _b)
  {
    Map<String, Object> boundHints = this.resolveHintBindPatterns(_b);
    
    if (this.qualifier == null && boundHints == this.hints)
      return this;
    
    EOQualifier boundQualifier = null;
    if (this.qualifier != null) {
      boundQualifier = this.qualifier.qualifierWithBindings
        (_b, this.requiresAllQualifierBindingVariables());
      if (boundQualifier == null) /* not all bindings could be resolved */
        return null;
      
      if (boundQualifier == this.qualifier && boundHints == this.hints)
        /* nothing was bound */
        return this;
    }
    
    EOFetchSpecification fs = new EOFetchSpecification(this);
    fs.setQualifier(boundQualifier);
    fs.setHints(boundHints);
    return fs;
  }
  
  public EOFetchSpecification fetchSpecificationWithQualifierBindings
    (String _firstKey, Object... valsAndKeys)
  {
    Map<String, Object> binds = mapForKeyValuesArgs(_firstKey, valsAndKeys);
    if (binds == null || binds.size() == 0)
      return this;
    
    return this.fetchSpecificationWithQualifierBindings(binds);
  }
  
  /* helpers */
  
  public static Map<String, Object> mapForKeyValuesArgs
    (String _firstKey, Object[] _valuesAndMoreKeys)
  {
    Map<String, Object> binds = 
      new HashMap<String, Object>(_valuesAndMoreKeys.length / 2);
    
    for (int i = 0; i < _valuesAndMoreKeys.length; i += 2) {
      if (i == 0)
        binds.put(_firstKey, _valuesAndMoreKeys[i]);
      else
        binds.put((String)_valuesAndMoreKeys[i - 1], _valuesAndMoreKeys[i]);
    }
    return binds;
  }
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.entityName != null) _d.append(" entity="    + this.entityName);
    if (this.qualifier  != null) _d.append(" qualifier=" + this.qualifier);
    if (this.hints      != null) _d.append(" hints="     + this.hints);
    
    if (this.fetchesRawRows)  _d.append(" raw");
    if (this.fetchesReadOnly) _d.append(" readonly");
  }
}
