/*
 * Copyright (c) 1998-2010 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 Scott Ferguson
 */

package com.caucho.amber.cfg;

import com.caucho.amber.AmberTableCache;
import com.caucho.amber.manager.AmberPersistenceUnit;
import com.caucho.amber.table.AmberTable;
import com.caucho.amber.type.*;
import com.caucho.amber.field.SubId;
import com.caucho.config.ConfigException;
import com.caucho.config.types.Period;
import com.caucho.util.L10N;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import javax.persistence.InheritanceType;
import javax.persistence.AttributeOverrides;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.logging.Logger;
import javax.persistence.AttributeOverride;
import javax.persistence.Basic;
import javax.persistence.Column;
import javax.persistence.DiscriminatorColumn;
import javax.persistence.Embeddable;
import javax.persistence.Entity;
import javax.persistence.IdClass;
import javax.persistence.Inheritance;
import javax.persistence.JoinColumn;
import javax.persistence.MappedSuperclass;
import javax.persistence.PrimaryKeyJoinColumn;
import javax.persistence.SecondaryTable;
import javax.persistence.Table;

/**
 * Configuration for an entity bean
 */
public class EntityIntrospector extends BaseConfigIntrospector {
  private static final L10N L = new L10N(EntityIntrospector.class);
  private static final Logger log
    = Logger.getLogger(EntityIntrospector.class.getName());

  /**
   * Creates the introspector.
   */
  EntityIntrospector(AmberConfigManager manager)
  {
    super(manager);
  }

  /**
   * Returns true for entity type.
   */
  public boolean isEntity(Class type)
  {
    getInternalEntityConfig(type, _annotationCfg);
 
    return (! _annotationCfg.isNull());
  }

  /**
   * Introspects.
   */
  public BeanType introspect(Class type)
    throws ConfigException, SQLException
  {
    BeanType beanType = null;
    
    try {
      EntityType entityType = null;

      EntityType parentType = introspectParent(type.getSuperclass());

      entityType = introspectEntityType(type, parentType);

      if (entityType == null)
	entityType = introspectMappedType(type, parentType);

      if (entityType == null)
	return introspectEmbeddableType(type);
      
      beanType = entityType;
      
      // jpa/0ge2
      entityType.setInstanceClassName(type.getName() + "__ResinExt");
      entityType.setEnhanced(true);

      MappedSuperclassConfig mappedSuperOrEntityConfig
	= introspectEntityConfig(type);
				 
      entityType.setParentType(parentType);

      introspectTable(type, entityType, parentType);

      // inheritance must be after table since it adds columns
      introspectInheritance(type, entityType, parentType);
      
      introspectTableCache(entityType, type);

      getInternalIdClassConfig(type, _annotationCfg);
      IdClass idClassAnn = (IdClass) _annotationCfg.getAnnotation();
      IdClassConfig idClassConfig = _annotationCfg.getIdClassConfig();

      Class idClass = null;
      if (! _annotationCfg.isNull()) {
        if (idClassAnn != null)
          idClass = idClassAnn.value();
        else {
          String s = idClassConfig.getClassName();
          idClass = _persistenceUnit.loadTempClass(s);
        }

        // XXX: temp. introspects idClass as an embeddable type.
	_persistenceUnit.addEntityClass(idClass.getName(), idClass);

        // jpa/0i49 vs jpa/0i40
        // embeddable.setFieldAccess(isField);
      }

      if (entityType.getId() != null) {
      }
      else if (entityType.isFieldAccess())
        introspectIdField(_persistenceUnit, entityType, parentType,
                          type, idClass, mappedSuperOrEntityConfig);
      else {
        introspectIdMethod(_persistenceUnit, entityType, parentType,
                           type, idClass, mappedSuperOrEntityConfig);
      }

      HashMap<String, IdConfig> idMap = null;

      AttributesConfig attributes = null;

      if (mappedSuperOrEntityConfig != null) {
        attributes = mappedSuperOrEntityConfig.getAttributes();

        if (attributes != null)
          idMap = attributes.getIdMap();
      }

      // if ((idMap == null) || (idMap.size() == 0)) {
      //   idMap = entityType.getSuperClass();
      // }

      if (entityType.isEntity() && (entityType.getId() == null) && ((idMap == null) || (idMap.size() == 0)))
        throw new ConfigException(L.l("{0} does not have any primary keys.  Entities must have at least one @Id or exactly one @EmbeddedId field.",
                                      entityType.getName()));

      // Introspect overridden attributes. (jpa/0ge2)
      introspectAttributeOverrides(entityType, type);

      introspectSecondaryTable(entityType, type);

      if (entityType.isFieldAccess())
        introspectFields(_persistenceUnit, entityType, parentType, type,
                         mappedSuperOrEntityConfig, false);
      else
        introspectMethods(_persistenceUnit, entityType, parentType, type,
                          mappedSuperOrEntityConfig);

      introspectCallbacks(type, entityType);
      

      // Adds entity listeners, if any.
      introspectEntityListeners(type, entityType, _persistenceUnit);

      // Adds sql result set mappings, if any.
      introspectSqlResultSetMappings(type, entityType, entityType.getName());

      // Adds named queries, if any.
      introspectNamedQueries(type, entityType.getName());
      introspectNamedNativeQueries(type, entityType.getName());
    } catch (ConfigException e) {
      if (beanType != null)
        beanType.setConfigException(e);

      throw e;
    } catch (SQLException e) {
      if (beanType != null)
        beanType.setConfigException(e);

      throw e;
    } catch (RuntimeException e) {
      if (beanType != null)
        beanType.setConfigException(e);

      throw e;
    }

    return beanType;
  }

