/*
 * Copyright (c) 1998-2012 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.field.*;
import com.caucho.amber.manager.AmberPersistenceUnit;
import com.caucho.amber.table.AmberColumn;
import com.caucho.amber.table.AmberTable;
import com.caucho.config.ConfigException;
import com.caucho.java.JavaWriter;
import com.caucho.make.ClassDependency;
import com.caucho.util.L10N;
import com.caucho.vfs.PersistentDependency;

import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.logging.Logger;

/**
 * Represents a stateful type:
 * embeddable, entity or mapped-superclass.
 */
abstract public class AmberBeanType extends AbstractEnhancedType
{
  private static final Logger log = Logger.getLogger(AmberBeanType.class.getName());
  private static final L10N L = new L10N(AmberBeanType.class);

  private boolean _isFieldAccess;

  // fields declared on this class
  private ArrayList<AmberField> _selfFields = new ArrayList<AmberField>();

  private volatile boolean _isConfigured;

  private ArrayList<PersistentDependency> _dependencies
    = new ArrayList<PersistentDependency>();

  private HashMap<String,String> _completionFields
    = new HashMap<String,String>();

  private AmberColumn _discriminator;

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

  public boolean isEntity()
  {
    return false;
  }

  /**
   * Set true for field-access.
   */
  public void setFieldAccess(boolean isFieldAccess)
  {
    _isFieldAccess = isFieldAccess;
  }

  /**
   * Set true for field-access.
   */
  public boolean isFieldAccess()
  {
    return _isFieldAccess;
  }

  /**
   * Returns true for an embeddable
   */
  public boolean isEmbeddable()
  {
    return false;
  }

  /**
   * Returns the discriminator.
   */
  public AmberColumn getDiscriminator()
  {
    return _discriminator;
  }

  /**
   * Sets the discriminator.
   */
  public void setDiscriminator(AmberColumn discriminator)
  {
    _discriminator = discriminator;
  }

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

  /**
   * Adds a new field.
   */
  public void addField(AmberField field)
  {
    _selfFields.add(field);
    Collections.sort(_selfFields, new AmberFieldCompare());
  }

  /**
   * Returns the fields declared on this instance
   */
  public ArrayList<AmberField> getSelfFields()
  {
    return _selfFields;
  }

  /**
   * Returns the fields.
   */
  public ArrayList<AmberField> getFields()
  {
    return _selfFields;
  }

  /**
   * Returns the field with a given name.
   */
  public AmberField getField(String name)
  {
    for (AmberField field : getFields()) {
      if (field.getName().equals(name))
        return field;
    }

    return null;
  }

  /**
   * Sets the bean class.
   */
  @Override
  public void setBeanClass(Class beanClass)
  {
    super.setBeanClass(beanClass);

    addDependency(_tBeanClass);
  }

  /**
   * Adds a dependency.
   */
  public void addDependency(Class cl)
  {
    addDependency(new ClassDependency(cl));
  }

  /**
   * Adds a dependency.
   */
  public void addDependency(PersistentDependency depend)
  {
    if (! _dependencies.contains(depend))
      _dependencies.add(depend);
  }

  /**
   * Gets the dependency.
   */
  public ArrayList<PersistentDependency> getDependencies()
  {
    return _dependencies;
  }

  /**
   * Adds a new completion field.
   */
  public void addCompletionField(String name)
  {
    _completionFields.put(name, name);
  }

  /**
   * Returns true if and only if it has the completion field.
   */
  public boolean containsCompletionField(String completionField)
  {
    return _completionFields.containsKey(completionField);
  }

  /**
   * Remove all completion fields.
   */
  public void removeAllCompletionFields()
  {
    _completionFields.clear();
  }

  /**
   * Set true if configured.
   */
  public boolean startConfigure()
  {
    synchronized (this) {
      if (_isConfigured)
        return false;

      _isConfigured = true;

      return true;
    }
  }

  /**
   * Initialize the type.
   */
  @Override
  public void init()
    throws ConfigException
  {
    // jpa/0l14, jpa/0ge3
    for (AmberField field : _selfFields) {
      // ejb/0602
      if (getPersistenceUnit().isJPA()) {
        if (field instanceof ManyToOneField)
          ((ManyToOneField) field).init((EntityType) this);
      }
    }
  }

