/*
 * Copyright (c) 1998-2008 Caucho Technology -- all rights reserved
 *
 * This file is part of Resin(R) Open Source
 *
 * Each copy or derived work must preserve the copyright notice and this
 * notice unmodified.
 *
 * Resin Open Source is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * Resin Open Source 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, or any warranty
 * of NON-INFRINGEMENT.  See the GNU General Public License for more
 * details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Resin Open Source; if not, write to the
 *
 *   Free Software Foundation, Inc.
 *   59 Temple Place, Suite 330
 *   Boston, MA 02111-1307  USA
 *
 * @author Rodrigo Westrupp
 */

package com.caucho.amber.type;

import com.caucho.amber.AmberRuntimeException;
import com.caucho.amber.cfg.AbstractConfigIntrospector;
import com.caucho.amber.entity.AmberCompletion;
import com.caucho.amber.entity.AmberEntityHome;
import com.caucho.amber.entity.Entity;
import com.caucho.amber.entity.EntityItem;
import com.caucho.amber.entity.Listener;
import com.caucho.amber.field.*;
import com.caucho.amber.gen.EntityComponent;
import com.caucho.amber.idgen.AmberTableGenerator;
import com.caucho.amber.idgen.IdGenerator;
import com.caucho.amber.idgen.SequenceIdGenerator;
import com.caucho.amber.manager.AmberConnection;
import com.caucho.amber.manager.AmberPersistenceUnit;
import com.caucho.amber.table.Column;
import com.caucho.amber.table.Table;
import com.caucho.bytecode.*;
import com.caucho.config.ConfigException;
import com.caucho.jdbc.*;
import com.caucho.java.JavaWriter;
import com.caucho.java.gen.ClassComponent;
import com.caucho.lifecycle.Lifecycle;
import com.caucho.util.CharBuffer;
import com.caucho.util.L10N;

import java.io.IOException;
import java.lang.reflect.*;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.logging.Logger;

/**
 * Base for entity or mapped-superclass types.
 */
public class EntityType extends BeanType {
  private static final Logger log = Logger.getLogger(EntityType.class.getName());
  private static final L10N L = new L10N(EntityType.class);

  private EntityType _parentType;
  
  Table _table;

  private String _rootTableName;

  private ArrayList<Table> _secondaryTables
    = new ArrayList<Table>();

  private ArrayList<ListenerType> _listeners
    = new ArrayList<ListenerType>();

  private ArrayList<ListenerCallback> _postLoadCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _prePersistCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _postPersistCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _preUpdateCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _postUpdateCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _preRemoveCallbacks
    = new ArrayList<ListenerCallback>();

  private ArrayList<ListenerCallback> _postRemoveCallbacks
    = new ArrayList<ListenerCallback>();

  private Id _id;

  private String _discriminatorValue;
  private boolean _isJoinedSubClass;

  private HashSet<String> _eagerFieldNames;

  private HashMap<String,EntityType> _subEntities;

  private ArrayList<AmberField> _mappedSuperclassFields
    = new ArrayList<AmberField>();

  private ArrayList<AmberField> _fields;

  private boolean _hasDependent;

  private JClass _proxyClass;

  private AmberEntityHome _home;

  protected int _defaultLoadGroupIndex;
  protected int _loadGroupIndex;

  protected int _minDirtyIndex;
  protected int _dirtyIndex;

  private boolean _excludeDefaultListeners;
  private boolean _excludeSuperclassListeners;

  protected boolean _hasLoadCallback;

  private HashMap<String,IdGenerator> _idGenMap
    = new HashMap<String,IdGenerator>();

  private final Lifecycle _lifecycle = new Lifecycle();

  private VersionField _versionField;

  private int _flushPriority;

  private boolean _isIdentityGenerator;
  private boolean _isSequenceGenerator;


  public EntityType(AmberPersistenceUnit amberPersistenceUnit)
  {
    super(amberPersistenceUnit);
  }

  /**
   * returns true for a loadable entity 
   */
  @Override
  public boolean isEntity()
  {
    return ! getBeanClass().isAbstract();
  }
  
  /**
   * Sets the table.
   */
  public void setTable(Table table)
  {
    _table = table;

    // jpa/0gg0
    if (table == null)
      return;

    table.setType(this);

    if (_rootTableName == null)
      _rootTableName = table.getName();
  }

  /**
   * Returns the table.
   */
  public Table getTable()
  {
    // jpa/0gg0
    if (_table == null && ! isAbstractClass()) {
      String sqlName = AbstractConfigIntrospector.toSqlName(getName());
      setTable(_amberPersistenceUnit.createTable(sqlName));
    }

    return _table;
  }

  /**
   * Gets the instance class.
   */
  public Class getInstanceClass()
  {
    return getInstanceClass(Entity.class);
  }

  /**
   * Returns the component interface name.
   */
  @Override
  public String getComponentInterfaceName()
  {
    return "com.caucho.amber.entity.Entity";
  }