  private EntityType introspectEntityType(Class type,
					  EntityType parentType)
    throws SQLException
  {
    getInternalEntityConfig(type, _annotationCfg);
      
    Entity entityAnn = (Entity) _annotationCfg.getAnnotation();
    EntityConfig entityConfig = _annotationCfg.getEntityConfig();

    boolean isEntity = ! _annotationCfg.isNull();

    if (! isEntity)
      return null;
    
    EntityType entityType;
    String typeName;
    
    if (entityConfig != null)
      typeName = entityConfig.getClassName();
    else
      typeName = entityAnn.name();

    // Validates the type
    String entityName;
    Inheritance inheritanceAnn = null;
    InheritanceConfig inheritanceConfig = null;
    Class rootClass = type;
    Entity rootEntityAnn = null;
    EntityConfig rootEntityConfig = null;

    validateType(type, true);

      // jpa/0ge2
      // if (hasInheritance) {

    if (entityConfig == null)
      entityName = entityAnn.name();
    else {
      entityName = entityConfig.getClassName();

      int p = entityName.lastIndexOf('.');

      if (p > 0)
	entityName = entityName.substring(p + 1);
    }

    if ((entityName == null) || "".equals(entityName)) {
      entityName = type.getSimpleName();
    }

    entityType = _persistenceUnit.createEntity(entityName, type);

    _configManager.addType(type, new EntityConfig(type.getName(), this, entityType));

    boolean isField = isField(type, entityConfig, false);

    if (isField)
      entityType.setFieldAccess(true);

    return entityType;
  }

