package org.opengroupware.jope.eoaccess;

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

import org.opengroupware.jope.foundation.NSDisposable;
import org.opengroupware.jope.foundation.NSException;

/*
 * EOActiveRecord
 */
public class EOActiveRecord extends EOCustomObject
  implements NSDisposable
{
  
  protected EODatabase database;
  protected EOEntity   entity;
  protected boolean    isNew;
  protected Map<String, Object> values;
  protected Map<String, Object> snapshot;
  
  /* construction */
  
  public EOActiveRecord(EODatabase _database, EOEntity _entity) {
    this.database = _database;
    this.entity   = _entity;
    this.isNew    = true;
  }
  
  public EOActiveRecord(EODatabase _database, String _entityName) {
    this(_database, 
         _database != null ? _database.entityNamed(_entityName) : null);
  }
  
  public EOActiveRecord(EOEntity _entity) {
    this(null, _entity);
  }

  public EOActiveRecord(EODatabase _database) {
    this(_database, (EOEntity)null);
  }

  public EOActiveRecord() {
    /* This is allowed for custom subclasses which can be found using the
     * model. The use should be restricted to object creation.
     */
    this(null, (EOEntity)null);
  }

  /* initialization */
  
  public void awakeFromFetch(EODatabase _db) {
    this.database = _db;
    this.isNew    = false;
    if (_db != null && this.entity == null)
      this.entity = _db.entityForObject(this);
  }
  public void awakeFromInsertion(EODatabase _db) {
    this.database = _db;
    this.isNew    = true;
    if (_db != null && this.entity == null)
      this.entity = _db.entityForObject(this);
  }
  
  /* accessors */
  
  public void setDatabase(EODatabase _db) {
    if (this.database == null)
      this.database = _db;
    else if (this.database != _db) {
      /* we are migrating to a different database?! */
      this.database = _db;
    }
  }
  public EODatabase database() {
    return this.database;
  }
  
  public void setEntity(EOEntity _entity) {
    if (this.entity == null)
      this.entity = _entity;
    else if (this.entity != _entity) {
      /* maybe we should forbid this */
      this.entity = _entity;
    }
  }
  public EOEntity entity() {
    /* Note: do not call db.entityForObject() here, causes cycles */
    return this.entity;
  }
  public String entityName() {
    return this.entity != null ? this.entity.name() : null;
  }
  
  public boolean isNew() {
    return this.isNew;
  }
  
  public boolean isReadOnly() {
    if (this.isNew)
      return false;
    if (this.snapshot == null) /* no snapshot was made! */
      return true;
    
    EOEntity lEntity = this.entity();
    if (lEntity != null)
      return lEntity.isReadOnly();
    
    return false; /* we have a snapshot */
  }

  public boolean hasChanges() {
    if (this.isNew)
      return true;
    if (this.snapshot == null)
      return false;
    
    Map<String, Object> changes = this.changesFromSnapshot(this.snapshot());
    if (changes == null || changes.size() == 0)
      return false;
    
    return true;
  }
  
  /* snapshot */
  
  protected void setSnapshot(Map<String, Object> _values) {
    this.snapshot = _values;
  }
  public Map<String, Object> snapshot() {
    return this.snapshot;
  }
  
  /* saving */
  
  public Exception validateForSave() {
    if (this.isReadOnly())
      return new NSException("object is readonly");
    
    return super.validateForSave();
  }

  public Exception save() {
    /* Note: we have no reference to the datasource which is why we can't
     *       just call the matching methods in there. But the datasource knows
     *       about us and lets us do the work.
     */
    Exception e = null;
    
    if (this.database == null)
      return new NSException("cannot save w/o having a database assigned!");
    
    /* validate and create database operation */
    
    EODatabaseOperation op = null;
    if (this.isNew()) {
      if ((e = this.validateForInsert()) != null)
        return e;
      
      op = new EODatabaseOperation(this, this.entity());
      op.setDatabaseOperator(EOAdaptorOperation.AdaptorInsertOperator);
    }
    else {
      if ((e = this.validateForUpdate()) != null)
        return e;
      
      op = new EODatabaseOperation(this, this.entity());
      op.setDatabaseOperator(EOAdaptorOperation.AdaptorUpdateOperator);
    }
    
    if (op == null) /* can't really happen, but stay on the safe side */
      return new NSException("could not construct DB operation for save");
    
    if (this.snapshot != null)
      op.setDBSnapshot(this.snapshot);
    
    /* perform database operation */
    
    e = this.database.performDatabaseOperation(op);
    if (e != null) return e;
    
    /* worked out, update tracking state */
    
    this.snapshot = this.isNew() ? op.newRow() : op.dbSnapshot();
    this.isNew    = false;
    
    return null /* null problemo */;
  }
  
  public Exception delete() {
    /* validate */
    
    Exception e = this.validateForDelete();
    if (e != null) return e;
    
    /* check for new objects */
    
    if (this.isNew())
      return null; /* nothing to be done in the DB */
    
    /* create database operation */
    
    EODatabaseOperation op = new EODatabaseOperation(this, this.entity());
    
    if (op == null) /* can't really happen, but stay on the safe side */
      return new NSException("could not construct DB operation for delete");

    op.setDatabaseOperator(EOAdaptorOperation.AdaptorDeleteOperator);
    if (this.snapshot != null)
      op.setDBSnapshot(this.snapshot);
    
    /* perform database operation */
    
    e = this.database.performDatabaseOperation(op);
    if (e != null) return e;
    
    /* clear some tracking state */
    
    this.snapshot = null;
    
    return null /* everything is bloomy */;
  }
  
  /* KVC */
  
  public void handleTakeValueForUnboundKey(Object _value, String _key) {
    //System.err.println("TODO: unbound takeValue " + _key + ": " + _value);
    
    if (this.values == null)
      this.values = new HashMap<String, Object>(8);
    
    this.willChange(); // TODO: only use if the value actually changed
    if (_value == null) {
      this.values.remove(_key);
    }
    else {
      this.values.put(_key, _value);
    }
  }
  public Object handleQueryWithUnboundKey(String _key) {
    this.willRead();
    return this.values != null && _key != null ? this.values.get(_key) : null;
  }
  
  /* dispose */
  
  public void dispose() {
    this.database = null;
    this.entity   = null;
  }
  
  /* description */
  
  public void appendAttributesToDescription(StringBuffer _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.entity != null)
      _d.append(" entity=" + this.entity.name());
    if (this.database != null)
      _d.append(" db=" + this.database);
    
    if (this.values != null)
      _d.append(" values=" + this.values);
  }
}