  /**
   * Gets a component generator.
   */
  @Override
  public ClassComponent getComponentGenerator()
  {
    return new EntityComponent();
  }

  /**
   * Returns the flush priority.
   */
  public int getFlushPriority()
  {
    return _flushPriority;
  }

  /**
   * Adds a mapped superclass field.
   */
  public void addMappedSuperclassField(AmberField field)
  {
    if (_mappedSuperclassFields.contains(field))
      return;

    _mappedSuperclassFields.add(field);
    Collections.sort(_mappedSuperclassFields, new AmberFieldCompare());
  }

  /**
   * Returns the mapped superclass fields.
   */
  public ArrayList<AmberField> getMappedSuperclassFields()
  {
    return _mappedSuperclassFields;
  }

  /**
   * Returns the mapped superclass field with a given name.
   */
  public AmberField getMappedSuperclassField(String name)
  {
    for (int i = 0; i < _mappedSuperclassFields.size(); i++) {
      AmberField field = _mappedSuperclassFields.get(i);

      if (field.getName().equals(name))
        return field;
    }

    return null;
  }

  /**
   * returns the merged fields
   */
  @Override
  public ArrayList<AmberField> getFields()
  {
    if (_fields != null)
      return _fields;
    else
      return super.getFields();
  }

  /**
   * Returns the root table name.
   */
  public String getRootTableName()
  {
    return _rootTableName;
  }

  /**
   * Sets the root table name.
   */
  public void setRootTableName(String rootTableName)
  {
    _rootTableName = rootTableName;
  }

  /**
   * Returns the version field.
   */
  public VersionField getVersionField()
  {
    return _versionField;
  }

  /**
   * Sets the version field.
   */
  public void setVersionField(VersionField versionField)
  {
    addField(versionField);

    _versionField = versionField;
  }

  /**
   * Adds a secondary table.
   */
  public void addSecondaryTable(Table table)
  {
    if (! _secondaryTables.contains(table)) {
      _secondaryTables.add(table);
    }

    table.setType(this);
  }

  /**
   * Gets the secondary tables.
   */
  public ArrayList<Table> getSecondaryTables()
  {
    return _secondaryTables;
  }

  /**
   * Adds an entity listener.
   */
  public void addListener(ListenerType listener)
  {
    if (_listeners.contains(listener))
      return;

    _listeners.add(listener);
  }

  /**
   * Gets the entity listeners.
   */
  public ArrayList<ListenerType> getListeners()
  {
    return _listeners;
  }

  /**
   * Gets a secondary table.
   */
  public Table getSecondaryTable(String name)
  {
    for (Table table : _secondaryTables) {
      if (table.getName().equals(name))
        return table;
    }

    return null;
  }

  /**
   * Returns true if and only if it has a
   * many-to-one, one-to-one or embedded field/property.
   */
  public boolean hasDependent()
  {
    return _hasDependent;
  }

  /**
   * Sets true if and only if it has a
   * many-to-one, one-to-one or embedded field/property.
   */
  public void setHasDependent(boolean hasDependent)
  {
    _hasDependent = hasDependent;
  }

  /**
   * Returns the java type.
   */
  @Override
  public String getForeignTypeName()
  {
    return getId().getForeignTypeName();
  }

  /**
   * Gets the proxy class.
   */
  public JClass getProxyClass()
  {
    if (_proxyClass != null)
      return _proxyClass;
    else
      return _beanClass;
  }

  /**
   * Gets the proxy class.
   */
  public void setProxyClass(JClass proxyClass)
  {
    _proxyClass = proxyClass;
  }

  /**
   * Returns true if the corresponding class is abstract.
   */
  public boolean isAbstractClass()
  {
    // ejb/0600 - EJB 2.1 are not abstract in this sense
    return getBeanClass().isAbstract() && _proxyClass == null;
  }

  /**
   * Sets the id.
   */
  public void setId(Id id)
  {
    _id = id;
  }

  /**
   * Returns the id.
   */
  public Id getId()
  {
    return _id;
  }

  /**
   * Set true for joined-subclass
   */
  public void setJoinedSubClass(boolean isJoinedSubClass)
  {
    _isJoinedSubClass = isJoinedSubClass;
  }

  /**
   * Set true for joined-subclass
   */
  public boolean isJoinedSubClass()
  {
    if (getParentType() != null)
      return getParentType().isJoinedSubClass();
    else
      return _isJoinedSubClass;
  }

  /**
   * Sets the discriminator value.
   */
  public String getDiscriminatorValue()
  {
    if (_discriminatorValue != null)
      return _discriminatorValue;
    else {
      return getBeanClass().getSimpleName();
    }
  }

  /**
   * Sets the discriminator value.
   */
  public void setDiscriminatorValue(String value)
  {
    _discriminatorValue = value;
  }

  /**
   * Returns true if read-only
   */
  public boolean isReadOnly()
  {
    return getTable().isReadOnly();
  }

  /**
   * Sets true if read-only
   */
  public void setReadOnly(boolean isReadOnly)
  {
    getTable().setReadOnly(isReadOnly);
  }