  private EntityType introspectMappedType(Class type, EntityType parentType)
    throws SQLException
  {
    EntityType entityType;

    boolean isMappedSuperclass = false;
    MappedSuperclass mappedSuperAnn = null;
    MappedSuperclassConfig mappedSuperConfig = null;

    String typeName;

    MappedSuperclassConfig mappedSuperOrEntityConfig = null;

    getInternalMappedSuperclassConfig(type, _annotationCfg);
    mappedSuperAnn = (MappedSuperclass) _annotationCfg.getAnnotation();
    mappedSuperConfig = _annotationCfg.getMappedSuperclassConfig();

    isMappedSuperclass = ! _annotationCfg.isNull();

    if (! isMappedSuperclass)
      return null;
    
    mappedSuperOrEntityConfig = mappedSuperConfig;

    if (mappedSuperConfig != null)
      typeName = mappedSuperConfig.getClassName();
/*    else
      typeName = mappedSuperAnn.name();
*/
    // Validates the type
    String entityName;
    Inheritance inheritanceAnn = null;
    InheritanceConfig inheritanceConfig = null;
    Class rootClass = type;
    Entity rootEntityAnn = null;
    EntityConfig rootEntityConfig = null;

    validateType(type, false);

    // jpa/0ge2
    // if (hasInheritance) {

    if (mappedSuperConfig == null)
      entityName = null;//mappedSuperAnn.name();
    else {
      entityName = mappedSuperConfig.getSimpleClassName();
    }

    if ((entityName == null) || "".equals(entityName)) {
      entityName = type.getSimpleName();
    }

    entityType = _persistenceUnit.createMappedSuperclass(entityName, type);

    _configManager.addType(type, new EntityConfig(type.getName(), this, entityType));

    boolean isField = isField(type, mappedSuperOrEntityConfig, false);

    if (isField)
      entityType.setFieldAccess(true);

    // jpa/0ge2
    entityType.setInstanceClassName(type.getName() + "__ResinExt");
    entityType.setEnhanced(true);

    return entityType;
  }

  /**
   * Introspects.
   */
  private EmbeddableType introspectEmbeddableType(Class type)
    throws ConfigException, SQLException
  {
    getInternalEmbeddableConfig(type, _annotationCfg);
    Embeddable embeddableAnn = (Embeddable) _annotationCfg.getAnnotation();
    EmbeddableConfig embeddableConfig = _annotationCfg.getEmbeddableConfig();

    /*
    if (_annotationCfg.isNull())
      return null;
    */

    String typeName = type.getName();
    EmbeddableType embeddableType
      = _persistenceUnit.createEmbeddable(typeName, type);

    _configManager.addType(type, new EmbeddableConfig(type.getName(), this, embeddableType));

    try {
      boolean isField = isField(type, embeddableConfig);

      if (isField)
        embeddableType.setFieldAccess(true);

      // XXX: jpa/0u21
      Embeddable ann = (Embeddable) type.getAnnotation(javax.persistence.Embeddable.class);

      if (ann == null) {
        isField = true;
        embeddableType.setIdClass(true);
      
	_persistenceUnit.getAmberContainer().addEmbeddable(typeName,
							   embeddableType);
      }

      embeddableType.setInstanceClassName(type.getName() +
                                          "__ResinExt");
      embeddableType.setEnhanced(true);

      if (isField)
        introspectFields(_persistenceUnit, embeddableType, null,
                         type, embeddableConfig, true);
      else
        introspectMethods(_persistenceUnit, embeddableType, null,
                          type, embeddableConfig);

    } catch (ConfigException e) {
      if (embeddableType != null)
	embeddableType.setConfigException(e);

      throw e;
    } catch (RuntimeException e) {
      if (embeddableType != null)
	embeddableType.setConfigException(e);

      throw e;
    }

    return embeddableType;
  }

  private MappedSuperclassConfig introspectEntityConfig(Class type)
    throws SQLException
  {
    getInternalEntityConfig(type, _annotationCfg);
      
    if (! _annotationCfg.isNull())
      return _annotationCfg.getEntityConfig();
    
    getInternalMappedSuperclassConfig(type, _annotationCfg);

    return _annotationCfg.getMappedSuperclassConfig();
  }

  private EntityType introspectParent(Class parentClass)
    throws SQLException
  {
    if (parentClass == null)
      return null;

    getInternalEntityConfig(parentClass, _annotationCfg);

    if (! _annotationCfg.isNull())
      return (EntityType) _configManager.introspect(parentClass);
    else if (parentClass.isAnnotationPresent(javax.persistence.Entity.class))
      return (EntityType) _configManager.introspect(parentClass);
    else if (parentClass.isAnnotationPresent(javax.persistence.MappedSuperclass.class))
      return (EntityType) _configManager.introspect(parentClass);
    else
      return null;
  }



