/*
 * The contents of this file are subject to the Mozilla Public License Version 1.1
 * (the "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at <http://www.mozilla.org/MPL/>.
 * 
 * Software distributed under the License is distributed on an "AS IS" basis, WITHOUT
 * WARRANTY OF ANY KIND, either express or implied. See the License for the specific
 * language governing rights and limitations under the License.
 * 
 * The Original Code is the Venice Web Communities System.
 * 
 * The Initial Developer of the Original Code is Eric J. Bowersox <erbo@silcom.com>,
 * for Silverwrist Design Studios.  Portions created by Eric J. Bowersox are
 * Copyright (C) 2002 Eric J. Bowersox/Silverwrist Design Studios.  All Rights Reserved.
 * 
 * Contributor(s): 
 */
package com.silverwrist.dynamo.obj;

import java.lang.reflect.*;
import java.util.*;
import com.silverwrist.util.*;
import com.silverwrist.dynamo.except.*;
import com.silverwrist.dynamo.iface.*;

class JavaDynamicClass implements DynamicClass, DynamicWrapper
{
  /*--------------------------------------------------------------------------------
   * Internal class implementing properties
   *--------------------------------------------------------------------------------
   */

  private class Property
  {
    /*====================================================================
     * Attributes
     *====================================================================
     */

    private String m_name;
    private Field m_field = null;
    private Method m_get_method = null;
    private Method[] m_set_methods = null;

    /*====================================================================
     * Constructor
     *====================================================================
     */

    Property(String name)
    {
      m_name = name;

    } // end constructor

    /*====================================================================
     * External operations
     *====================================================================
     */

    String getName()
    {
      return m_name;

    } // end getName

    void setField(Field field)
    {
      if (m_field==null)
	m_field = field;
      else
	throw new IllegalStateException("multiple field defs not permitted");

    } // end setField

    void setGetMethod(Method method)
    {
      if (m_get_method==null)
	m_get_method = method;
      else
	throw new IllegalStateException("multiple getters not permitted");

    } // end setGetMethod

    void setSetMethods(Method[] methods)
    {
      if (m_set_methods==null)
	m_set_methods = methods;
      // else this is a no-op

    } // end setSetMethod

