/*
 * 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 Scott Ferguson
 */

package com.caucho.quercus.program;

import com.caucho.quercus.QuercusContext;
import com.caucho.quercus.QuercusModuleException;
import com.caucho.quercus.QuercusException;
import com.caucho.quercus.QuercusRuntimeException;
import com.caucho.quercus.annotation.*;
import com.caucho.quercus.env.*;
import com.caucho.quercus.expr.Expr;
import com.caucho.quercus.expr.LiteralExpr;
import com.caucho.quercus.function.AbstractFunction;
import com.caucho.quercus.lib.spl.Serializable;
import com.caucho.quercus.marshal.JavaMarshal;
import com.caucho.quercus.marshal.Marshal;
import com.caucho.quercus.marshal.MarshalFactory;
import com.caucho.quercus.module.ModuleContext;
import com.caucho.util.L10N;
import com.caucho.vfs.WriteStream;

import java.io.IOException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.net.URL;
import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Represents an introspected Java class.
 */
public class JavaClassDef extends ClassDef implements InstanceInitializer {
  private final static Logger log
    = Logger.getLogger(JavaClassDef.class.getName());
  private final static L10N L = new L10N(JavaClassDef.class);

  private final ModuleContext _moduleContext;

  private final String _name;
  private final Class<?> _type;

  private QuercusClass _quercusClass;

  private HashSet<String> _instanceOfSet;
  private HashSet<String> _instanceOfSetLowerCase;

  private final boolean _isAbstract;
  private final boolean _isInterface;
  private final boolean _isDelegate;
  private boolean _isPhpClass;

  private String _resourceType;

  private JavaClassDef _componentDef;

  protected volatile boolean _isInit;

  private final HashMap<String, Value> _constMap
    = new HashMap<String, Value>();

  private final HashMap<String, Object> _constJavaMap
    = new HashMap<String, Object>();

  private final MethodMap<AbstractJavaMethod> _functionMap
    = new MethodMap<AbstractJavaMethod>(null, this);

  private final HashMap<String, AbstractJavaMethod> _getMap
    = new HashMap<String, AbstractJavaMethod>();

  private final HashMap<String, AbstractJavaMethod> _setMap
    = new HashMap<String, AbstractJavaMethod>();

  // _fieldMap stores all public non-static fields
  // used by getField and setField
  private final HashMap<String, FieldMarshalPair> _fieldMap
    = new HashMap<String, FieldMarshalPair> ();

  private AbstractJavaMethod _cons;
  private AbstractJavaMethod __construct;
  private AbstractJavaMethod __destruct;

  private JavaMethod __fieldGet;
  private JavaMethod __fieldSet;

  private FunctionArrayDelegate _funArrayDelegate;
  private ArrayDelegate _arrayDelegate;

  private JavaMethod __call;
  private JavaMethod __callStatic;

  private JavaMethod __toString;

  private Method _printRImpl;
  private Method _varDumpImpl;
  private Method _jsonEncode;
  private Method _entrySet;

  private TraversableDelegate _traversableDelegate;
  private CountDelegate _countDelegate;

  private AbstractFunction _serializeFun;
  private AbstractFunction _unserializeFun;

  private Method _iteratorMethod;

  private Marshal _marshal;

  private String _extension;

  public JavaClassDef(ModuleContext moduleContext, String name, Class<?> type)
  {
    super(null, name, null,
          ClassDef.NULL_STRING_ARRAY, ClassDef.NULL_STRING_ARRAY);

    _moduleContext = moduleContext;
    _name = name;
    _type = type;

    _isAbstract = Modifier.isAbstract(type.getModifiers());
    _isInterface = type.isInterface();
    _isDelegate = type.isAnnotationPresent(ClassImplementation.class);

    if (type.isArray() && ! isArray()) {
      throw new IllegalStateException(L.l("'{0}' needs to be called with JavaArrayClassDef", type));
    }
  }

  public JavaClassDef(ModuleContext moduleContext,
                      String name,
                      Class<?> type,
                      String extension)
  {
    this(moduleContext, name, type);

    _extension = extension;

    moduleContext.addExtensionClass(extension, name);
  }

  private void fillInstanceOfSet(Class<?> type, boolean isTop)
  {
    if (type == null)
      return;

    if (isTop && _isDelegate) {
      _instanceOfSet.add(_name);
      _instanceOfSetLowerCase.add(_name.toLowerCase(Locale.ENGLISH));
    }
    else {
      String name = type.getSimpleName();

      _instanceOfSet.add(name);
      _instanceOfSetLowerCase.add(name.toLowerCase(Locale.ENGLISH));
    }

    fillInstanceOfSet(type.getSuperclass(), false);

    Class<?> []ifaceList = type.getInterfaces();
    if (ifaceList != null) {
      for (Class<?> iface : ifaceList)
        fillInstanceOfSet(iface, false);
    }
  }