  /**
   * Returns the cache timeout.
   */
  public long getCacheTimeout()
  {
    return getTable().getCacheTimeout();
  }

  /**
   * Sets the cache timeout.
   */
  public void setCacheTimeout(long timeout)
  {
    getTable().setCacheTimeout(timeout);
  }

  /**
   * Adds a new field.
   */
  @Override
  public void addField(AmberField field)
  {
    super.addField(field);

    if (! field.isLazy()) {
      if (_eagerFieldNames == null)
        _eagerFieldNames = new HashSet<String>();

      _eagerFieldNames.add(field.getName());
    }
  }

  /**
   * Gets the EAGER field names.
   */
  public HashSet<String> getEagerFieldNames()
  {
    return _eagerFieldNames;
  }

  /**
   * Returns the field with a given name.
   */
  @Override
  public AmberField getField(String name)
  {
    if (_id != null) {
      ArrayList<IdField> keys = _id.getKeys();

      for (int i = 0; i < keys.size(); i++) {
        IdField key = keys.get(i);

        if (key.getName().equals(name))
          return key;
      }
    }

    return super.getField(name);
  }

  /**
   * Returns the columns.
   */
  public ArrayList<Column> getColumns()
  {
    // jpa/0gg0
    if (getTable() == null)
      return null;

    return getTable().getColumns();
  }

  /**
   * Gets the exclude default listeners flag.
   */
  public boolean getExcludeDefaultListeners()
  {
    return _excludeDefaultListeners;
  }

  /**
   * Sets the exclude default listeners flag.
   */
  public void setExcludeDefaultListeners(boolean b)
  {
    _excludeDefaultListeners = b;
  }

  /**
   * Gets the exclude superclass listeners flag.
   */
  public boolean getExcludeSuperclassListeners()
  {
    return _excludeSuperclassListeners;
  }

  /**
   * Sets the exclude superclass listeners flag.
   */
  public void setExcludeSuperclassListeners(boolean b)
  {
    _excludeSuperclassListeners = b;
  }

  /**
   * True if the load lifecycle callback should be generated.
   */
  public void setHasLoadCallback(boolean hasCallback)
  {
    _hasLoadCallback = hasCallback;
  }

  /**
   * True if the load lifecycle callback should be generated.
   */
  public boolean getHasLoadCallback()
  {
    return _hasLoadCallback;
  }

  /**
   * Returns the root type.
   */
  public EntityType getRootType()
  {
    EntityType parent = getParentType();

    if (parent != null)
      return parent.getRootType();
    else
      return this;
  }

  /**
   * Returns the parent type.
   */
  public EntityType getParentType()
  {
    return _parentType;
  }

  /**
   * Returns the parent type.
   */
  public void setParentType(EntityType parentType)
  {
    _parentType = parentType;
  }

  /**
   * Adds a sub-class.
   */
  public void addSubClass(EntityType type)
  {
    if (_subEntities == null)
      _subEntities = new HashMap<String,EntityType>();

    _subEntities.put(type.getDiscriminatorValue(), type);
  }

  /**
   * Gets a sub-class.
   */
  public EntityType getSubClass(String discriminator)
  {
    if (_subEntities == null)
      return this;

    EntityType subType = _subEntities.get(discriminator);

    if (subType != null)
      return subType;
    else {
      // jpa/0l15
      for (EntityType subEntity : _subEntities.values()) {
        subType = subEntity.getSubClass(discriminator);

        if (subType != subEntity)
          return subType;
      }

      return this;
    }
  }

  /**
   * Creates a new entity for this specific instance type.
   */
  public Entity createBean()
  {
    try {
      Entity entity = (Entity) getInstanceClass().newInstance();

      return entity;
    } catch (Exception e) {
      throw new AmberRuntimeException(e);
    }
  }

  /**
   * Returns the home.
   */
  public AmberEntityHome getHome()
  {
    if (_home == null) {
      _home = getPersistenceUnit().getEntityHome(getName());
    }

    return _home;
  }

  /**
   * Returns the next load group.
   */
  public int nextLoadGroupIndex()
  {
    int nextLoadGroupIndex = getLoadGroupIndex() + 1;

    _loadGroupIndex = nextLoadGroupIndex;

    return nextLoadGroupIndex;
  }

  /**
   * Returns the current load group.
   */
  public int getLoadGroupIndex()
  {
    return _loadGroupIndex;
  }

  /**
   * Sets the next default loadGroupIndex
   */
  public void nextDefaultLoadGroupIndex()
  {
    _defaultLoadGroupIndex = nextLoadGroupIndex();
  }

  /**
   * Returns the current load group.
   */
  public int getDefaultLoadGroupIndex()
  {
    return _defaultLoadGroupIndex;
  }

  /**
   * Returns true if the load group is owned by this type (not a subtype).
   */
  public boolean isLoadGroupOwnedByType(int i)
  {
    return getDefaultLoadGroupIndex() <= i && i <= getLoadGroupIndex();
  }