    Object doGet(Object on_obj) throws DynamicObjectException
    {
      try
      { // try calling the various possible methods
	if (m_get_method!=null)
	  return m_get_method.invoke(on_obj,NULL_ARG_LIST);
	else if (m_field!=null)
	  return m_field.get(on_obj);
	else
	  throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.GET,m_name);

      } // end try
      catch (IllegalAccessException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e);

      } // end catch
      catch (InvocationTargetException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e.getCause());

      } // end catch
      catch (UndeclaredThrowableException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e.getCause());

      } // end catch

    } // end doGet

    void doSet(Object on_obj, Object value) throws DynamicObjectException
    {
      try
      { // try calling the various possible methods
	if (m_set_methods!=null)
	{ // pick a method and invoke it
	  Object[] args = new Object[] { value };
	  Method m = selectMethod(m_set_methods,args);
	  if (m==null)
	    throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.SET,m_name);
	  else
	    m.invoke(on_obj,args);
	    
	} // end if
	else if (m_field!=null)
	  m_field.set(on_obj,value);
	else
	  throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.SET,m_name);

      } // end try
      catch (IllegalAccessException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e);

      } // end catch
      catch (InvocationTargetException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e.getCause());

      } // end catch
      catch (UndeclaredThrowableException e)
      { // translate to CallFailureException
	throw new CallFailureException(JavaDynamicClass.this,e.getCause());

      } // end catch

    } // end doSet

  } // end class Property

  /*--------------------------------------------------------------------------------
   * Static data members
   *--------------------------------------------------------------------------------
   */

  private static final Object[] NULL_ARG_LIST = new Object[0];
  private static final Method[] METHOD_TEMPLATE = new Method[0];

  /*--------------------------------------------------------------------------------
   * Attributes
   *--------------------------------------------------------------------------------
   */

  private Class m_klass;
  private Map m_static_methods;
  private Map m_object_methods;
  private Map m_static_properties;
  private Map m_object_properties;

  /*--------------------------------------------------------------------------------
   * Constructor
   *--------------------------------------------------------------------------------
   */

  JavaDynamicClass(Class klass)
  {
    m_klass = klass;  // save Java class

    // Allocate various HashMaps required to properly introspect the class's methods
    // and properties.
    HashMap ometh_map = new HashMap();
    HashMap smeth_map = new HashMap();
    HashMap oprop_map = new HashMap();
    HashMap oprop_set_map = new HashMap();
    HashMap sprop_map = new HashMap();
    HashMap sprop_set_map = new HashMap();

    // Get the methods for the object and fill them in as object methods, and as
    // property getters and setters.
    Method[] meths = klass.getMethods();
    int i;
    for (i=0; i<meths.length; i++)
    { // look at all methods and create appropriate entries
      Map meth_map, prop_map, prop_set_map;
      if (Modifier.isStatic(meths[i].getModifiers()))
      { // add to "static" maps
	meth_map = smeth_map;
	prop_map = sprop_map;
	prop_set_map = sprop_set_map;

      } // end if
      else
      { // add to "object" maps
	meth_map = ometh_map;
	prop_map = oprop_map;
	prop_set_map = oprop_set_map;

      } // end else

      // Add method to list of methods with this name.
      String name = meths[i].getName();
      ArrayList al = (ArrayList)(meth_map.get(name));
      if (al==null)
      { // create method list
	al = new ArrayList();
	meth_map.put(name,al);

      } // end if

      al.add(meths[i]);

      Property p = null;
      if (isGetterMethod(meths[i]))
      { // create property name and Property entry
	name = makePropertyName(name);
	p = (Property)(prop_map.get(name));
	if (p==null)
	{ // create new Property entry
	  p = new Property(name);
	  prop_map.put(name,p);

	} // end if

	p.setGetMethod(meths[i]);

      } // end if (found getter)
      else if (isSetterMethod(meths[i]))
      { // create property name and ArrayList for setter
	name = makePropertyName(name);
	al = (ArrayList)(prop_set_map.get(name));
	if (al==null)
	{ // create new ArrayList
	  al = new ArrayList();
	  prop_set_map.put(name,al);

	} // end if

	al.add(meths[i]);

      } // end else if (found setter)

    } // end for

    // Freeze the static method map.
    HashMap tmp = null;
    Iterator it;
    if (smeth_map.isEmpty())
      m_static_methods = Collections.EMPTY_MAP;
    else
    { // copy entries to fresh HashMap, converting to Method arrays
      tmp = new HashMap();
      it = smeth_map.keySet().iterator();
      while (it.hasNext())
      { // get name, then ArrayList, then make array
	String name = (String)(it.next());
	ArrayList al = (ArrayList)(smeth_map.get(name));
	tmp.put(name,al.toArray(METHOD_TEMPLATE));
	al.clear();

      } // end while

      // freeze static methods
      m_static_methods = Collections.unmodifiableMap(tmp);
      smeth_map.clear();

    } // end else

    smeth_map = null;

    // Freeze the object method map.
    if (ometh_map.isEmpty())
      m_object_methods = Collections.EMPTY_MAP;
    else
    { // copy entries to fresh HashMap, converting to Method arrays
      tmp = new HashMap();
      it = ometh_map.keySet().iterator();
      while (it.hasNext())
      { // get name, then ArrayList, then make array
	String name = (String)(it.next());
	ArrayList al = (ArrayList)(ometh_map.get(name));
	tmp.put(name,al.toArray(METHOD_TEMPLATE));
	al.clear();

      } // end while

      // freeze object methods
      m_object_methods = Collections.unmodifiableMap(tmp);
      ometh_map.clear();

    } // end else

    ometh_map = null;

    // Load the setter arrays for static properties.
    it = sprop_set_map.keySet().iterator();
    while (it.hasNext())
    { // get name, then ArrayList, then Property, then save off array
      String name = (String)(it.next());
      ArrayList al = (ArrayList)(sprop_set_map.get(name));
      Property p = (Property)(sprop_map.get(name));
      if (p==null)
      { // create new Property entry
	p = new Property(name);
	sprop_map.put(name,p);

      } // end if

      p.setSetMethods((Method[])(al.toArray(METHOD_TEMPLATE)));
      al.clear();

    } // end while

    sprop_set_map.clear();  // zap the setter maps
    sprop_set_map = null;

    // Load the setter arrays for object properties.
    it = oprop_set_map.keySet().iterator();
    while (it.hasNext())
    { // get name, then ArrayList, then Property, then save off array
      String name = (String)(it.next());
      ArrayList al = (ArrayList)(oprop_set_map.get(name));
      Property p = (Property)(oprop_map.get(name));
      if (p==null)
      { // create new Property entry
	p = new Property(name);
	oprop_map.put(name,p);

      } // end if

      p.setSetMethods((Method[])(al.toArray(METHOD_TEMPLATE)));
      al.clear();

    } // end while

    oprop_set_map.clear();  // zap the setter maps
    oprop_set_map = null;

    Field[] fields = klass.getFields();
    for (i=0; i<fields.length; i++)
    { // look up property entry for each field
      Map prop_map;
      if (Modifier.isStatic(fields[i].getModifiers()))
	prop_map = sprop_map; // add to "static" maps
      else // add to "object" maps
	prop_map = oprop_map;

      String name = fields[i].getName();
      Property p = (Property)(prop_map.get(name));
      if (p==null)
      { // create new Property entry
	p = new Property(name);
	prop_map.put(name,p);

      } // end if

      p.setField(fields[i]);  // save off field pointer
      
    } // end for

    // Freeze the two property maps.
    if (sprop_map.isEmpty())
      m_static_properties = Collections.EMPTY_MAP;
    else
      m_static_properties = Collections.unmodifiableMap(sprop_map);
    if (oprop_map.isEmpty())
      m_object_properties = Collections.EMPTY_MAP;
    else
      m_object_properties = Collections.unmodifiableMap(oprop_map);

  } // end constructor

  /*--------------------------------------------------------------------------------
   * Internal operations
   *--------------------------------------------------------------------------------
   */

  private static final boolean isGetterMethod(Method m)
  {
    Class[] parms = m.getParameterTypes();
    if (parms.length!=0)
      return false;
    String name = m.getName();
    if (name.startsWith("get") && (name.length()>3))
      return true;
    if (name.startsWith("is") && (name.length()>2))
    { // "is" is a valid getter, but only for Booleans
      Class rtype = m.getReturnType();
      if ((rtype==Boolean.class) || (rtype==Boolean.TYPE))
	return true;

    } // end if

    return false;

  } // end isGetterMethod

  private static final boolean isSetterMethod(Method m)
  {
    Class[] parms = m.getParameterTypes();
    if (parms.length!=1)
      return false;
    Class rtype = m.getReturnType();
    if (rtype!=Void.TYPE)
      return false;
    String name = m.getName();
    return (name.startsWith("set") && (name.length()>3));

  } // end isSetterMethod

  private static final String makePropertyName(String name)
  {
    String rc = null;
    if (name.startsWith("get") || name.startsWith("set"))
      rc = name.substring(3);
    else if (name.startsWith("is"))
      rc = name.substring(2);
    if (Character.isUpperCase(rc.charAt(0)) && !(Character.isUpperCase(rc.charAt(1))))
      return StringUtils.uncapitalise(rc);
    else
      return rc;
    
  } // end makePropertyName

  private static final int getFigureOfMerit(Class param_class, Class actual_class)
  {
    if (param_class==actual_class)
      return 0;
    if (param_class.isPrimitive())
      return 0;
    if (param_class.isInterface())
      return 1;

    int figm = 0;
    while ((actual_class!=Object.class) && (actual_class!=param_class))
    { // go up the class hierarchy until we get a match
      actual_class = actual_class.getSuperclass();
      figm += 2;

    } // end while

    return figm;

  } // end getFigureOfMerit

  private static final int getFigureOfMerit(Method method, Class[] arg_classes)
  {
    Class[] param_classes = method.getParameterTypes();
    if (param_classes.length!=arg_classes.length)
      return -1;  // parameter count mismatch

    int figm = 0;
    for (int i=0; i<param_classes.length; i++)
    { // check each parameter type to see which is right
      if (!(param_classes[i].isAssignableFrom(arg_classes[i])))
	return -1;  // parameter type mismatch
      figm += getFigureOfMerit(param_classes[i],arg_classes[i]);
      
    } // end for

    return figm;

  } // end getFigureOfMerit

  private static final Method selectMethod(Method[] methods, Object[] args)
  {
    if (methods.length==1)
      return methods[0];   // trivial

    Class[] arg_classes = new Class[args.length];
    int i;
    for (i=0; i<args.length; i++)
      arg_classes[i] = args[i].getClass();

    Method rc = null;
    int current_figm = Integer.MAX_VALUE;
    for (i=0; i<methods.length; i++)
    { // get the figure of merit for this method
      int figm = getFigureOfMerit(methods[i],arg_classes);
      if (figm<0)
	continue;
      if (figm<current_figm)
      { // this is the new "best" method
	rc = methods[i];
	current_figm = figm;

      } // end if

      if (current_figm==0)
	break;  // can't get any better than this

    } // end for

    return rc;

  } // end selectMethod

  private static final Object unwrapObject(Object obj)
  {
    while (obj instanceof DynamicWrapper)
      obj = ((DynamicWrapper)obj).unwrap();
    return obj;

  } // end unwrapObject

  private static final Object[] unwrapArray(Object[] arr)
  {
    for (int i=0; i<arr.length; i++)
    { // look for a dynamically wrapped object
      if (arr[i] instanceof DynamicWrapper)
      { // we need to unwrap at least part of this array - do it
	Object[] rc = new Object[arr.length];
	if (i>0)
	  System.arraycopy(arr,0,rc,0,i);
	for (int j=i; j<arr.length; j++)
	  rc[j] = unwrapObject(arr[j]);
	return rc;

      } // end if

    } // end for

    return arr;  // no need to unwrap anything

  } // end unwrapArray

  /*--------------------------------------------------------------------------------
   * Overrides from class Object
   *--------------------------------------------------------------------------------
   */

  public boolean equals(Object obj)
  {
    if (obj==null)
      return false;
    if (!(obj instanceof JavaDynamicClass))
      return false;
    JavaDynamicClass other = (JavaDynamicClass)obj;
    return m_klass.equals(other.m_klass);

  } // end equals

  public int hashCode()
  {
    return m_klass.hashCode();

  } // end hashCode

  public String toString()
  {
    return "dynamic " + m_klass.toString();

  } // end toString

  /*--------------------------------------------------------------------------------
   * Implementations from interface NamedObject
   *--------------------------------------------------------------------------------
   */

  public String getName()
  {
    return m_klass.getName();

  } // end getName

  /*--------------------------------------------------------------------------------
   * Implementations from interface DynamicClass
   *--------------------------------------------------------------------------------
   */

  public Object get(String property_name) throws DynamicObjectException
  {
    Property p = (Property)(m_static_properties.get(property_name));
    if (p==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.PROPERTY,property_name);
    return p.doGet(null);

  } // end get

  public void set(String property_name, Object value) throws DynamicObjectException
  {
    Property p = (Property)(m_static_properties.get(property_name));
    if (p==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.PROPERTY,property_name);
    p.doSet(null,unwrapObject(value));

  } // end set

  public Object call(String method_name, Object[] parameters) throws DynamicObjectException
  {
    Method m = null;
    Method[] arr = (Method[])(m_static_methods.get(method_name));
    Object[] real_parameters = null;
    if (arr!=null)
    { // convert parameters and select
      real_parameters = unwrapArray(parameters);
      m = selectMethod(arr,real_parameters);

    } // end if

    if (m==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.CALL,method_name);
    try
    { // call the static method
      return m.invoke(null,real_parameters);

    } // end try
    catch (IllegalAccessException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e);
      
    } // end catch
    catch (InvocationTargetException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e.getCause());

    } // end catch
    catch (UndeclaredThrowableException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e.getCause());

    } // end catch

  } // end call

  public Object call(String method_name, List parameters) throws DynamicObjectException
  {
    return call(method_name,parameters.toArray());

  } // end call

  public DynamicObject cast(Object source) throws ClassCastException
  {
    Object obj = unwrapObject(source);
    if (m_klass.isInstance(obj))
      return new JavaDynamicObject(this,obj);
    throw new ClassCastException("not instance of class " + m_klass.getName());

  } // end cast

  /*--------------------------------------------------------------------------------
   * Implementations from interface DynamicWrapper
   *--------------------------------------------------------------------------------
   */

  public Object unwrap()
  {
    return m_klass;

  } // end unwrap

  /*--------------------------------------------------------------------------------
   * External operations
   *--------------------------------------------------------------------------------
   */

  Object getForObject(Object target, String property_name) throws DynamicObjectException
  {
    Property p = (Property)(m_object_properties.get(property_name));
    if (p==null)
      p = (Property)(m_static_properties.get(property_name));
    if (p==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.PROPERTY,property_name);
    return p.doGet(target);

  } // end getForObject

  void setForObject(Object target, String property_name, Object value) throws DynamicObjectException
  {
    Property p = (Property)(m_object_properties.get(property_name));
    if (p==null)
      p = (Property)(m_static_properties.get(property_name));
    if (p==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.PROPERTY,property_name);
    p.doSet(target,unwrapObject(value));

  } // end setForObject

  Object callForObject(Object target, String method_name, Object[] parameters) throws DynamicObjectException
  {
    Method m = null;
    Method[] arr = (Method[])(m_object_methods.get(method_name));
    if (arr==null)
      arr = (Method[])(m_static_methods.get(method_name));
    Object[] real_parameters = null;
    if (arr!=null)
    { // convert parameters and select
      real_parameters = unwrapArray(parameters);
      m = selectMethod(arr,real_parameters);

    } // end if

    if (m==null)
      throw new CodeNotFoundException(JavaDynamicClass.this,CodeNotFoundException.CALL,method_name);
    try
    { // call the static method
      return m.invoke(target,real_parameters);

    } // end try
    catch (IllegalAccessException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e);
      
    } // end catch
    catch (InvocationTargetException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e.getCause());

    } // end catch
    catch (UndeclaredThrowableException e)
    { // translate to CallFailureException
      throw new CallFailureException(JavaDynamicClass.this,e.getCause());

    } // end catch

  } // end callForObject

} // end class JavaDynamicClass