  public static JavaClassDef create(ModuleContext moduleContext,
                                    String name, Class<?> type)
  {
    if (Double.class.isAssignableFrom(type)
        || Float.class.isAssignableFrom(type))
      return new DoubleClassDef(moduleContext);
    else if (Long.class.isAssignableFrom(type)
             || Integer.class.isAssignableFrom(type)
             || Short.class.isAssignableFrom(type)
             || Byte.class.isAssignableFrom(type))
      return new LongClassDef(moduleContext);
    else if (BigDecimal.class.isAssignableFrom(type))
      return new BigDecimalClassDef(moduleContext);
    else if (BigInteger.class.isAssignableFrom(type))
      return new BigIntegerClassDef(moduleContext);
    else if (String.class.isAssignableFrom(type)
             || Character.class.isAssignableFrom(type))
      return new StringClassDef(moduleContext);
    else if (Boolean.class.isAssignableFrom(type))
      return new BooleanClassDef(moduleContext);
    else if (Calendar.class.isAssignableFrom(type))
      return new CalendarClassDef(moduleContext);
    else if (Date.class.isAssignableFrom(type))
      return new DateClassDef(moduleContext);
    else if (URL.class.isAssignableFrom(type))
      return new URLClassDef(moduleContext);
    else if (Map.class.isAssignableFrom(type))
      return new JavaMapClassDef(moduleContext, name, type);
    else if (List.class.isAssignableFrom(type))
      return new JavaListClassDef(moduleContext, name, type);
    else if (Collection.class.isAssignableFrom(type)
             && ! Queue.class.isAssignableFrom(type))
      return new JavaCollectionClassDef(moduleContext, name, type);
    else
      return null;
  }

  /**
   * Returns the class name.
   */
  @Override
  public String getName()
  {
    return _name;
  }

  /**
   * Returns the class name.
   */
  public String getSimpleName()
  {
    return _type.getSimpleName();
  }

  public Class<?> getType()
  {
    return _type;
  }

  /**
   * Returns the type of this resource.
   */
  public String getResourceType()
  {
    return _resourceType;
  }

  protected ModuleContext getModuleContext()
  {
    return _moduleContext;
  }

  /**
   * Returns the name of the extension that this class is part of.
   */
  @Override
  public String getExtension()
  {
    return _extension;
  }

  @Override
  public boolean isA(Env env, String name)
  {
    if (_instanceOfSet == null) {
      _instanceOfSet = new HashSet<String>();
      _instanceOfSetLowerCase = new HashSet<String>();

      fillInstanceOfSet(_type, true);
    }

    return (_instanceOfSet.contains(name)
            || _instanceOfSetLowerCase.contains(name.toLowerCase(Locale.ENGLISH)));
  }

  /**
   * Adds the interfaces to the set
   */
  @Override
  public void addInterfaces(HashSet<String> interfaceSet)
  {
    addInterfaces(interfaceSet, _type, true);
  }

  protected void addInterfaces(HashSet<String> interfaceSet,
                               Class<?> type,
                               boolean isTop)
  {
    if (type == null)
      return;

    interfaceSet.add(_name.toLowerCase(Locale.ENGLISH));
    interfaceSet.add(type.getSimpleName().toLowerCase(Locale.ENGLISH));

    if (type.getInterfaces() != null) {
      for (Class<?> iface : type.getInterfaces()) {
        addInterfaces(interfaceSet, iface, false);
      }
    }

    // php/1z21
    addInterfaces(interfaceSet, type.getSuperclass(), false);
  }

  private boolean hasInterface(String name, Class<?> type)
  {
    Class<?>[] interfaces = type.getInterfaces();

    if (interfaces != null) {
      for (Class<?> intfc : interfaces) {
        if (intfc.getSimpleName().equalsIgnoreCase(name))
          return true;

        if (hasInterface(name, intfc))
          return true;
      }
    }

    return false;
  }

  @Override
  public boolean isAbstract()
  {
    return _isAbstract;
  }

  public boolean isArray()
  {
    return false;
  }

  @Override
  public boolean isInterface()
  {
    return _isInterface;
  }

  public boolean isDelegate()
  {
    return _isDelegate;
  }

  public void setPhpClass(boolean isPhpClass)
  {
    _isPhpClass = isPhpClass;
  }

  public boolean isPhpClass()
  {
    return _isPhpClass;
  }

  public JavaClassDef getComponentDef()
  {
    if (_componentDef == null) {
      Class<?> compType = getType().getComponentType();
      _componentDef = _moduleContext.getJavaClassDefinition(compType.getName());
    }

    return _componentDef;
  }

  public Value wrap(Env env, Object obj)
  {
    if (! _isInit)
      init();

    if (_resourceType != null)
      return new JavaResourceValue(env, obj, this);
    else
      return new JavaValue(env, obj, this);
  }

  private int cmpObject(Object lValue, Object rValue)
  {
    if (lValue == rValue)
      return 0;

    if (lValue == null)
      return -1;

    if (rValue == null)
      return 1;

    if (lValue instanceof Comparable) {
      if (!(rValue instanceof Comparable))
        return -1;

      return ((Comparable) lValue).compareTo(rValue);
    }
    else if (rValue instanceof Comparable) {
      return 1;
    }

    if (lValue.equals(rValue))
      return 0;

    String lName = lValue.getClass().getName();
    String rName = rValue.getClass().getName();

    return lName.compareTo(rName);
  }

