package org.opengroupware.jope.eoaccess;

import java.util.ArrayList;
import java.util.HashMap;
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.EOFetchSpecification;
import org.opengroupware.jope.eocontrol.EOKeyValueCoding;
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;
import org.opengroupware.jope.foundation.NSKeyValueCodingAdditions;
import org.opengroupware.jope.foundation.NSObject;

/*
 * EODatabaseChannel
 * 
 * Important: dispose the object if you don't need it anymore.
 * 
 * TODO: document
 * 
 * THREAD: this object is NOT synchronized. Its considered a cheap object which
 *         can be created on demand.
 */
public class EODatabaseChannel extends NSObject
  implements NSDisposable, Iterator
{
  protected static final Log log = LogFactory.getLog("EODatabaseChannel");
  
  protected EODatabase          database;
  protected EOAdaptorChannel    adChannel;
  protected EOEntity            currentEntity;
  protected Class               currentClass;
  protected boolean             isLocking;
  protected boolean             fetchesRawRows;
  protected boolean             makeNoSnapshots;

  protected int recordCount;
  protected Iterator<Map<String, Object>> records;
  
  public EODatabaseChannel(EODatabase _db) {
    this.database = _db;
  }
  
  /* accessors */
  
  public void setCurrentEntity(EOEntity _entity) {
    this.currentEntity = _entity;
  }
  public EOEntity currentEntity() {
    return this.currentEntity;
  }
  
  /* operations */
  
  public EOAdaptorChannel adaptorChannel() {
    return this.adChannel;
  }
  
  public Exception selectObjectsWithFetchSpecification
    (EOFetchSpecification _fs)
  {
    boolean isDebugOn = log.isDebugEnabled();
    
    if (isDebugOn) log.debug("select: " + _fs);
    
    /* tear down */
    this.cancelFetch();
    
    /* prepare */

    this.setCurrentEntity(this.database.entityNamed(_fs.entityName()));
    if (this.currentEntity == null) {
      log.error("missing entity named: " + _fs.entityName());
      return new Exception("did not find entity for fetch!");
    }
    
    this.isLocking      = _fs.locksObjects();
    this.fetchesRawRows = _fs.fetchesRawRows();
    
    EOAttribute[] attributes = _fs.fetchAttributeNames() != null
      ? this.currentEntity.attributesWithNames(_fs.fetchAttributeNames())
      : this.currentEntity.attributes();

    if (isDebugOn) {
      log.debug("  entity:     " + this.currentEntity);
      log.debug("  attributes: " + attributes);
    }
    
    this.makeNoSnapshots = false;
    if (this.currentEntity.isReadOnly())
      this.makeNoSnapshots = true;
    else if (_fs.fetchesReadOnly())
      this.makeNoSnapshots = true;
    
    /* determine object class */
    
    if (!this.fetchesRawRows) {
      String clsName = this.currentEntity.className();
    
      if (this.database.classLookupContext() != null) {
        if (clsName == null || clsName.length() == 0)
          this.currentClass = EOActiveRecord.class;
        else {
          this.currentClass = 
            this.database.classLookupContext().lookupClass(clsName);
          if (this.currentClass == null)
            log.error("failed to lookup EO class: " + clsName);
        }
      }
      else {
        if (clsName == null || clsName.length() == 0)
          this.currentClass = EOActiveRecord.class;
        else {
          this.currentClass = NSJavaRuntime.NSClassFromString(clsName);
          if (this.currentClass == null)
            log.error("failed to lookup EO class: " + clsName);
        }
      }
      
      if (this.currentClass == null) {
        this.currentClass = EOActiveRecord.class;
        log.error("got not class for EO, using EOActiveRecord: " +
                  this.currentClass);
      }
    }
    
    /* setup */
    
    Exception error = null;
    try {
      this.adChannel = this.acquireChannel(); 
      if (this.adChannel == null) // TODO: improve error
        return new NSException("could not create adaptor channel");
      
      /* perform fetch */
      
      List<Map<String, Object>> results;
      
      /* Note: custom queries are detected by the adaptor */
      results = this.adChannel.selectAttributes
        (attributes, _fs, this.isLocking, this.currentEntity);
      
      if (results == null) {
        log.error("could not perform adaptor query: ",
                       this.adChannel.lastException);
        return this.adChannel.consumeLastException();
      }
      
      this.recordCount = results.size();
      this.records = results.iterator();
    }
    catch (Exception e) {
      error = e;
    }
    finally {
      this.releaseChannel();
    }
    
    return error;
  }
  
  public void cancelFetch() {
    this.records        = null;
    this.currentEntity  = null;
    this.recordCount    = 0;
    this.isLocking      = false;
    this.fetchesRawRows = false;
    this.currentClass   = null;
  }
  
  public boolean isFetchInProgress() {
    return this.records != null;
  }
  
  /* fetching */
  
  public Map<String, Object> fetchRow() {
    if (this.records == null)
      return null;
    
    if (!this.records.hasNext()) {
      this.cancelFetch();
      return null;
    }
    
    return this.records.next();
  }
  
  public Object fetchObject() {
    boolean isDebugOn = log.isDebugEnabled();
    
    if (isDebugOn) log.debug("fetch object ...");
    
    Map<String, Object> row = this.fetchRow();
    if (row == null) {
      if (isDebugOn) log.debug("  return no row, finished fetching.");
      return null;
    }
    if (this.fetchesRawRows) {
      if (isDebugOn) log.debug("  return raw row: " + row);
      return row;
    }
    if (this.currentClass == null) {
      log.warn("  missing class, return raw row: " + row);
      return row;
    }
    
    // TODO: we might want to do uniquing here ..
    
    /* instantiate new object */
    
    Object eo = NSJavaRuntime.NSAllocateObject
      (this.currentClass, EOEntity.class, this.currentEntity);
    if (eo == null) {
      log.error("failed to allocate EO: " + this.currentClass + ": " + row);
      return null;
    }
    
    if (isDebugOn) log.debug("  allocated: " + eo);
    
    /* apply row values */
    
    Set<String> keys = row.keySet();
    
    if (eo instanceof EOKeyValueCoding) {
      if (isDebugOn) log.debug("  push row: " + row);
      EOKeyValueCoding eok = (EOKeyValueCoding)eo;
      
      for (String attributeName: keys)
        eok.takeStoredValueForKey(row.get(attributeName), attributeName);
      
      if (isDebugOn) log.debug("  filled: " + eo);
    }
    else {
      // TODO: call default implementation
      log.error("attempt to construct a non-EO, not yet implemented: " + eo);
    }
    
    /* awake objects */
    
    if (eo != null) {
      if (eo instanceof EOEnterpriseObject) {
        if (isDebugOn) log.debug("  awake ...: " + eo);
        ((EOEnterpriseObject)eo).awakeFromFetch(this.database);
      }
    }
    
    /* make snapshot */

    if (!this.makeNoSnapshots) {
      /* Why don't we just reuse the row? Because applying the row on the object
       * might have changed or cooerced values which would be incorrectly
       * reported as changes later on.
       * 
       * We make the snapshot after the awake for the same reasons.
       */
      
      Map<String, Object> snapshot = null;
      if (eo instanceof EOKeyValueCoding) {
        if (isDebugOn) log.debug("  make snapshot ...");
        EOKeyValueCoding eok = (EOKeyValueCoding)eo;
        
        snapshot = new HashMap<String, Object>(keys.size());
        for (String attributeName: keys)
          snapshot.put(attributeName, eok.storedValueForKey(attributeName));
      }
  
      /* record snapshot */
      
      if (snapshot != null) {
        if (eo instanceof EOActiveRecord) {
          // should we calculate the snapshot by querying the object itself to
          // acount for KVC based changes? probably.
          ((EOActiveRecord)eo).setSnapshot(row);
        }
        // else: do something with editing context or database?
      }
    }
    
    if (isDebugOn) log.debug("fetched object: " + eo);
    return eo;
  }
  
  /* database operations (handled by EODatabaseContext in EOF) */
  
  public Exception performDatabaseOperations(EODatabaseOperation[] _ops) {
    if (_ops == null || _ops.length == 0)
      return null; /* nothing to do */
    
    /* turn db ops into adaptor ops */
    
    List<EOAdaptorOperation> aops = 
      this.adaptorOperationsForDatabaseOperations(_ops);
    if (aops == null || aops.size() == 0)
      return null; /* nothing to do */
    
    /* perform adaptor ops */
    
    Exception error = null;
    try {
      this.adChannel = this.acquireChannel(); 
      if (this.adChannel == null) // TODO: improve error
        return new NSException("could not create adaptor channel");
      
      error = this.adChannel.performAdaptorOperations(aops);
    }
    catch (Exception e) {
      error = e;
    }
    finally {
      this.releaseChannel();
    }
    
    return error;
  }
  
  protected List<EOAdaptorOperation> adaptorOperationsForDatabaseOperations
    (EODatabaseOperation[] _ops)
  {
    if (_ops == null || _ops.length == 0)
      return null; /* nothing to do */
    
    List<EOAdaptorOperation> aops = new ArrayList<EOAdaptorOperation>(4);
    
    for (int i = 0; i < _ops.length; i++) {
      EOEntity           entity = _ops[i].entity();
      EOAdaptorOperation aop = new EOAdaptorOperation(entity);
      aop.setAdaptorOperator(_ops[i].databaseOperator());
      
      switch (_ops[i].databaseOperator()) {
        case EOAdaptorOperation.AdaptorDeleteOperator: {
          // TODO: do we also want to add attrs used for locking?
          Map<String, Object> snapshot = _ops[i].dbSnapshot();
          EOQualifier pq;
          if (snapshot == null)
            pq = entity.qualifierForPrimaryKey(_ops[i].object());
          else
            pq = entity.qualifierForPrimaryKey(snapshot);

          if (pq == null) {
            log.error("could not calculate primary key qualifier for op");
            throw new NSException("could not determine primary-key qualifier!");
          }
          aop.setQualifier(pq);
          break;
        }
        
        case EOAdaptorOperation.AdaptorInsertOperator: {
          Map<String, Object> values =
            NSKeyValueCodingAdditions.Utility.valuesForKeys
              (_ops[i].object(), entity.classPropertyNames());
          aop.setChangedValues(values);
          
          _ops[i].setNewRow(values);
          
          // TODO: we need to know our new primary key for auto-increment keys!
          break;
        }
        
        case EOAdaptorOperation.AdaptorUpdateOperator:
          Map<String, Object> snapshot = _ops[i].dbSnapshot();
          
          /* calculate qualifier */
          
          EOQualifier pq;
          if (snapshot == null)
            pq = entity.qualifierForPrimaryKey(_ops[i].object());
          else
            pq = entity.qualifierForPrimaryKey(snapshot);
          
          if (pq == null) {
            log.error("could not calculate primary key qualifier for " +
                           "operation, snapshot: " + snapshot);
            throw new NSException("could not determine primary-key qualifier!");
          }
          
          EOAttribute[] lockAttrs = entity.attributesUsedForLocking();
          if (lockAttrs != null && lockAttrs.length > 0 && snapshot != null) {
            EOQualifier[] qs = new EOQualifier[lockAttrs.length + 1];
            qs[0] = pq;
            for (int j = 1; j < lockAttrs.length; j++) {
              String name = lockAttrs[j - 1].name();
              if (name == null) name = lockAttrs[j - 1].columnName();
              qs[j] = new EOKeyValueQualifier(name, snapshot.get(name));
            }
            pq = new EOAndQualifier(qs);
          }
          
          aop.setQualifier(pq);
          
          /* calculate changed values */
          
          Map<String, Object> values = null;
          
          if (_ops[i].object() instanceof EOEnterpriseObject) {
            EOEnterpriseObject eo = (EOEnterpriseObject)_ops[i].object();
            if (snapshot != null)
              values = eo.changesFromSnapshot(snapshot);
            else {
              /* no snapshot, need to update everything */
              values = NSKeyValueCodingAdditions.Utility.valuesForKeys
                (_ops[i].object(), entity.classPropertyNames());
            }
          }
          else {
            log.warn("object for update is not an EOEnterpriseObject");
            /* for other objects we just update everything */
            values = NSKeyValueCodingAdditions.Utility.valuesForKeys
              (_ops[i].object(), entity.classPropertyNames());
            // TODO: changes might include non-class props (like assocs)
          }
          
          if (values == null || values.size() == 0) {
            if (log.isInfoEnabled())
              log.info("no values to update: " + _ops[i]);
            aop = null;
          }
          else {
            aop.setChangedValues(values);

            /* Note: we need to copy the snapshot because we might ignore it in
             *       case the dbop fails.
             */
            if (snapshot != null && snapshot != values) {
              snapshot = new HashMap<String, Object>(snapshot);
              snapshot.putAll(values); /* overwrite old values */
            }
            
            _ops[i].setDBSnapshot(snapshot);
          }
          break;
          
        default:
          log.warn("unsupported database operation: " + _ops[i]);
          aop = null;
          break;
      }
      
      if (aop != null) {
        aops.add(aop);
        _ops[i].addAdaptorOperation(aop);
      }
    }
    return aops;
  }
  
  /* dispose */
  
  public void dispose() {
    this.cancelFetch();
    this.releaseChannel();
    this.database = null;
  }
  
  /* iterator */

  public boolean hasNext() {
    return this.records.hasNext();
  }

  public Object next() {
    return this.fetchObject();
  }

  public void remove() {
    throw new UnsupportedOperationException
      ("EODatabaseChannel does not support remove");
  }
  
  /* channel */
  
  protected EOAdaptorChannel acquireChannel() {
    if (this.database == null)
      return null;
    
    EOAdaptor adaptor = this.database.adaptor();
    if (adaptor == null)
      return null;
    
    log.info("opening adaptor channel ...");
    return adaptor.openChannelFromPool();
  }
  
  protected void releaseChannel() {
    if (this.adChannel != null) {
      EOAdaptor adaptor = null;
      
      if (this.database != null)
        adaptor = this.database.adaptor();
      
      if (log.isInfoEnabled())
        log.info("releasing adaptor channel: " + this.adChannel);
      
      if (adaptor != null)
        adaptor.releaseChannel(this.adChannel);
      else
        this.adChannel.dispose();
      this.adChannel = null;
    }
  }
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.database != null)
      _d.append(" db=" + this.database);
    if (this.adChannel != null)
      _d.append(" channel=" + this.adChannel);
  }
}