  private void introspectInheritance(Class type,
				     EntityType entityType,
				     EntityType parentType)
    throws SQLException
  {
    // Inheritance annotation/configuration is specified
    // on the entity class that is the root of the entity
    // class hierarachy.
    InheritanceConfig inheritanceConfig;
    Inheritance inheritanceAnn;

    getInternalInheritanceConfig(type, _annotationCfg);
    inheritanceAnn = (Inheritance) _annotationCfg.getAnnotation();
    inheritanceConfig = _annotationCfg.getInheritanceConfig();

    boolean hasInheritance = ! _annotationCfg.isNull();

    if (! hasInheritance &&
	(parentType == null || ! parentType.isEntity()))
      return;

    introspectDiscriminatorValue(type, entityType);
    
    if (parentType != null) {
      if (hasInheritance)
	throw new ConfigException(L.l("'{0}' cannot have @Inheritance. It must be specified on the entity class that is the root of the entity class hierarchy.",
				      type));
      
      EntityType rootType = entityType.getRootType();
      rootType.addSubClass(entityType);

      getInternalPrimaryKeyJoinColumnConfig(type, _annotationCfg);
      PrimaryKeyJoinColumn joinAnn
	= (PrimaryKeyJoinColumn) _annotationCfg.getAnnotation();
      PrimaryKeyJoinColumnConfig primaryKeyJoinColumnConfig
	= _annotationCfg.getPrimaryKeyJoinColumnConfig();

      if (rootType.isJoinedSubClass()) {
        linkInheritanceTable(rootType.getTable(),
                             entityType.getTable(),
                             joinAnn,
                             primaryKeyJoinColumnConfig);

        entityType.setId(new SubId(entityType, rootType));
      }

      return;
    }
    
    if (! hasInheritance)
      return;

    if ((inheritanceAnn != null) || (inheritanceConfig != null))
      introspectInheritance(_persistenceUnit, entityType, type,
			    inheritanceAnn, inheritanceConfig);
  }

  private void introspectDiscriminatorValue(Class type, EntityType entityType)
  {
    DiscriminatorValue discValueAnn = (DiscriminatorValue) type.getAnnotation(DiscriminatorValue.class);

    String discriminatorValue = null;

    if (discValueAnn != null)
      discriminatorValue = discValueAnn.value();

    if (discriminatorValue == null || discriminatorValue.equals("")) {
      String name = entityType.getBeanClass().getSimpleName();

      discriminatorValue = name;
    }

    entityType.setDiscriminatorValue(discriminatorValue);
  }

  /**
   * Introspects the Inheritance
   */
  void introspectInheritance(AmberPersistenceUnit persistenceUnit,
                             EntityType entityType,
                             Class type,
                             Inheritance inheritanceAnn,
                             InheritanceConfig inheritanceConfig)
    throws ConfigException, SQLException
  {
    InheritanceType strategy;

    if (inheritanceAnn != null)
      strategy = inheritanceAnn.strategy();
    else
      strategy = inheritanceConfig.getStrategy();

    switch (strategy) {
    case JOINED:
      entityType.setJoinedSubClass(true);
      break;
    }

    getInternalDiscriminatorColumnConfig(type, _annotationCfg);
    DiscriminatorColumn discriminatorAnn = (DiscriminatorColumn) _annotationCfg.getAnnotation();
    DiscriminatorColumnConfig discriminatorConfig = _annotationCfg.getDiscriminatorColumnConfig();

    String columnName = null;

    if (discriminatorAnn != null)
      columnName = discriminatorAnn.name();

    if (columnName == null || columnName.equals(""))
      columnName = "DTYPE";

    AmberType columnType = null;
    DiscriminatorType discType = DiscriminatorType.STRING;

    if (discriminatorAnn != null)
      discType = discriminatorAnn.discriminatorType();

    switch (discType) {
    case STRING:
      columnType = StringType.create();
      break;
    case CHAR:
      columnType = PrimitiveCharType.create();
      break;
    case INTEGER:
      columnType = PrimitiveIntType.create();
      break;
    default:
      throw new IllegalStateException();
    }

    AmberTable table = entityType.getTable();

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

    com.caucho.amber.table.AmberColumn column
      = table.createColumn(columnName, columnType);

    if (discriminatorAnn != null) {
//      column.setNotNull(! discriminatorAnn.nullable());

      column.setLength(discriminatorAnn.length());

      if (! "".equals(discriminatorAnn.columnDefinition()))
        column.setSQLType(discriminatorAnn.columnDefinition());
    }
    else {
      column.setNotNull(true);
      column.setLength(10);
    }

    entityType.setDiscriminator(column);
  }

