/*
  Copyright (C) 2006 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;
import org.opengroupware.jope.foundation.NSException;
import org.opengroupware.jope.foundation.NSJavaRuntime;

/*
 * EODatabaseDataSource
 * 
 * Naming convention:
 *   find...     - a method which returns a single object and uses LIMIT 1
 *   fetch..     - a method which returns a List of objects
 *   iteratorFor - a method which returns an Iterator of fetched objects
 *   perform...  - a method which applies a change to the database
 * 
 * TODO: document
 * Note: Hm, the fetch-specification also has an entity name. Should it
 *       override the entityName when its set?
 */
public class EODatabaseDataSource extends EODataSource implements NSDisposable {
  protected static final Log log = LogFactory.getLog("EODatabaseDataSource");
    
  protected EODatabase  database;
  protected String      entityName;
  protected String      fetchSpecificationName;
  protected EOQualifier auxiliaryQualifier;
  protected boolean     isFetchEnabled;
  protected Object      qualifierBindings; /* key/value coding on object */

  public EODatabaseDataSource(EODatabase _db, String _entityName) {
    // Note: in EOF this takes an EOEditingContext
    this.database       = _db;
    this.entityName     = _entityName;
    this.isFetchEnabled = true;
  }
  
  /* accessors */
  
  public void setFetchEnabled(boolean _flag) {
    this.isFetchEnabled = _flag;
  }
  public boolean isFetchEnabled() {
    return this.isFetchEnabled;
  }
  
  public EOEntity entity() {
    String ename = null;
    
    if (this.entityName != null)
      ename = this.entityName;
    else {
      EOFetchSpecification fs = this.fetchSpecification();
      if (fs != null) ename = fs.entityName();
    }
    
    return this.database != null ? this.database.entityNamed(ename) : null;
  }
  
  /* 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() {
    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, null);
    }
    
    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;
  }
  
  /* fetching */

  public EODatabaseChannel iteratorForObjects(EOFetchSpecification _fs) {
    this.lastException = null;
    
    if (_fs == null) {
      log.error("no fetch specification for fetch!");
      return null;
    }
    else if (log.isDebugEnabled())
      log.debug("fetch: " + _fs);
    
    EODatabaseChannel ch = new EODatabaseChannel(this.database);
    if (ch == null) {
      log.error("could not create database channel!");
      return null;
    }
    
    Exception error = ch.selectObjectsWithFetchSpecification(_fs);
    if (error != null) {
      this.lastException = error;
      log.error("could not fetch from database channel: " + ch, error);
      return null;
    }
    
    if (log.isDebugEnabled())
      log.debug("returning channel as iterator: " + ch);
    return ch;
  }
  