  public int cmpObject(Object lValue, Object rValue, JavaClassDef rClassDef)
  {
    int cmp = cmpObject(lValue, rValue);

    if (cmp != 0)
        return cmp;

    // attributes
    // XX: not sure how to do this, to imitate PHP objects,
    // should getters be involved as well?

    for (
      Map.Entry<String, FieldMarshalPair> lEntry : _fieldMap.entrySet()) {
      String lFieldName = lEntry.getKey();
      FieldMarshalPair rFieldPair = rClassDef._fieldMap.get(lFieldName);

      if (rFieldPair == null)
        return 1;

      FieldMarshalPair lFieldPair = lEntry.getValue();

      try {
        Object lResult = lFieldPair._field.get(lValue);
        Object rResult = rFieldPair._field.get(lValue);

        int resultCmp = cmpObject(lResult, rResult);

        if (resultCmp != 0)
          return resultCmp;
      }
      catch (IllegalAccessException e) {
        log.log(Level.FINE,  e.getMessage(), e);
        return 0;
      }
    }

    return 0;
  }

  /**
   * Returns the field getter.
   *
   * @param name
   * @return Value attained through invoking getter
   */
  public Value getField(Env env, Value qThis, StringValue nameV)
  {
    String name = nameV.toString();

    AbstractJavaMethod get = _getMap.get(name);

    if (get != null) {
      try {
        return get.callMethod(env, getQuercusClass(), qThis);
      } catch (Exception e) {
        log.log(Level.FINE, e.getMessage(), e);

        return null;
      }
    }

    FieldMarshalPair fieldPair = _fieldMap.get(name);
    if (fieldPair != null) {
      try {
        Object result = fieldPair._field.get(qThis.toJavaObject());
        return fieldPair._marshal.unmarshal(env, result);
      } catch (Exception e) {
        log.log(Level.FINE,  e.getMessage(), e);

        return null;
      }
    }

    AbstractFunction phpGet = qThis.getQuercusClass().getFieldGet();

    if (phpGet != null) {
      return phpGet.callMethod(env, getQuercusClass(), qThis, nameV);
    }

    if (__fieldGet != null) {
      try {
        return __fieldGet.callMethod(env, getQuercusClass(), qThis, nameV);
      } catch (Exception e) {
        log.log(Level.FINE,  e.getMessage(), e);

        return null;
      }
    }

    return null;
  }

  public Value putField(Env env,
                        Value qThis,
                        StringValue nameV,
                        Value value)
  {
    String name = nameV.toString();

    AbstractJavaMethod setter = _setMap.get(name);
    if (setter != null) {
      try {
        return setter.callMethod(env, getQuercusClass(), qThis, value);
      } catch (Exception e) {
        log.log(Level.FINE, e.getMessage(), e);

        return NullValue.NULL;
      }
    }

    FieldMarshalPair fieldPair = _fieldMap.get(name);

    if (fieldPair != null) {
      try {
        Class<?> type = fieldPair._field.getType();
        Object marshaledValue = fieldPair._marshal.marshal(env, value, type);
        fieldPair._field.set(qThis.toJavaObject(), marshaledValue);

        return value;

      } catch (Exception e) {
        log.log(Level.FINE, e.getMessage(), e);
        return NullValue.NULL;
      }
    }

    if (! qThis.isFieldInit()) {
      AbstractFunction phpSet = qThis.getQuercusClass().getFieldSet();

      if (phpSet != null) {
        qThis.setFieldInit(true);

        try {
          return phpSet.callMethod(env, getQuercusClass(), qThis, nameV, value);

        } finally {
          qThis.setFieldInit(false);
        }
      }
    }


    if (__fieldSet != null) {
      try {
        return __fieldSet.callMethod(env,
                                     getQuercusClass(),
                                     qThis,
                                     nameV,
                                     value);
      } catch (Exception e) {
        log.log(Level.FINE, e.getMessage(), e);

        return NullValue.NULL;
      }
    }

    return null;
  }

  /**
   * Returns the marshal instance.
   */
  public Marshal getMarshal()
  {
    return _marshal;
  }

  /**
   * Eval new
   */
  @Override
  public Value callNew(Env env, Value []args)
  {
    if (_cons != null) {
      if (__construct != null) {
        Value value = _cons.call(env, Value.NULL_ARGS);

        __construct.callMethod(env, __construct.getQuercusClass(), value, args);

        return value;
      }
      else {
        return _cons.call(env, args);
      }
    }
    else if (__construct != null) {
      return __construct.call(env, args);
    }
    else {
      return NullValue.NULL;
    }
  }

  @Override
  public AbstractFunction getCall()
  {
    return __call;
  }

  @Override
  public AbstractFunction getCallStatic()
  {
    return __callStatic;
  }

  @Override
  public AbstractFunction getSerialize()
  {
    return _serializeFun;
  }

  @Override
  public AbstractFunction getUnserialize()
  {
    return _unserializeFun;
  }