  /**
   * Returns the next dirty index
   */
  public int nextDirtyIndex()
  {
    int dirtyIndex = getDirtyIndex();

    _dirtyIndex = dirtyIndex + 1;

    return dirtyIndex;
  }

  /**
   * Returns the current dirty group.
   */
  public int getDirtyIndex()
  {
    return _dirtyIndex;
  }

  /**
   * Returns the min dirty group.
   */
  public int getMinDirtyIndex()
  {
    return _minDirtyIndex;
  }

  /**
   * Returns true if the load group is owned by this type (not a subtype).
   */
  public boolean isDirtyIndexOwnedByType(int i)
  {
    return getMinDirtyIndex() <= i && i < getDirtyIndex();
  }

  /**
   * Initialize the entity.
   */
  @Override
  public void init()
    throws ConfigException
  {
    if (getConfigException() != null)
      return;

    if (! _lifecycle.toInit())
      return;

    super.init();

    // forces table lazy load
    getTable();

    initId();

    _fields = getMergedFields();

    for (AmberField field : getFields()) {
      if (field.isUpdateable())
        field.setIndex(nextDirtyIndex());

      field.init();
    }

    if (getMappedSuperclassFields() == null)
      return;

    for (AmberField field : getMappedSuperclassFields()) {
      if (field.isUpdateable())
        field.setIndex(nextDirtyIndex());

      field.init();
    }
  }

  protected void initId()
  {
    assert getId() != null : "null id for " + getName();

    getId().init();
  }

  protected ArrayList<AmberField> getMergedFields()
  {
    ArrayList<AmberField> mappedFields = getMappedSuperclassFields();
    
    if (mappedFields == null)
      return getFields();
	
    ArrayList<AmberField> resultFields = new ArrayList<AmberField>();

    resultFields.addAll(getFields());

    for (AmberField field : mappedFields) {
      resultFields.add(field);
    }

    Collections.sort(resultFields, new AmberFieldCompare());

    return resultFields;
  }

  /**
   * Start the entry.
   */
  public void start()
    throws ConfigException
  {
    init();

    startGenerator();

    startImpl();

    if (! _lifecycle.toActive())
      return;
  }

  private void startGenerator()
  {
    IdField idGenField = getId().getGeneratedIdField();

    if (idGenField == null)
      return;

    JdbcMetaData md = getPersistenceUnit().getMetaData();

    if ("sequence".equals(idGenField.getGenerator())) {
      _isIdentityGenerator = false;
      _isSequenceGenerator = true;

      if (! md.supportsSequences())
	throw new ConfigException(L.l("'{0}' does not support sequences",
				      md.getDatabaseName()));
    }
    else if ("identity".equals(idGenField.getGenerator())) {
      _isIdentityGenerator = true;
      _isSequenceGenerator = false;

      if (! md.supportsIdentity())
	throw new ConfigException(L.l("'{0}' does not support identity",
				      md.getDatabaseName()));
    }
    else if ("auto".equals(idGenField.getGenerator())) {
      if (md.supportsIdentity())
	_isIdentityGenerator = true;
      else if (md.supportsSequences())
	_isSequenceGenerator = true;
    }

    if (! _isIdentityGenerator
	&& getGenerator(idGenField.getName()) == null) {
      IdGenerator gen;
      
      if (_isSequenceGenerator) {
	String name = getTable().getName() + "_cseq";
	
	gen = getPersistenceUnit().createSequenceGenerator(name, 1);
      }
      else
	gen = getPersistenceUnit().getTableGenerator("caucho");

      _idGenMap.put(idGenField.getName(), gen);
    }

    // XXX: really needs to be called from the table-init code
    for (IdGenerator idGen : _idGenMap.values()) {
      try {
	if (idGen instanceof SequenceIdGenerator) {
	  ((SequenceIdGenerator) idGen).init(_amberPersistenceUnit);
	}
	else if (idGen instanceof AmberTableGenerator) {
	  // jpa/0g60
	  ((AmberTableGenerator) idGen).init(_amberPersistenceUnit);
	}
      } catch (SQLException e) {
	throw ConfigException.create(e);
      }
    }
  }