  @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 EODatabaseChannel 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 =
      new EOFetchSpecification(findEntity.name(), q, null);
    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);
    }
    
    EODatabaseChannel ch = this.iteratorForObjects(_fs);
    if (ch == null) {
      log.error("could not open database channel for fetch: " + _fs);
      return null;
    }
    
    Object object = ch.fetchObject();
    ch.cancelFetch();
    ch.dispose(); ch = null;
    return object;
  }
  
  public Object find(String _fname) {
    EOEntity entity = this.entity();
    if (entity == null)
      return null;
    
    return this.find(entity.fetchSpecificationNamed(_fname));
  }
  public Object find(String _fn, String _firstKey, Object... valsAndKeys) {
    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) {
    return this.find(this.fetchSpecificationForFind(_pkeys));
  }
  
  public Object find() {
    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);
  }
  
  
  /* change operations */
  
  public Object createObject() {
    Class    clazz = null;
    EOEntity e     = this.entity();
    if (e != null) {
      String clsName = e.className();
    
      if (this.database.classLookupContext() != null) {
        if (clsName == null || clsName.length() == 0)
          clsName = "EOActiveRecord";
        clazz = 
          this.database.classLookupContext().lookupClass(clsName);
      }
      else {
        if (clsName == null || clsName.length() == 0)
          clazz = EOActiveRecord.class;
        else
          clazz = NSJavaRuntime.NSClassFromString(clsName);
      }
    }
    else
      clazz = EOActiveRecord.class;
    
    Object eo = NSJavaRuntime.NSAllocateObject(clazz, EOEntity.class, e);
    
    if (eo != null) {
      if (eo instanceof EOActiveRecord && this.database != null)
        ((EOActiveRecord)eo).setDatabase(this.database);
    }
    
    if (log.isDebugEnabled())
      log.debug("createObject: " + eo);
    
    return eo;
  }

  // hm, in this case we could act like an EOEditingContext and queue changes
  
  public Exception updateObject(Object _object) {
    if (_object == null)
      return null;
    
    if (_object instanceof EOActiveRecord) {
      EOActiveRecord ar = ((EOActiveRecord)_object);
      if (this.database != null) ar.setDatabase(this.database);
      return ((EOActiveRecord)_object).save();
    }
    
    if (_object instanceof EOValidation) {
      Exception e = ((EOValidation)_object).validateForUpdate();
      if (e != null) return e;
    }
    
    EODatabaseOperation op = new EODatabaseOperation(_object, this.entity());
    op.setDatabaseOperator(EOAdaptorOperation.AdaptorUpdateOperator);
    
    return this.database.performDatabaseOperation(op);
  }

  public Exception insertObject(Object _object) {
    if (_object == null)
      return null;

    Exception error = null;
    if (_object instanceof EOActiveRecord) {
      EOActiveRecord ar = ((EOActiveRecord)_object);
      
      /* those can happen if the record was not allocated by us */
      if (this.database != null) ar.setDatabase(this.database);
      if (ar.entity() == null) ar.setEntity(this.entity());
      
      /* attempt to save */
      if ((error = ar.save()) != null)
        return error;
    }
    else {
      if (_object instanceof EOValidation) {
        error = ((EOValidation)_object).validateForInsert();
        if (error != null) return error;
      }

      EODatabaseOperation op = new EODatabaseOperation(_object, this.entity());
      op.setDatabaseOperator(EOAdaptorOperation.AdaptorUpdateOperator);
    
      if ((error = this.database.performDatabaseOperation(op)) != null)
        return error;
    }
    
    /* awake object */
    
    if (_object instanceof EOEnterpriseObject)
      ((EOEnterpriseObject)_object).awakeFromInsertion(this.database);
    
    return null /* everything OK */;
  }

  public Exception deleteObject(Object _object) {
    if (_object == null)
      return null;
    
    if (_object instanceof EOActiveRecord) {
      EOActiveRecord ar = ((EOActiveRecord)_object);
      if (this.database != null) ar.setDatabase(this.database);
      return ar.delete();
    }
    
    if (_object instanceof EOValidation) {
      Exception e = ((EOValidation)_object).validateForDelete();
      if (e != null) return e;
    }

    EODatabaseOperation op = new EODatabaseOperation(_object, this.entity());
    op.setDatabaseOperator(EOAdaptorOperation.AdaptorDeleteOperator);
    
    return this.database.performDatabaseOperation(op);
  }
  
  
  /* adaptor operations */
  
  public Exception perform(EOAdaptorOperation[] _ops) {
    if (_ops == null)
      return new NSException("missing entity for perform");
    
    EOAdaptor adaptor = this.database.adaptor();
    EOAdaptorChannel adChannel = adaptor.openChannelFromPool();
    if (adChannel == null)
      return new NSException("could not open adaptor channel");
    
    Exception error = adChannel.performAdaptorOperations(_ops);
    adaptor.releaseChannel(adChannel); adChannel = null;
    
    return error;
  }
  
  public Exception perform(String _opName, Map<String, Object> _binds) {
    EOEntity entity = this.entity();
    if (entity == null) {
      // TODO: improve error
      return new NSException("missing entity for perform");
    }
    
    /* lookup operations in entity */
    
    EOAdaptorOperation[] ops = entity.adaptorOperationsNamed(_opName);
    if (ops == null)
      return new NSException("did not find operations to perform: " + _opName);
    
    /* apply bindings */
    
    if (_binds != null && _binds.size() == 0) {
      for (int i = 0; i < ops.length; i++) {
        ops[i] = ops[i].adaptorOperationWithQualifierBindings(_binds);
        if (ops[i] == null)
          return new NSException("could not apply bindings for operation");
      }
    }
    
    /* run operations */
    
    return this.perform(ops);
  }
  
  public Exception perform
    (String _opName, String firstKey, Object... _valuesAndMoreKeys)
  {
    return this.perform
      (_opName,
       EOFetchSpecification.mapForKeyValuesArgs(firstKey, _valuesAndMoreKeys));
  }
  
  /* dispose */
  
  public void dispose() {
    this.database   = null;
    this.entityName = null;
    this.fetchSpecification = null;
    this.auxiliaryQualifier = null;
    this.qualifierBindings  = null;
  }
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
  }
}