  /**
   * Eval a method
   */
  public AbstractFunction findFunction(StringValue methodName)
  {
    return _functionMap.getRaw(methodName);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value []args)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, args);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value a1)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, a1);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value a1, Value a2)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, a1, a2);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value a1, Value a2, Value a3)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, a1, a2, a3);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value a1, Value a2, Value a3, Value a4)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, a1, a2, a3, a4);
  }

  /**
   * Eval a method
   */
  public Value callMethod(Env env, Value qThis,
                          StringValue methodName, int hash,
                          Value a1, Value a2, Value a3, Value a4, Value a5)
  {
    AbstractFunction fun = _functionMap.get(methodName, hash);

    return fun.callMethod(env, getQuercusClass(), qThis, a1, a2, a3, a4, a5);
  }

  public Set<? extends Map.Entry<Value,Value>> entrySet(Object obj)
  {
    try {
      if (_entrySet == null) {
        return null;
      }

      return (Set) _entrySet.invoke(obj);
    } catch (Exception e) {
      throw new QuercusException(e);
    }
  }

  /**
   * Initialize the quercus class methods.
   */
  @Override
  public void initClassMethods(QuercusClass cl, String bindingClassName)
  {
    init();

    cl.addInitializer(this);

    if (_cons != null) {
      cl.setConstructor(_cons);
      cl.addMethod(_moduleContext.createString("__construct"), _cons);
    }

    if (__construct != null) {
      cl.setConstructor(__construct);
      cl.addMethod(_moduleContext.createString("__construct"), __construct);
    }

    if (__destruct != null) {
      cl.setDestructor(__destruct);
      cl.addMethod(_moduleContext.createString("__destruct"), __destruct);
    }

    for (AbstractJavaMethod value : _functionMap.values()) {
      cl.addMethod(_moduleContext.createString(value.getName()), value);
    }

    if (__fieldGet != null)
      cl.setFieldGet(__fieldGet);

    if (__fieldSet != null)
      cl.setFieldSet(__fieldSet);

    if (__call != null)
      cl.setCall(__call);

    if (__callStatic != null) {
      cl.setCallStatic(__callStatic);
    }

    if (__toString != null) {
      cl.addMethod(_moduleContext.createString("__toString"), __toString);
    }

    if (_arrayDelegate != null)
      cl.setArrayDelegate(_arrayDelegate);
    else if (_funArrayDelegate != null)
      cl.setArrayDelegate(_funArrayDelegate);

    if (_serializeFun != null) {
      cl.setSerialize(_serializeFun, _unserializeFun);
    }

    if (_traversableDelegate != null)
      cl.setTraversableDelegate(_traversableDelegate);
    else if (cl.getTraversableDelegate() == null
             && _iteratorMethod != null) {
      // adds support for Java classes implementing iterator()
      // php/
      cl.setTraversableDelegate(new JavaTraversableDelegate(_iteratorMethod));
    }

    if (_countDelegate != null) {
      cl.setCountDelegate(_countDelegate);
    }
  }

  /**
   * Initialize the quercus class fields.
   */
  @Override
  public void initClassFields(QuercusClass cl, String bindingClassName)
  {
    for (Map.Entry<String,Value> entry : _constMap.entrySet()) {
      cl.addConstant(_moduleContext.createString(entry.getKey()),
                     new LiteralExpr(entry.getValue()));
    }

    for (Map.Entry<String,Object> entry : _constJavaMap.entrySet()) {
      cl.addJavaConstant(_moduleContext.createString(entry.getKey()),
                         entry.getValue());
    }
  }

  /**
   * Finds the matching constant
   */
  public Value findConstant(Env env, String name)
  {
    return _constMap.get(name);
  }

  /**
   * Creates a new instance.
   */
  @Override
  public void initInstance(Env env, Value value, boolean isInitFieldValues)
  {
    if (value instanceof ObjectValue) {
      ObjectValue object = (ObjectValue) value;

      if (__destruct != null) {
        env.addObjectCleanup(object);
      }
    }
  }

  /**
   * Returns the quercus class
   */
  public QuercusClass getQuercusClass()
  {
    if (_quercusClass == null) {
      init();

      _quercusClass = new QuercusClass(_moduleContext, this, null);
    }

    return _quercusClass;
  }

  /**
   * Returns the constructor
   */
  @Override
  public AbstractFunction findConstructor()
  {
    return null;
  }

  @Override
  public final void init()
  {
    if (_isInit)
      return;

    synchronized (this) {
      if (_isInit)
        return;

      super.init();

      try {
        initInterfaceList(_type);
        introspect();
      }
      finally {
        _isInit = true;
      }
    }
  }

  private void initInterfaceList(Class<?> type)
  {
    Class<?>[] ifaces = type.getInterfaces();

    if (ifaces == null)
      return;

    for (Class<?> iface : ifaces) {
      JavaClassDef javaClassDef = _moduleContext.getJavaClassDefinition(iface);

      if (javaClassDef != null)
        addInterface(javaClassDef.getName());

      // recurse for parent interfaces
      initInterfaceList(iface);
    }
  }

  /**
   * Introspects the Java class.
   */
  private void introspect()
  {
    introspectConstants(_type);
    introspectEnums(_type);

    introspectMethods(_moduleContext, _type);
    introspectFields(_moduleContext, _type);

    _marshal = new JavaMarshal(this, false);

    AbstractJavaMethod consMethod = getConsMethod();

    if (consMethod != null) {
      if (consMethod.isStatic())
        _cons = consMethod;
      else
        __construct = consMethod;
    }

    if (Serializable.class.equals(_type)) {
      _serializeFun = findFunction(Serializable.SERIALIZE);
      _unserializeFun = findFunction(Serializable.UNSERIALIZE);
    }

    //Method consMethod = getConsMethod(_type);

    /*
    if (consMethod != null) {
      if (Modifier.isStatic(consMethod.getModifiers()))
        _cons = new JavaMethod(_moduleContext, consMethod);
      else
        __construct = new JavaMethod(_moduleContext, consMethod);
    }
    */

    if (_cons == null) {
      Constructor<?> []cons = _type.getConstructors();

      if (cons.length > 0) {
        int i;
        for (i = 0; i < cons.length; i++) {
          if (cons[i].isAnnotationPresent(Construct.class))
            break;
        }

        if (i < cons.length) {
          _cons = new JavaConstructor(_moduleContext, this, cons[i]);
        }
        else {
          _cons = new JavaConstructor(_moduleContext, this, cons[0]);
          for (i = 1; i < cons.length; i++) {
            _cons = _cons.overload(new JavaConstructor(_moduleContext,
                                                       this,
                                                       cons[i]));
          }
        }

      } else
        _cons = null;
    }

    if (_cons != null)
      _cons.setConstructor(true);

    if (__construct != null)
      __construct.setConstructor(true);

    introspectAnnotations(_type);
  }

  private void introspectAnnotations(Class<?> type)
  {
    try {
      if (type == null)
        return;

      // interfaces
      for (Class<?> iface : type.getInterfaces())
        introspectAnnotations(iface);

      // super-class
      introspectAnnotations(type.getSuperclass());

      // this
      for (Annotation annotation : type.getAnnotations()) {
        if (annotation.annotationType() == Delegates.class) {
          Class<?>[] delegateClasses = ((Delegates) annotation).value();

          for (Class<?> cl : delegateClasses) {
            boolean isDelegate = addDelegate(cl);

            if (! isDelegate)
              throw new IllegalArgumentException(
                L.l("unknown @Delegate class '{0}'", cl));
          }
        }
        else if (annotation.annotationType() == ResourceType.class) {
          _resourceType = ((ResourceType) annotation).value();
        }
      }
    } catch (RuntimeException e) {
      throw e;
    } catch (InstantiationException e) {
      throw new QuercusModuleException(e.getCause());
    } catch (Exception e) {
      throw new QuercusModuleException(e);
    }
  }

  private boolean addDelegate(Class<?> cl)
    throws InstantiationException, IllegalAccessException
  {
    boolean isDelegate = false;

    if (TraversableDelegate.class.isAssignableFrom(cl)) {
      _traversableDelegate = (TraversableDelegate) cl.newInstance();
      isDelegate = true;
    }

    if (ArrayDelegate.class.isAssignableFrom(cl)) {
      _arrayDelegate = (ArrayDelegate) cl.newInstance();
      isDelegate = true;
    }

    if (CountDelegate.class.isAssignableFrom(cl)) {
      _countDelegate = (CountDelegate) cl.newInstance();
      isDelegate = true;
    }

    return isDelegate;
  }

  private <T> boolean addDelegate(Class<T> cl,
                                  ArrayList<T> delegates,
                                  Class<? extends Object> delegateClass)
  {
    if (!cl.isAssignableFrom(delegateClass))
      return false;

    for (T delegate : delegates) {
      if (delegate.getClass() == delegateClass) {
        return true;
      }
    }

    try {
      delegates.add((T) delegateClass.newInstance());
    }
    catch (InstantiationException e) {
      throw new QuercusModuleException(e);
    }
    catch (IllegalAccessException e) {
      throw new QuercusModuleException(e);
    }

    return true;
  }

  /*
  private Method getConsMethod(Class type)
  {
    Method []methods = type.getMethods();

    for (int i = 0; i < methods.length; i++) {
      Method method = methods[i];

      if (! method.getName().equals("__construct"))
        continue;
      if (! Modifier.isPublic(method.getModifiers()))
        continue;

      return method;
    }

    return null;
  }
  */

  private AbstractJavaMethod getConsMethod()
  {
    for (AbstractJavaMethod method : _functionMap.values()) {
      if (method.getName().equals("__construct"))
        return method;
    }

    return null;
  }

  /**
   * Introspects the Java class.
   */
  private void introspectFields(ModuleContext moduleContext, Class<?> type)
  {
    if (type == null)
      return;

    if (! Modifier.isPublic(type.getModifiers()))
      return;

    // Introspect getXXX and setXXX
    // also register whether __get, __getField, __set, __setField exists
    Method[] methods = type.getMethods();

    for (Method method : methods) {
      if (Modifier.isStatic(method.getModifiers()))
        continue;

      if (method.isAnnotationPresent(Hide.class))
        continue;

      String methodName = method.getName();
      int length = methodName.length();

      if (length > 3) {
        if (methodName.startsWith("get")) {
          String quercusName
            = javaToQuercusConvert(methodName.substring(3, length));

          AbstractJavaMethod existingGetter = _getMap.get(quercusName);
          AbstractJavaMethod newGetter
            = new JavaMethod(moduleContext, this, method);

          if (existingGetter != null) {
            newGetter = existingGetter.overload(newGetter);
          }

          _getMap.put(quercusName, newGetter);
        }
        else if (methodName.startsWith("is")) {
          String quercusName
            = javaToQuercusConvert(methodName.substring(2, length));

          AbstractJavaMethod existingGetter = _getMap.get(quercusName);
          AbstractJavaMethod newGetter
            = new JavaMethod(moduleContext, this, method);

          if (existingGetter != null) {
            newGetter = existingGetter.overload(newGetter);
          }

          _getMap.put(quercusName, newGetter);
        }
        else if (methodName.startsWith("set")) {
          String quercusName
            = javaToQuercusConvert(methodName.substring(3, length));

          AbstractJavaMethod existingSetter = _setMap.get(quercusName);
          AbstractJavaMethod newSetter
            = new JavaMethod(moduleContext, this, method);

          if (existingSetter != null)
            newSetter = existingSetter.overload(newSetter);

          _setMap.put(quercusName, newSetter);
        } else if ("__get".equals(methodName)) {
          if (_funArrayDelegate == null)
            _funArrayDelegate = new FunctionArrayDelegate();

          _funArrayDelegate.setArrayGet(new JavaMethod(moduleContext, this, method));
        } else if ("__set".equals(methodName)) {
          if (_funArrayDelegate == null)
            _funArrayDelegate = new FunctionArrayDelegate();

          _funArrayDelegate.setArrayPut(new JavaMethod(moduleContext, this, method));
        } else if ("__count".equals(methodName)) {
          FunctionCountDelegate delegate = new FunctionCountDelegate();

          delegate.setCount(new JavaMethod(moduleContext, this, method));

          _countDelegate = delegate;
        } else if ("__getField".equals(methodName)) {
          __fieldGet = new JavaMethod(moduleContext, this, method);
        } else if ("__setField".equals(methodName)) {
          __fieldSet = new JavaMethod(moduleContext, this, method);
        } else if ("__fieldGet".equals(methodName)) {
          __fieldGet = new JavaMethod(moduleContext, this, method);
        } else if ("__fieldSet".equals(methodName)) {
          __fieldSet = new JavaMethod(moduleContext, this, method);
        }
      }
    }

    // server/2v00
    /*
    if (__fieldGet != null)
      _getMap.clear();

    if (__fieldSet != null)
      _setMap.clear();
    */

    // Introspect public non-static fields
    Field[] fields = type.getFields();

    for (Field field : fields) {
      if (Modifier.isStatic(field.getModifiers()))
        continue;
      else if (field.isAnnotationPresent(Hide.class))
        continue;

      MarshalFactory factory = moduleContext.getMarshalFactory();
      Marshal marshal = factory.create(field.getType(), false);

      _fieldMap.put(field.getName(),
                    new FieldMarshalPair(field, marshal));
    }


   // introspectFields(quercus, type.getSuperclass());
  }

  /**
   * helper for introspectFields
   *
   * @param s (eg: Foo, URL)
   * @return (foo, URL)
   */
  private String javaToQuercusConvert(String s)
  {
    if (s.length() == 1) {
      char ch = Character.toLowerCase(s.charAt(0));

      return String.valueOf(ch);
    }
    else if (Character.isUpperCase(s.charAt(1))) {
      return s;
    }
    else {
      StringBuilder sb = new StringBuilder();

      sb.append(Character.toLowerCase(s.charAt(0)));

      int length = s.length();
      for (int i = 1; i < length; i++) {
        sb.append(s.charAt(i));
      }

      return sb.toString();
    }
  }

  private void introspectConstants(Class<?> type)
  {
    if (type == null)
      return;

    if (! Modifier.isPublic(type.getModifiers()))
      return;

    Field []fields = type.getFields();

    for (Field field : fields) {
      if (_constMap.get(field.getName()) != null)
        continue;
      else if (_constJavaMap.get(field.getName()) != null)
        continue;
      else if (! Modifier.isPublic(field.getModifiers()))
        continue;
      else if (! Modifier.isStatic(field.getModifiers()))
        continue;
      else if (! Modifier.isFinal(field.getModifiers()))
        continue;
      else if (field.isAnnotationPresent(Hide.class))
        continue;

      try {
        Object obj = field.get(null);

        Value value = QuercusContext.objectToValue(obj);

        if (value != null)
          _constMap.put(field.getName().intern(), value);
        else
          _constJavaMap.put(field.getName().intern(), obj);
      } catch (Throwable e) {
        log.log(Level.FINEST, e.toString(), e);
      }
    }
  }

  private void introspectEnums(Class<?> type)
  {
    if (type == null) {
      return;
    }

    if (! Modifier.isPublic(type.getModifiers())) {
      return;
    }

    Class<?>[] classes = type.getClasses();

    for (Class<?> cls : classes) {
      if (! cls.isEnum()) {
        continue;
      }

      String name = cls.getSimpleName();

      if (_constMap.get(name) != null)
        continue;
      else if (_constJavaMap.get(name) != null)
        continue;
      else if (cls.isAnnotationPresent(Hide.class))
        continue;


      Object[] constants = cls.getEnumConstants();
      if (constants.length == 0) {
        continue;
      }

      // php/0cs3
      // use one of the enums as a handle for other enum siblings
      Object obj = constants[0];

      try {
        Value value = QuercusContext.objectToValue(obj);

        if (value != null)
          _constMap.put(name.intern(), value);
        else
          _constJavaMap.put(name.intern(), obj);
      }
      catch (Throwable e) {
        log.log(Level.FINEST, e.toString(), e);
      }
    }
  }

  /**
   * Introspects the Java class.
   */
  private void introspectMethods(ModuleContext moduleContext,
                                 Class<?> type)
  {
    if (type == null)
      return;

    Method []methods = type.getMethods();

    for (Method method : methods) {
      if (! Modifier.isPublic(method.getModifiers()))
        continue;

      if (method.isAnnotationPresent(Hide.class))
        continue;

      if (_isPhpClass && method.getDeclaringClass() == Object.class)
        continue;

      if ("iterator".equals(method.getName())
          && method.getParameterTypes().length == 0
          && Iterator.class.isAssignableFrom(method.getReturnType())) {
        _iteratorMethod = method;
      }

      if ("printRImpl".equals(method.getName())) {
        _printRImpl = method;
      } else if ("varDumpImpl".equals(method.getName())) {
        _varDumpImpl = method;
      } else if (method.isAnnotationPresent(JsonEncode.class)) {
        _jsonEncode = method;
      } else if (method.isAnnotationPresent(EntrySet.class)) {
        _entrySet = method;
      } else if ("__call".equals(method.getName())) {
        __call = new JavaMethod(moduleContext, this, method);
      } else if ("__callStatic".equals(method.getName())) {
        __callStatic = new JavaMethod(moduleContext, this, method);
      } else if ("__toString".equals(method.getName())) {
        __toString = new JavaMethod(moduleContext, this, method);
        _functionMap.put(_moduleContext.createString(method.getName()), __toString);
      } else if ("__destruct".equals(method.getName())) {
        __destruct = new JavaMethod(moduleContext, this, method);
        _functionMap.put(_moduleContext.createString(method.getName()), __destruct);
      } else {
        if (method.getName().startsWith("quercus_"))
          throw new UnsupportedOperationException(
            L.l("{0}: use @Name instead", method.getName()));

        JavaMethod newFun = new JavaMethod(moduleContext, this, method);
        String funName = newFun.getName();

        StringValue nameV = moduleContext.createString(funName);

        AbstractJavaMethod fun = _functionMap.getRaw(nameV);

        if (fun != null)
          fun = fun.overload(newFun);
        else
          fun = newFun;

        _functionMap.put(nameV, fun);
      }
    }

    /* Class.getMethods() is recursive
    introspectMethods(moduleContext, type.getSuperclass());

    Class []ifcs = type.getInterfaces();

    for (Class ifc : ifcs) {
      introspectMethods(moduleContext, ifc);
    }
    */
  }

  public JavaMethod getToString()
  {
    return __toString;
  }

  public StringValue toString(Env env,
                              JavaValue value)
  {
    if (__toString == null) {
      return null;
    }

    QuercusClass cls = getQuercusClass();
    Value str = __toString.callMethod(env, cls, value, Expr.NULL_ARGS);

    return str.toStringValue(env);
  }

  public boolean jsonEncode(Env env,
                            Object obj,
                            JsonEncodeContext context,
                            StringValue sb)
  {
    if (_jsonEncode == null)
      return false;

    try {
      _jsonEncode.invoke(obj, env, context, sb);
      return true;

    } catch (InvocationTargetException e) {
      throw new QuercusRuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new QuercusRuntimeException(e);
    }
  }

  /**
   *
   * @return false if printRImpl not implemented
   * @throws IOException
   */
  public boolean printRImpl(Env env,
                            Object obj,
                            WriteStream out,
                            int depth,
                            IdentityHashMap<Value, String> valueSet)
    throws IOException
  {

    try {
      if (_printRImpl == null) {
        return false;

      }

      _printRImpl.invoke(obj, env, out, depth, valueSet);
      return true;
    } catch (InvocationTargetException e) {
      throw new QuercusRuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new QuercusRuntimeException(e);
    }
  }

  public boolean varDumpImpl(Env env,
                             Value obj,
                             Object javaObj,
                             WriteStream out,
                             int depth,
                             IdentityHashMap<Value, String> valueSet)
    throws IOException
  {
    try {
      if (_varDumpImpl == null)
        return false;

      _varDumpImpl.invoke(javaObj, env, obj, out, depth, valueSet);
      return true;
    } catch (InvocationTargetException e) {
      throw new QuercusRuntimeException(e);
    } catch (IllegalAccessException e) {
      throw new QuercusRuntimeException(e);
    }
  }

  private class JavaTraversableDelegate
    implements TraversableDelegate
  {
    private Method _iteratorMethod;

    public JavaTraversableDelegate(Method iterator)
    {
      _iteratorMethod = iterator;
    }

    public Iterator<Map.Entry<Value, Value>>
      getIterator(Env env, ObjectValue qThis)
    {
      try {
        Object javaObj = qThis.toJavaObject();

        Iterator<?> iterator = (Iterator<?>) _iteratorMethod.invoke(javaObj);

        return new JavaIterator(env, iterator);
      } catch (InvocationTargetException e) {
        throw new QuercusRuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new QuercusRuntimeException(e);
      }
    }

    public Iterator<Value> getKeyIterator(Env env, ObjectValue qThis)
    {
      try {
        Object javaObj = qThis.toJavaObject();

        Iterator<?> iterator = (Iterator<?>) _iteratorMethod.invoke(javaObj);

        return new JavaKeyIterator(iterator);
      } catch (InvocationTargetException e) {
        throw new QuercusRuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new QuercusRuntimeException(e);
      }
    }

    public Iterator<Value> getValueIterator(Env env, ObjectValue qThis)
    {
      try {
        Object javaObj = qThis.toJavaObject();

        Iterator<?> iterator = (Iterator<?>) _iteratorMethod.invoke(javaObj);

        return new JavaValueIterator(env, iterator);
      } catch (InvocationTargetException e) {
        throw new QuercusRuntimeException(e);
      } catch (IllegalAccessException e) {
        throw new QuercusRuntimeException(e);
      }
    }
  }

  private class JavaKeyIterator
    implements Iterator<Value>
  {
    private Iterator<?> _iterator;
    private int _index;

    public JavaKeyIterator(Iterator<?> iterator)
    {
      _iterator = iterator;
    }

    public Value next()
    {
      _iterator.next();

      return LongValue.create(_index++);
    }

    public boolean hasNext()
    {
      return _iterator.hasNext();
    }

    public void remove()
    {
      throw new UnsupportedOperationException();
    }
  }

  private class JavaValueIterator
    implements Iterator<Value>
  {
    private Env _env;
    private Iterator<?> _iterator;

    public JavaValueIterator(Env env, Iterator<?> iterator)
    {
      _env = env;
      _iterator = iterator;
    }

    public Value next()
    {
      Object next = _iterator.next();

      if (next instanceof Map.Entry) {
        Map.Entry entry = (Map.Entry) next;

        Object value = entry.getValue();

        if (value instanceof Value) {
          return (Value) value;
        }
        else {
          return _env.wrapJava(value);
        }
      }
      else {
        return _env.wrapJava(next);
      }
    }

    public boolean hasNext()
    {
      if (_iterator != null)
        return _iterator.hasNext();
      else
        return false;
    }

    public void remove()
    {
      throw new UnsupportedOperationException();
    }
  }

  private class JavaIterator
    implements Iterator<Map.Entry<Value, Value>>
  {
    private Env _env;
    private Iterator<?> _iterator;

    private int _index;

    public JavaIterator(Env env, Iterator<?> iterator)
    {
      _env = env;
      _iterator = iterator;
    }

    public Map.Entry<Value, Value> next()
    {
      Object next = _iterator.next();
      int index = _index++;

      if (next instanceof Map.Entry) {
        Map.Entry entry = (Map.Entry) next;

        Object key = entry.getKey();
        Object value = entry.getValue();

        if (key instanceof Value
            && value instanceof Value) {
          return (Map.Entry<Value, Value>) entry;
        }
        else if (key instanceof Value) {
          Value v = _env.wrapJava(value);

          return new JavaEntry((Value) key, v);
        }
        else {
          Value k = _env.wrapJava(key);

          return new JavaEntry(k, _env.wrapJava(value));
        }
      }
      else {
        return new JavaEntry(LongValue.create(index), _env.wrapJava(next));
      }
    }

    public boolean hasNext()
    {
      if (_iterator != null)
        return _iterator.hasNext();
      else
        return false;
    }

    public void remove()
    {
      throw new UnsupportedOperationException();
    }
  }

  private class JavaEntry
    implements Map.Entry<Value, Value>
  {
    private Value _key;
    private Value _value;

    public JavaEntry(Value key, Value value)
    {
      _key = key;
      _value = value;
    }

    public Value getKey()
    {
      return _key;
    }

    public Value getValue()
    {
      return _value;
    }

    public Value setValue(Value value)
    {
      throw new UnsupportedOperationException();
    }
  }

  private class MethodMarshalPair {
    public Method _method;
    public Marshal _marshal;

    public MethodMarshalPair(Method method,
                              Marshal marshal)
    {
      _method = method;
      _marshal = marshal;
    }
  }

  private class FieldMarshalPair {
    public Field _field;
    public Marshal _marshal;

    public FieldMarshalPair(Field field,
                             Marshal marshal)
    {
      _field = field;
      _marshal = marshal;
    }
  }

  private static class LongClassDef extends JavaClassDef {
    LongClassDef(ModuleContext module)
    {
      super(module, "Long", Long.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return LongValue.create(((Number) obj).longValue());
    }
  }

  private static class DoubleClassDef extends JavaClassDef {
    DoubleClassDef(ModuleContext module)
    {
      super(module, "Double", Double.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new DoubleValue(((Number) obj).doubleValue());
    }
  }

  private static class BigIntegerClassDef extends JavaClassDef {
    BigIntegerClassDef(ModuleContext module)
    {
      super(module, "BigInteger", BigInteger.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new BigIntegerValue(env, (BigInteger) obj, this);
    }
  }

  private static class BigDecimalClassDef extends JavaClassDef {
    BigDecimalClassDef(ModuleContext module)
    {
      super(module, "BigDecimal", BigDecimal.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new BigDecimalValue(env, (BigDecimal) obj, this);
    }
  }

  private static class StringClassDef extends JavaClassDef {
    StringClassDef(ModuleContext module)
    {
      super(module, "String", String.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return env.createString((String) obj);
    }
  }

  private static class BooleanClassDef extends JavaClassDef {
    BooleanClassDef(ModuleContext module)
    {
      super(module, "Boolean", Boolean.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      if (Boolean.TRUE.equals(obj))
        return BooleanValue.TRUE;
      else
        return BooleanValue.FALSE;
    }
  }

  private static class CalendarClassDef extends JavaClassDef {
    CalendarClassDef(ModuleContext module)
    {
      super(module, "Calendar", Calendar.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new JavaCalendarValue(env, (Calendar)obj, this);
    }
  }

  private static class DateClassDef extends JavaClassDef {
    DateClassDef(ModuleContext module)
    {
      super(module, "Date", Date.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new JavaDateValue(env, (Date)obj, this);
    }
  }

  private static class URLClassDef extends JavaClassDef {
    URLClassDef(ModuleContext module)
    {
      super(module, "URL", URL.class);
    }

    @Override
    public Value wrap(Env env, Object obj)
    {
      return new JavaURLValue(env, (URL)obj, this);
    }
  }
}