  private void startImpl()
  {
    try {
      ClassLoader loader = Thread.currentThread().getContextClassLoader();

      for (ListenerType listenerType : getListeners()) {
        String listenerClass = listenerType.getBeanClass().getName();

        Class cl = Class.forName(listenerClass, false, loader);
        Object listener;

        try {
          listener = cl.newInstance();
        } catch (InstantiationException e) {
          throw new ConfigException(L.l("'{0}' could not be instantiated.",
                                        cl));
        }

        for (JMethod jMethod : listenerType.getCallbacks(Listener.PRE_PERSIST)) {
          Method method = getListenerMethod(cl, jMethod.getName());

          if (method != null)
            _prePersistCallbacks.add(new ListenerCallback(listener, method));
        }

        for (JMethod jMethod : listenerType.getCallbacks(Listener.POST_PERSIST)) {
          Method method = getListenerMethod(cl, jMethod.getName());

          if (method != null)
            _postPersistCallbacks.add(new ListenerCallback(listener, method));
        }

        for (JMethod jMethod : listenerType.getCallbacks(Listener.POST_LOAD)) {
          Method method = getListenerMethod(cl, jMethod.getName());

          if (method != null)
            _postLoadCallbacks.add(new ListenerCallback(listener, method));
        }
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw ConfigException.create(e);
    }
  }

  private Method getListenerMethod(Class cl, String methodName)
  {
    if (cl == null || cl.equals(Object.class))
      return null;

    Method []methods = cl.getDeclaredMethods();

    for (int i = 0; i < methods.length; i++) {
      Class []paramTypes = methods[i].getParameterTypes();
      
      if (methods[i].getName().equals(methodName)
          && paramTypes.length == 1
          && getBeanClass().isAssignableTo(paramTypes[0])) {
        return methods[i];
      }
    }

    return getListenerMethod(cl.getSuperclass(), methodName);
  }

  /**
   * Generates a string to load the field.
   */
  @Override
  public int generateLoad(JavaWriter out, String rs,
                          String indexVar, int index)
    throws IOException
  {
    // ejb/0ag3
    // out.print("(" + getInstanceClassName() + ") ");

    out.print("aConn.loadProxy(\"" + getName() + "\", ");

    index = getId().generateLoadForeign(out, rs, indexVar, index);

    out.println(");");

    return index;
  }

  /**
   * Returns true if there's a field with the matching load group.
   */
  public boolean hasLoadGroup(int loadGroupIndex)
  {
    if (loadGroupIndex == 0)
      return true;

    for (AmberField field : getFields()) {
      if (field.hasLoadGroup(loadGroupIndex))
        return true;
    }

    return false;
  }

  /**
   * Generates loading code after the basic fields.
   */
  public int generatePostLoadSelect(JavaWriter out, int index,
				    int loadGroupIndex)
    throws IOException
  {
    if (loadGroupIndex == 0 && getDiscriminator() != null)
      index++;

    // jpa/0l40
    for (EntityType type = this; type != null; type = type.getParentType()) {
      index = generatePostLoadSelect(out, index,
				     type.getMappedSuperclassFields());

      index = generatePostLoadSelect(out, index, type.getFields());
    }

    return index;
  }

  private int generatePostLoadSelect(JavaWriter out,
				     int index,
				     ArrayList<AmberField> fields)
    throws IOException
  {
    if (fields != null) {
      for (int i = 0; i < fields.size(); i++) {
	AmberField field = fields.get(i);

	// jpa/0l40 if (field.getLoadGroupIndex() == loadGroupIndex)
	index = field.generatePostLoadSelect(out, index);
      }
    }

    return index;
  }

  /**
   * Generates the load code for native fields
   */
  public void generateLoadNative(JavaWriter out)
    throws IOException
  {
    int index = 0;
    
    for (AmberField field : getFields()) {
      index = field.generateLoadNative(out, index);
    }
  }

  /**
   * Generates the load code for native fields
   */
  public void generateNativeColumnNames(ArrayList<String> names)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generateNativeColumnNames(names);
    }
  }

  /**
   * Generates a string to set the field.
   */
  @Override
  public void generateSet(JavaWriter out, String pstmt,
                          String index, String value)
    throws IOException
  {
    if (getId() != null)
      getId().generateSet(out, pstmt, index, value);
  }

  /**
   * Gets the value.
   */
  @Override
  public Object getObject(AmberConnection aConn, ResultSet rs, int index)
    throws SQLException
  {
    return getHome().loadLazy(aConn, rs, index);
  }

  /**
   * Finds the object
   */
  @Override
  public EntityItem findItem(AmberConnection aConn, ResultSet rs, int index)
    throws SQLException
  {
    return getHome().findItem(aConn, rs, index);
  }

  /**
   * Gets the value.
   */
  public Object getLoadObject(AmberConnection aConn,
                              ResultSet rs, int index)
    throws SQLException
  {
    return getHome().loadFull(aConn, rs, index);
  }

  /**
   * Returns true for sequence generator
   */
  public boolean isSequenceGenerator()
  {
    return _isSequenceGenerator;
  }

  /**
   * Returns true for sequence generator
   */
  public boolean isIdentityGenerator()
  {
    return _isIdentityGenerator;
  }

  /**
   * Sets the named generator.
   */
  public void setGenerator(String name, IdGenerator gen)
  {
    Thread.dumpStack();
    _idGenMap.put(name, gen);
  }

  /**
   * Sets the named generator.
   */
  public IdGenerator getGenerator(String name)
  {
    return _idGenMap.get(name);
  }

  /**
   * Gets the named generator.
   */
  public long nextGeneratorId(AmberConnection aConn, String name)
    throws SQLException
  {
    IdGenerator idGen = _idGenMap.get(name);

    return idGen.allocate(aConn);
  }

  /**
   * Loads from an object.
   */
  public void generateLoadFromObject(JavaWriter out, String obj)
    throws IOException
  {
    getId().generateLoadFromObject(out, obj);

    for (AmberField field : getFields()) {
      field.generateLoadFromObject(out, obj);
    }
  }

  /**
   * Copy from an object.
   */
  public void generateCopyLoadObject(JavaWriter out,
                                     String dst, String src,
                                     int loadGroup)
    throws IOException
  {
    if (getParentType() != null) // jpa/0ge3
      getParentType().generateCopyLoadObject(out, dst, src, loadGroup);

    ArrayList<AmberField> fields = getFields();

    for (AmberField field : fields) {
      // XXX: setter issue, too

      field.generateCopyLoadObject(out, dst, src, loadGroup);
    }
  }

  /**
   * Copy from an object.
   */
  public void generateMergeFrom(JavaWriter out,
                               String dst, String src)
    throws IOException
  {
    if (getParentType() != null)
      getParentType().generateMergeFrom(out, dst, src);

    ArrayList<AmberField> fields = getFields();

    for (int i = 0; i < fields.size(); i++) {
      AmberField field = fields.get(i);

      field.generateMergeFrom(out, dst, src);
    }
  }

  /**
   * Copy from an object.
   */
  public void generateCopyUpdateObject(JavaWriter out,
                                       String dst, String src,
                                       int updateIndex)
    throws IOException
  {
    if (getParentType() != null)
      getParentType().generateCopyUpdateObject(out, dst, src, updateIndex);

    ArrayList<AmberField> fields = getFields();
    for (int i = 0; i < fields.size(); i++) {
      AmberField field = fields.get(i);

      field.generateCopyUpdateObject(out, dst, src, updateIndex);
    }
  }

  /**
   * Checks entity-relationships from an object.
   */
  public void generateDumpRelationships(JavaWriter out,
                                        int updateIndex)
    throws IOException
  {
    if (getParentType() != null) // jpa/0ge3
      getParentType().generateDumpRelationships(out, updateIndex);

    ArrayList<AmberField> fields = getFields();

    for (int i = 0; i < fields.size(); i++) {
      AmberField field = fields.get(i);

      field.generateDumpRelationships(out, updateIndex);
    }
  }

  /**
   * Generates the select clause for a load.
   */
  public String generateKeyLoadSelect(String id)
  {
    String select = getId().generateLoadSelect(id);

    if (getDiscriminator() != null) {
      if (select != null && ! select.equals(""))
        select = select + ", ";

      select = select + getDiscriminator().getName();
    }

    return select;
  }

  /**
   * Generates the select clause for a load.
   */
  public String generateFullLoadSelect(String id)
  {
    CharBuffer cb = CharBuffer.allocate();

    String idSelect = getId().generateSelect(id);

    if (idSelect != null)
      cb.append(idSelect);

    String loadSelect = generateLoadSelect(id);

    if (! idSelect.equals("") && ! loadSelect.equals(""))
      cb.append(",");

    cb.append(loadSelect);

    return cb.close();
  }

  /**
   * Generates the select clause for a load.
   */
  public String generateLoadSelect(String id)
  {
    return generateLoadSelect(getTable(), id);
  }

  /**
   * Generates the select clause for a load.
   */
  public String generateLoadSelect(Table table, String id)
  {
    StringBuilder sb = new StringBuilder();

    // jpa/0l11
    if (getTable() == table && getDiscriminator() != null) {
      if (id != null) {
        if (getDiscriminator().getTable() == getTable()) {
          sb.append(id + ".");
          sb.append(getDiscriminator().getName());
        }
        else {
          // jpa/0l4b
          sb.append("'" + getDiscriminatorValue() + "'");
        }
      }
    }

    generateLoadSelect(sb, table, id, 0);

    if (sb.length() > 0)
      return sb.toString();
    else
      return null;
  }

  /**
   * Generates the select clause for a load.
   */
  @Override
  public void generateLoadSelect(StringBuilder sb, Table table,
				 String id, int loadGroup)
  {
    if (_parentType != null)
      _parentType.generateLoadSelect(sb, table, id, loadGroup);
    
    super.generateLoadSelect(sb, table, id, loadGroup);
  }

  /**
   * Generates the auto insert sql.
   */
  public String generateAutoCreateSQL(Table table)
  {
    return generateCreateSQL(table, true);
  }

  /**
   * Generates the insert sql.
   */
  public String generateCreateSQL(Table table)
  {
    return generateCreateSQL(table, false);
  }

  /**
   * Generates the insert sql.
   */
  private String generateCreateSQL(Table table, boolean isAuto)
  {
    CharBuffer sql = new CharBuffer();

    sql.append("insert into ");
    sql.append(JavaWriter.escapeJavaString(table.getName()) + " (");

    boolean isFirst = true;

    ArrayList<String> idColumns = new ArrayList<String>();

    for (IdField field : getId().getKeys()) {
      if (isAuto && field.getGenerator() != null)
	continue;
      
      for (Column key : field.getColumns()) {
        String name;

        if (table == key.getTable())
          name = key.getName();
        else
          name = table.getDependentIdLink().getSourceColumn(key).getName();

        idColumns.add(name);

        if (! isFirst)
          sql.append(", ");
        isFirst = false;

        sql.append(name);
      }
    }

    if (table == getTable() && getDiscriminator() != null) {
      if (! isFirst)
        sql.append(", ");
      isFirst = false;

      sql.append(getDiscriminator().getName());
    }

    ArrayList<String> columns = new ArrayList<String>();
    generateInsertColumns(table, columns);

    for (String columnName : columns) {
      if (! isFirst)
        sql.append(", ");
      isFirst = false;

      sql.append(columnName);
    }

    sql.append(") values (");

    isFirst = true;
    for (int i = 0; i < idColumns.size(); i++) {
      if (! isFirst)
        sql.append(", ");
      isFirst = false;

      sql.append("?");
    }

    if (table == getTable() && getDiscriminator() != null) {
      if (! isFirst)
        sql.append(", ");
      isFirst = false;

      sql.append("'" + getDiscriminatorValue() + "'");
    }

    for (int i = 0; i < columns.size(); i++) {
      if (! isFirst)
        sql.append(", ");
      isFirst = false;

      sql.append("?");
    }

    sql.append(")");

    return sql.toString();
  }

  protected void generateInsertColumns(Table table, ArrayList<String> columns)
  {
    if (getParentType() != null)
      getParentType().generateInsertColumns(table, columns);

    for (AmberField field : getFields()) {
      if (field.getTable() == table)
        field.generateInsertColumns(columns);
    }
  }

  /**
   * Generates the update sql.
   */
  public void generateInsertSet(JavaWriter out,
                                Table table,
                                String pstmt,
                                String query,
                                String obj)
    throws IOException
  {
    if (getParentType() != null)
      getParentType().generateInsertSet(out, table, pstmt, query, obj);

    for (AmberField field : getFields()) {
      if (field.getTable() == table)
        field.generateInsertSet(out, pstmt, query, obj);
    }
  }

  /**
   * Generates the select clause for a load.
   */
  public String generateIdSelect(String id)
  {
    CharBuffer cb = CharBuffer.allocate();

    cb.append(getId().generateSelect(id));

    if (getDiscriminator() != null) {
      cb.append(", ");
      cb.append(getDiscriminator().getName());
    }

    return cb.close();
  }

  /**
   * Generates the update sql.
   */
  public void generateUpdateSQLPrefix(CharBuffer sql)
  {
    sql.append("update " + getTable().getName() + " set ");
  }

  /**
   * Generates the update sql.
   *
   * @param sql the partially built sql
   * @param group the dirty group
   * @param mask the group's mask
   * @param isFirst marks the first set group
   */
  public boolean generateUpdateSQLComponent(CharBuffer sql,
                                            int group,
                                            long mask,
                                            boolean isFirst)
  {
    ArrayList<AmberField> fields = getFields();

    while (mask != 0) {
      int i = 0;
      for (i = 0; (mask & (1L << i)) == 0; i++) {
      }

      mask &= ~(1L << i);

      AmberField field = null;

      for (int j = 0; j < fields.size(); j++) {
        field = fields.get(j);

        if (field.getIndex() == i + group * 64)
          break;
        else
          field = null;
      }

      if (field != null) {

        // jpa/0x00
        if (field instanceof VersionField)
          continue;

        if (! isFirst)
          sql.append(", ");
        isFirst = false;

        field.generateUpdate(sql);
      }
    }

    // jpa/0x00
    for (int j = 0; j < fields.size(); j++) {
      AmberField field = fields.get(j);

      if (field instanceof VersionField) {
        if (! isFirst)
          sql.append(", ");
        isFirst = false;

        field.generateUpdate(sql);
        break;
      }
    }

    return isFirst;
  }

  /**
   * Generates the update sql.
   */
  public void generateUpdateSQLSuffix(CharBuffer sql)
  {
    sql.append(" where ");

    sql.append(getId().generateMatchArgWhere(null));

    // optimistic locking
    if (_versionField != null) {
      sql.append(" and ");
      sql.append(_versionField.generateMatchArgWhere(null));
    }
  }

  /**
   * Generates the update sql.
   */
  public String generateUpdateSQL(long mask)
  {
    if (mask == 0)
      return null;

    CharBuffer sql = CharBuffer.allocate();

    sql.append("update " + getTable().getName() + " set ");

    boolean isFirst = true;

    ArrayList<AmberField> fields = getFields();

    while (mask != 0) {
      int i = 0;
      for (i = 0; (mask & (1L << i)) == 0; i++) {
      }

      mask &= ~(1L << i);

      AmberField field = null;

      for (int j = 0; j < fields.size(); j++) {
        field = fields.get(j);

        if (field.getIndex() == i)
          break;
        else
          field = null;
      }

      if (field != null) {
        if (! isFirst)
          sql.append(", ");
        isFirst = false;

        field.generateUpdate(sql);
      }
    }

    if (isFirst)
      return null;

    sql.append(" where ");

    sql.append(getId().generateMatchArgWhere(null));

    return sql.toString();
  }

  /**
   * Generates code after the remove.
   */
  public void generatePreDelete(JavaWriter out)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generatePreDelete(out);
    }
  }

  /**
   * Generates code after the remove.
   */
  public void generatePostDelete(JavaWriter out)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generatePostDelete(out);
    }
  }

  /**
   * Deletes by the primary key.
   */
  public void delete(AmberConnection aConn, Object key)
    throws SQLException
  {
    getHome().delete(aConn, key);
  }

  /**
   * Deletes by the primary key.
   */
  public void update(Entity entity)
    throws SQLException
  {
    // aConn.addCompletion(_tableCompletion);
  }

  /**
   * Updates global (persistence unit) entity priorities
   * for flushing.
   */
  public int updateFlushPriority(ArrayList<EntityType> updatingEntities)
  {
    // jpa/0h25, jpa/0h26, jpa/0h29, jpa/0j67

    _flushPriority = 0;

    ArrayList<AmberField> fields = getFields();

    for (int i = 0; i < fields.size(); i++) {
      AmberField field = fields.get(i);

      if (field instanceof EntityManyToOneField) {
        EntityManyToOneField manyToOne = (EntityManyToOneField) field;

        EntityType targetRelatedType = manyToOne.getEntityTargetType();

        if (targetRelatedType instanceof EntityType) {
          EntityType targetType = (EntityType) targetRelatedType;

          if (! updatingEntities.contains(targetType)) {
            updatingEntities.add(targetType);
            targetType.updateFlushPriority(updatingEntities);
          }

          int targetPriority = targetType.getFlushPriority();

          if (targetPriority >= _flushPriority) {
            EntityType type = null;

            // jpa/0j67
            if (! manyToOne.isAnnotatedManyToOne()) {
              for (AmberField targetField : targetType.getFields()) {
                if (targetField instanceof EntityManyToOneField) {
                  EntityManyToOneField targetManyToOne = (EntityManyToOneField) targetField;

                  type = targetManyToOne.getEntityTargetType();

                  if (this == type) {
                    if (targetManyToOne.isAnnotatedManyToOne()) {
                      break;
                    }
                  }
                }
              }
            }

            if (this == type)
              continue;

            _flushPriority = targetPriority + 1;
          }
        }
      }
    }

    return _flushPriority;
  }

  /**
   * Returns a completion for the given field.
   */
  public AmberCompletion createManyToOneCompletion(String name,
                                                   Entity source,
                                                   Object newTarget)
  {
    AmberField field = getField(name);

    EntityType parentType = this;

    // jpa/0l40
    while (field == null) {
      parentType = parentType.getParentType();

      if (parentType == null)
        break;

      field = parentType.getField(name);
    }

    if (field instanceof EntityManyToOneField) {
      EntityManyToOneField manyToOne = (EntityManyToOneField) field;

      return getTable().getInvalidateCompletion();
    }
    else
      throw new IllegalStateException();
  }

  /**
   * XXX: temp hack.
   */
  public boolean isEJBProxy(String typeName)
  {
    return (getBeanClass() != getProxyClass() &&
            getProxyClass().getName().equals(typeName));
  }

  //
  // callbacks
  //

  /**
   * Callbacks before an entity is persisted
   */
  public void prePersist(Entity entity)
  {
    for (int i = 0; i < _prePersistCallbacks.size(); i++)
      _prePersistCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks after an entity is persisted
   */
  public void postPersist(Entity entity)
  {
    for (int i = 0; i < _postPersistCallbacks.size(); i++)
      _postPersistCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks before an entity is updateed
   */
  public void preUpdate(Entity entity)
  {
    for (int i = 0; i < _preUpdateCallbacks.size(); i++)
      _preUpdateCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks after an entity is updated
   */
  public void postUpdate(Entity entity)
  {
    for (int i = 0; i < _postUpdateCallbacks.size(); i++)
      _postUpdateCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks before an entity is removeed
   */
  public void preRemove(Entity entity)
  {
    for (int i = 0; i < _preRemoveCallbacks.size(); i++)
      _preRemoveCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks after an entity is removeed
   */
  public void postRemove(Entity entity)
  {
    for (int i = 0; i < _postRemoveCallbacks.size(); i++)
      _postRemoveCallbacks.get(i).invoke(entity);
  }

  /**
   * Callbacks after an entity is loaded
   */
  public void postLoad(Entity entity)
  {
    for (int i = 0; i < _postLoadCallbacks.size(); i++)
      _postLoadCallbacks.get(i).invoke(entity);
  }

  /**
   * Printable version of the entity.
   */
  @Override
  public String toString()
  {
    return getClass().getSimpleName() + "[" + _beanClass.getName() + "]";
  }
}
