/*
  Copyright (C) 2007 Helge Hess

  This file is part of JOPE.

  JOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  JOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with JOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/
package org.opengroupware.jope.eoaccess;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
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.foundation.NSDisposable;

/**
 * Common superclass of EODatabaseDataSource and EOAdaptorDataSource.
 */
public abstract class EOAccessDataSource extends EODataSource
  implements NSDisposable
{
  private static final Log log = LogFactory.getLog("EOAccessDataSource");
  protected String      entityName;
  protected String      fetchSpecificationName;
  protected EOQualifier auxiliaryQualifier;
  protected boolean     isFetchEnabled;
  protected Object      qualifierBindings; /* key/value coding on object */
  
  public EOAccessDataSource() {
    this.isFetchEnabled = true;
  }

  /* accessors */

  public void setFetchEnabled(boolean _flag) {
    this.isFetchEnabled = _flag;
  }
  public boolean isFetchEnabled() {
    return this.isFetchEnabled;
  }
  
  /* model */
  
  public abstract EOEntity entity();
  
  /* logging */
  
  public Log log() {
    return log;
  }
  
  /* bindings */
  
  public String[] qualifierBindingKeys() {
    EOFetchSpecification fs = this.fetchSpecification();
    EOQualifier q   = fs != null ? fs.qualifier() : null;
    EOQualifier aux = this.auxiliaryQualifier();
    
    if (q == null && aux == null)
      return null;
    
    Set<String> keys = new HashSet<String>(16);
    if (q   != null) q.addBindingKeysToSet(keys);
    if (aux != null) aux.addBindingKeysToSet(keys);
    return keys.toArray(new String[keys.size()]);
  }
  
  public void setQualifierBindings(Object _bindings) {
    if (this.qualifierBindings == _bindings)
      return;
    
    this.qualifierBindings = _bindings;
    // TODO: notify
  }
  public Object qualifierBindings() {
    return this.qualifierBindings;
  }
  
  /* fetch specification */
  
  public void setAuxiliaryQualifier(EOQualifier _q) {
    if (this.auxiliaryQualifier != _q) {
      this.auxiliaryQualifier = _q;
      // TODO: notify
    }
  }
  public EOQualifier auxiliaryQualifier() {
    return this.auxiliaryQualifier;
  }

  public void setFetchSpecification(EOFetchSpecification _fs) {
    this.fetchSpecificationName = null;
    super.setFetchSpecification(_fs);
  }
  
  public void setFetchSpecificationByName(String _name) {
    EOFetchSpecification fs = null;
    
    if (_name != null) {
      EOEntity entity = this.entity();
      if (entity != null)
        fs = entity.fetchSpecificationNamed(_name);
    }
    
    this.setFetchSpecification(fs);
    this.fetchSpecificationName = _name;
  }
  public String fetchSpecificationName() {
    return this.fetchSpecificationName;
  }
  
  public EOFetchSpecification fetchSpecificationForFetch() {
    /* Note: this always returns a copy of the fetchspec, so subsequent
     *       calls can feel free to modify the results of this method
     */
    EOFetchSpecification fs = this.fetchSpecification();
    if (fs == null) {
      if (this.entityName == null) {
        log().error("no entity name is set, cannot construct fetchspec");
        return null;
      }
      fs = new EOFetchSpecification(this.entityName, null /*q*/, null /*so*/);
    }
    
    EOQualifier aux = this.auxiliaryQualifier();
    if (aux == null && this.qualifierBindings == null)
      return fs;
    
    /* copy fetchspec */
    
    fs = new EOFetchSpecification(fs);
    
    /* merge in aux qualifier */
    
    if (aux != null) {
      EOQualifier q = fs.qualifier();
      if (q == null)
        fs.setQualifier(aux);
      else {
        q = new EOAndQualifier(new EOQualifier[] { q, aux });
        fs.setQualifier(q);
      }
    }
    
    /* apply bindings */
    
    if (this.qualifierBindings != null)
      fs = fs.fetchSpecificationWithQualifierBindings(this.qualifierBindings());
    
    return fs;
  }
  
  
  /* fetches */

  public abstract Iterator iteratorForObjects(EOFetchSpecification _fs);
  
  @Override
  public Iterator iteratorForObjects() {
    if (!this.isFetchEnabled()) {
      log.debug("fetch is disabled, returning empty operator ...");
      return new ArrayList<Object>(0).iterator();
    }
    
    return this.iteratorForObjects(this.fetchSpecificationForFetch());
  }
  
  public Iterator iteratorForSQL(String _sql) {
    if (_sql == null || _sql.length() == 0)
      return null;
    
    EOFetchSpecification fs =
      new EOFetchSpecification(this.entityName, null /* qualifier */, null);
    
    Map<String, Object> hints = new HashMap<String, Object>(1);
    hints.put("EOCustomQueryExpressionHintKey", _sql);
    fs.setHints(hints);

    return this.iteratorForObjects(fs);
  }
  
  public List fetchObjectsForSQL(String _sql) {
    return this.iteratorToList(this.iteratorForSQL(_sql));
  }
  
  public List fetchObjects(String _fn, String _firstKey, Object... valsAndKeys){
    EOEntity findEntity = this.entity();
    if (findEntity == null) {
      log.error("did not find entity, cannot construct fetchspec");
      // TODO: set lastException
      return null;
    }
    
    EOFetchSpecification fs = findEntity.fetchSpecificationNamed(_fn);
    if (fs == null) {
      log.error("did not find fetchspec: " + _fn);
      // TODO: set lastException
      return null;
    }
    
    Map<String, Object> binds = 
      EOFetchSpecification.mapForKeyValuesArgs(_firstKey, valsAndKeys);
    if (binds != null && binds.size() > 0)
      fs = fs.fetchSpecificationWithQualifierBindings(binds);
    
    return this.iteratorToList(this.iteratorForObjects(fs));
  }

  /* finders */
  // TODO: move out to 'Finder' objects which are also used by KVC
  
  public EOFetchSpecification fetchSpecificationForFind(Object[] _pkeyVals) {
    if (_pkeyVals == null || _pkeyVals.length < 1)
      return null;
    
    EOEntity findEntity = this.entity();
    if (findEntity == null) {
      log.error("did not find entity, cannot construct find fetchspec");
      return null;
    }
    
    String[] pkeys = findEntity.primaryKeyAttributeNames();
    if (pkeys == null || pkeys.length == 0) {
      // TODO: hm, should we invoke a 'primary key find' policy here? (like
      //       matching 'id' or 'tablename_id')
      log.error("did not find primary keys, cannot construct find fspec");
      return null;
    }
    
    /* build qualifier for primary keys */

    EOQualifier q;
    if (pkeys.length == 1) {
      q = new EOKeyValueQualifier(pkeys[0], _pkeyVals[0]);
    }
    else {
      EOQualifier[] qs = new EOQualifier[pkeys.length];
      for (int i = 0; i < pkeys.length; i++) {
        Object v = i < _pkeyVals.length ? _pkeyVals[i] : null;
        qs[i] = new EOKeyValueQualifier(pkeys[i], v);
      }
      q = new EOAndQualifier(qs);
    }
    
    /* construct fetch specification */
    
    EOFetchSpecification fs = this.fetchSpecificationForFetch();
    fs.setQualifier(q);
    fs.setSortOrderings(null); /* no sorting, makes DB faster */
    fs.setFetchLimit(1); /* we just want to find one record */
    
    return fs;
  }
  
  public Object find(EOFetchSpecification _fs) {
    if (_fs == null) return null;
    
    if (_fs.fetchLimit() != 1) {
      _fs = new EOFetchSpecification(_fs);
      _fs.setFetchLimit(1);
    }
    
    Iterator ch = this.iteratorForObjects(_fs);
    if (ch == null) {
      log.error("could not open iterator for fetch: " + _fs);
      return null;
    }
    
    Object object = ch.next();
    
    /* Note: we do not close the Iterator, so if its an external resource,
     *       you should override the find method in your subclass!
     */
    
    return object;
  }
  
  public Object find(String _fname) {
    EOEntity entity = this.entity();
    if (entity == null)
      return null;
    
    EOFetchSpecification fs = entity.fetchSpecificationNamed(_fname);
    if (fs == null) {
      log.warn("did not find fetch specification: '" + _fname + "'");
      // TODO: set lastException
      return null;
    }
    
    return this.find(fs);
  }
  public Object find(String _fn, String _firstKey, Object... valsAndKeys) {
    /*
     * Sample:
     *   find("findByToken", "token", "12345", "login", "donald");
     *   
     * This replaces the 'token' and 'login' binds in the named 'findByToken'
     * fetchspec with the mentioned values.
     */
    EOEntity findEntity = this.entity();
    if (findEntity == null)
      return null;
    
    EOFetchSpecification fs = findEntity.fetchSpecificationNamed(_fn);
    if (fs == null) {
      log.error("did not find fetchspec: " + _fn);
      // TODO: set lastException
      return null;
    }
    
    Map<String, Object> binds = 
      EOFetchSpecification.mapForKeyValuesArgs(_firstKey, valsAndKeys);
    if (binds != null && binds.size() > 0)
      fs = fs.fetchSpecificationWithQualifierBindings(binds);
    
    return this.find(fs);
  }

  public Object findById(Object... _pkeys) {
    EOFetchSpecification fs = this.fetchSpecificationForFind(_pkeys);
    if (fs == null) {
      log.error("did not find fetchspec for pkeys: " + _pkeys);
      // TODO: set lastException
      return null;
    }
    
    return this.find(fs);
  }
  
  public Object find() {
    /* find is like fetch, only difference is that it returns just one object */
    return this.find(this.fetchSpecificationForFetch());
  }
  
  public Object findBySQL(String _sql) {
    if (_sql == null || _sql.length() == 0)
      return null;
    
    EOFetchSpecification fs =
      new EOFetchSpecification(this.entityName, null /* qualifier */, null);
    fs.setFetchLimit(1);
    
    Map<String, Object> hints = new HashMap<String, Object>(1);
    hints.put("EOCustomQueryExpressionHintKey", _sql);
    fs.setHints(hints);
    return this.find(fs);
  }
  
  public Object findByMatchingAll(String _first, Object... valsAndKeys) {
    Map<String, Object> values = 
      EOFetchSpecification.mapForKeyValuesArgs(_first, valsAndKeys);
    EOQualifier q = EOQualifier.qualifierToMatchAllValues(values);
    
    EOFetchSpecification fs = this.fetchSpecificationForFetch();
    fs.setQualifier(q);
    return this.find(fs);
  }
  public Object findByMatchingAny(String _first, Object... valsAndKeys) {
    Map<String, Object> values = 
      EOFetchSpecification.mapForKeyValuesArgs(_first, valsAndKeys);
    EOQualifier q = EOQualifier.qualifierToMatchAllValues(values);
    
    EOFetchSpecification fs = this.fetchSpecificationForFetch();
    fs.setQualifier(q);
    return this.find(fs);
  }

  /* dispose */
  
  public void dispose() {
    this.entityName         = null;
    this.fetchSpecification = null;
    this.auxiliaryQualifier = null;
    this.qualifierBindings  = null;
  }
  
  /* description */
  
  @Override
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.entityName != null)
      _d.append(" entity=" + this.entityName);
    
    if (this.fetchSpecification != null)
      _d.append(" fs=" + this.fetchSpecification);

    if (this.auxiliaryQualifier != null)
      _d.append(" aux=" + this.auxiliaryQualifier);
    
    if (this.qualifierBindings != null)
      _d.append(" bindings=" + this.qualifierBindings);
  }
}
