/*
  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.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

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

/*
 * 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 */
  
  @Override
  public void awakeFromFetch(EODatabase _db) {
    this.database = _db;
    this.isNew    = false;
    if (_db != null && this.entity == null)
      this.entity = _db.entityForObject(this);
  }
  @Override
  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 */
  
  @Override
  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 */
  
  @Override
  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);
    }
  }
  @Override
  public Object handleQueryWithUnboundKey(String _key) {
    this.willRead();
    return this.values != null && _key != null ? this.values.get(_key) : null;
  }
  
  
  /* relationships */
  
  @Override
  public void addObjectToPropertyWithKey(Object _eo, String _key) {
    if (_eo == null || _key == null)
      return;
    
    EOEntity src = this.entity();
    if (src == null) {
      super.addObjectToPropertyWithKey(_eo, _key);
      return;
    }
    
    /* do not create List properties for toOne relationships */
    EORelationship rel = src.relationshipNamed(_key);
    if (rel == null || rel.isToMany())
      super.addObjectToPropertyWithKey(_eo, _key);
    else
      this.takeValueForKey(_eo, _key);
  }
  
  @Override
  public void addObjectToBothSidesOfRelationshipWithKey
    (EORelationshipManipulation _eo, String _key)
  {
    if (_eo == null || _key == null)
      return;
    
    this.addObjectToPropertyWithKey(_eo, _key);
    
    EOEntity src = this.entity();
    if (src == null) return; // TBD: log
    
    EORelationship rel = src.relationshipNamed(_key);
    if (rel == null)
      return; // TBD: log
    
    if ((rel = rel.inverseRelationship()) == null)
      return; /* there was no inverse */
    
    /* found the inverse, patch it :-) */
    _eo.addObjectToPropertyWithKey(this, rel.name());
  }
  
  @Override
  public void removeObjectToBothSidesOfRelationshipWithKey
    (EORelationshipManipulation _eo, String _key)
  {
    if (_eo == null || _key == null)
      return;
    
    this.removeObjectFromPropertyWithKey(_eo, _key);
    
    EOEntity src = this.entity();
    if (src == null) return; // TBD: log
    
    EORelationship rel = src.relationshipNamed(_key);
    if ((rel = rel.inverseRelationship()) == null)
      return; /* there was no inverse */
    
    /* found the inverse, patch it :-) */
    _eo.removeObjectFromPropertyWithKey(this, rel.name());
  }
  
  /* dispose */
  
  public void dispose() {
    this.database = null;
    this.entity   = null;
  }
  
  /* description */

  public void appendPropertiesToStringBuilder
    (StringBuilder _d, boolean _doWrap)
  {
    _d.append("{");
    if (_doWrap) _d.append("\n");
    
    Collection keys = null;
    if (this.entity != null) {
      keys = Arrays.asList
        (UList.valuesForKey(this.entity.attributes(), "name"));
    }
    if (keys == null)
      keys = this.values.keySet();
    
    /* necessary to avoid cycles of EOs */
    for (Object ko: keys) {
      String k = (String)ko;
      _d.append(_doWrap ? "  ": " ");
      _d.append(k);
      _d.append('=');
      
      Object v = this.storedValueForKey(k);
      if (v == null)
        _d.append("<null>");
      else if (v instanceof Number || v instanceof String)
        _d.append(v);
      else if (v instanceof Date)
        _d.append(v);
      else if (v instanceof List) {
        _d.append("<List:");
        _d.append(((List)v).size());
        _d.append(">");
      }
      else {
        _d.append("<");
        _d.append(v.getClass().getSimpleName());
        _d.append(">");
      }
      _d.append(_doWrap ? ";\n" : ";");
    }
    
    _d.append(_doWrap ? "}" : " }");
  }
  
  @Override
  public void appendAttributesToDescription(StringBuilder _d) {
    super.appendAttributesToDescription(_d);
    
    if (this.entity != null)
      _d.append(" entity=" + this.entity.name());
    
    // logging the DB is too much output for records
    //if (this.database != null)
    //  _d.append(" db=" + this.database);
    
    if (this.values != null || this.entity != null) {
      _d.append(" values=");
      this.appendPropertiesToStringBuilder(_d, true /* wrap */);
    }
  }
}