  /**
   * Converts the value.
   */
  @Override
  public String generateCastFromObject(String value)
  {
    return "((" + getInstanceClassName() + ") " + value + ")";
  }

  /**
   * Generates the select clause for a load.
   */
  public void generateLoadSelect(StringBuilder sb,
                                 AmberTable table,
                                 String id,
                                 int loadGroup)
  {
    // jpa/0l14, jpa/0ge3
    for (AmberField field : getFields()) {
      // jpa/0gg3
      if (field.getLoadGroupIndex() == loadGroup) {
        String propSelect = field.generateLoadSelect(table, id);

        if (propSelect != null) {
          if (sb.length() > 0)
            sb.append(", ");

          sb.append(propSelect);
        }
      }
    }
  }

  /**
   * Generates a string to load the field.
   */
  public int generateLoad(JavaWriter out,
                          String rs,
                          String indexVar,
                          int index,
                          int loadGroupIndex)
    throws IOException
  {
    if (getDiscriminator() != null) {
      EntityType parent = null;

      if (this instanceof EntityType)
        parent = ((EntityType) this).getParentType();

      boolean isAbstractParent = getPersistenceUnit().isJPA()
        && (parent == null 
            || Modifier.isAbstract(parent.getBeanClass().getModifiers()));

      if (loadGroupIndex == 0 || isAbstractParent)
        index++;
    }

    for (AmberField field : getFields()) {
      // jpa/0gg3
      if (field.getLoadGroupIndex() == loadGroupIndex)
        index = field.generateLoad(out, rs, indexVar, index);
    }

    return index;
  }

  /**
   * Generates the select clause for a load.
   */
  abstract public String generateLoadSelect(AmberTable table, String id);

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

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

  /**
   * Returns the load mask generated on create.
   */
  public void generatePrePersist(JavaWriter out)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generatePrePersist(out);
    }
  }

  /**
   * Generates the foreign delete
   */
  public void generateInvalidateForeign(JavaWriter out)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generateInvalidateForeign(out);
    }
  }

  /**
   * Generates any expiration code.
   */
  public void generateExpire(JavaWriter out)
    throws IOException
  {
    for (AmberField field : getFields()) {
      field.generateExpire(out);
    }
  }

  /**
   * Gets a matching getter.
   */
  public Method getGetter(String name)
  {
    return getGetter(_tBeanClass, name);
  }

  /**
   * Gets a matching getter.
   */
  public static Method getGetter(Class cl, String name)
  {
    Method []methods = cl.getMethods();

    for (int i = 0; i < methods.length; i++) {
      Class []param = methods[i].getParameterTypes();
      String methodName = methods[i].getName();

      if (name.equals(methodName) && param.length == 0)
        return methods[i];
    }

    cl = cl.getSuperclass();

    if (cl != null)
      return getGetter(cl, name);
    else
      return null;
  }

  /**
   * Gets a matching getter.
   */
  public static Field getField(Class cl, String name)
  {
    Field []fields = cl.getDeclaredFields();

    for (int i = 0; i < fields.length; i++) {
      if (name.equals(fields[i].getName()))
        return fields[i];
    }

    cl = cl.getSuperclass();

    if (cl != null)
      return getField(cl, name);
    else
      return null;
  }

  /**
   * Gets a matching getter.
   */
  public static Method getSetter(Class cl, String name)
  {
    Method []methods = cl.getMethods();

    for (int i = 0; i < methods.length; i++) {
      Class []param = methods[i].getParameterTypes();
      String methodName = methods[i].getName();

      if (name.equals(methodName) && param.length == 1)
        return methods[i];
    }

    cl = cl.getSuperclass();

    if (cl != null)
      return getSetter(cl, name);
    else
      return null;
  }

  /**
   * Returns the load mask generated on create.
   */
  public long getCreateLoadMask(int group)
  {
    long mask = 0;

    for (AmberField field : getFields()) {
      mask |= field.getCreateLoadMask(group);
    }

    return mask;
  }
}