  private void introspectTable(Class type,
                               EntityType entityType,
			       EntityType parentType)
  {
    // XXX: need better test
    boolean isEntity = ! (entityType instanceof MappedSuperclassType);
    
    AmberTable table = null;

    getInternalTableConfig(type, _annotationCfg);
    Table tableAnn = (Table) _annotationCfg.getAnnotation();
    TableConfig tableConfig = _annotationCfg.getTableConfig();

    String tableName = null;

    if (tableAnn != null)
      tableName = tableAnn.name();
    else if (tableConfig != null)
      tableName = tableConfig.getName();

    // jpa/0gg0, jpa/0gg2
    if (isEntity) { // && ! type.isAbstract()) {

      boolean hasTableConfig = true;

      if (tableName == null || tableName.equals("")) {
	hasTableConfig = false;
	tableName = toSqlName(entityType.getName());
      }

      /*
      InheritanceType strategy = null;
      
      if (parentType != null)
        strategy = parentType.getInheritanceStrategy();

      if (inheritanceAnn != null)
	strategy = (InheritanceType) inheritanceAnn.get("strategy");
      else if (inheritanceConfig != null)
	strategy = inheritanceConfig.getStrategy();
       */

      // jpa/0gg2
      if (! entityType.isEntity())
        return;
      /*
      if (type.isAbstract()
	  && strategy != InheritanceType.JOINED
	  && ! hasTableConfig) {
	// jpa/0gg0
      }
       */
      else if (parentType == null || parentType.getTable() == null) {
	entityType.setTable(_persistenceUnit.createTable(tableName));
      }
      else if (parentType.isJoinedSubClass()) {
	entityType.setTable(_persistenceUnit.createTable(tableName));

	EntityType rootType = parentType.getRootType();

	Class rootClass = rootType.getBeanClass();
	getInternalTableConfig(rootClass, _annotationCfg);
	Table rootTableAnn = (Table) _annotationCfg.getAnnotation();
	TableConfig rootTableConfig = _annotationCfg.getTableConfig();

	String rootTableName = null;

	if (rootTableAnn != null)
	  rootTableName = rootTableAnn.name();
	else if (rootTableConfig != null)
	  rootTableName = rootTableConfig.getName();

	if (rootTableName == null || rootTableName.equals("")) {
	  String rootEntityName = rootType.getName();

	  rootTableName = toSqlName(rootEntityName);
	}

	entityType.setRootTableName(rootTableName);
      }
      else
	entityType.setTable(parentType.getTable());
    }
  }

  private void introspectSecondaryTable(EntityType entityType, Class type)
  {
    getInternalSecondaryTableConfig(type, _annotationCfg);
    SecondaryTable secondaryTableAnn = (SecondaryTable) _annotationCfg.getAnnotation();
    SecondaryTableConfig secondaryTableConfig = _annotationCfg.getSecondaryTableConfig();

    AmberTable secondaryTable = null;

    if ((secondaryTableAnn != null) || (secondaryTableConfig != null)) {
      String secondaryName;

      if (secondaryTableAnn != null)
	secondaryName = secondaryTableAnn.name();
      else
	secondaryName = secondaryTableConfig.getName();

      secondaryTable = _persistenceUnit.createTable(secondaryName);

      entityType.addSecondaryTable(secondaryTable);

      // XXX: pk
    }

    if (secondaryTableAnn != null) {
      PrimaryKeyJoinColumn[] joinAnn = secondaryTableAnn.pkJoinColumns();

      linkSecondaryTable(entityType.getTable(),
			 secondaryTable,
			 joinAnn);
    }
  }

  private void introspectTableCache(EntityType entityType, Class type)
  {
    AmberTableCache tableCache = (AmberTableCache) type.getAnnotation(AmberTableCache.class);

    if (tableCache != null) {
      entityType.getTable().setReadOnly(tableCache.readOnly());

      long cacheTimeout = Period.toPeriod(tableCache.timeout());
      entityType.getTable().setCacheTimeout(cacheTimeout);
    }
  }

  private void introspectAttributeOverrides(EntityType entityType,
                                            Class type)
  {
    EntityType parent = entityType.getParentType();

    if (parent == null)
      return;

    boolean isAbstract = Modifier.isAbstract(parent.getBeanClass().getModifiers());

    if (parent.isEntity() && ! isAbstract)
      return;

    HashMap<String,ColumnConfig> overrideMap
      = new HashMap<String,ColumnConfig>();
    
    getInternalAttributeOverrideConfig(type, _annotationCfg);
    AttributeOverride attributeOverrideAnn = (AttributeOverride) _annotationCfg.getAnnotation();

    boolean hasAttributeOverride = (attributeOverrideAnn != null);

    AttributeOverrides attributeOverridesAnn
      = (AttributeOverrides) type.getAnnotation(AttributeOverrides.class);

    ArrayList<AttributeOverrideConfig> attributeOverrideList = null;

    EntityConfig entityConfig = getEntityConfig(type.getName());

    if (entityConfig != null)
      attributeOverrideList = entityConfig.getAttributeOverrideList();

    boolean hasAttributeOverrides = false;

    if ((attributeOverrideList != null) &&
	(attributeOverrideList.size() > 0)) {
      hasAttributeOverrides = true;
    }
    else if (attributeOverridesAnn != null)
      hasAttributeOverrides = true;

    if (hasAttributeOverride && hasAttributeOverrides)
      throw new ConfigException(L.l("{0} may not have both @AttributeOverride and @AttributeOverrides",
				    type));

    if (attributeOverrideList == null)
      attributeOverrideList = new ArrayList<AttributeOverrideConfig>();

    if (hasAttributeOverride) {
      // Convert annotation to configuration.

      AttributeOverrideConfig attOverrideConfig
	= convertAttributeOverrideAnnotationToConfig(attributeOverrideAnn);

      attributeOverrideList.add(attOverrideConfig);
    }
    else if (hasAttributeOverrides) {
      if (attributeOverrideList.size() > 0) {
	// OK: attributeOverrideList came from orm.xml
      }
      else {
	// Convert annotations to configurations.

	AttributeOverride attOverridesAnn[]
	  = attributeOverridesAnn.value();

	AttributeOverrideConfig attOverrideConfig;

        /* XXX:
	for (int i = 0; i < attOverridesAnn.length; i++) {
	  attOverrideConfig
	    = convertAttributeOverrideAnnotationToConfig((JAnnotation) attOverridesAnn[i]);

	  attributeOverrideList.add(attOverrideConfig);
	}
         * */
      }
    }

    for (AttributeOverrideConfig override : attributeOverrideList) {
      overrideMap.put(override.getName(), override.getColumn());
    }

    _depCompletions.add(new AttributeOverrideCompletion(this, entityType, type,
							overrideMap));
  }

  boolean isField(Class type,
                  AbstractEnhancedConfig typeConfig)
    throws ConfigException
  {
    for (Method method : type.getDeclaredMethods()) {
      Annotation ann[] = method.getDeclaredAnnotations();

      for (int i = 0; ann != null && i < ann.length; i++) {
	if (ann[i] instanceof Basic || ann[i] instanceof Column)
	  return false;
      }
    }

    return true;
  }

  private boolean isPropertyAnnotation(Class cl)
  {
    return (Basic.class.equals(cl)
	    || Column.class.equals(cl));
  }
}
