package org.apache.commons.beanutils;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.security.AccessController;
import java.security.PrivilegedAction;
import java.util.Hashtable;

public class MappedPropertyDescriptor extends PropertyDescriptor {
  private Class mappedPropertyType;
  
  private Method mappedReadMethod;
  
  private Method mappedWriteMethod;
  
  private static final Class[] stringClassArray = new Class[] { String.class };
  
  public MappedPropertyDescriptor(String propertyName, Class beanClass) throws IntrospectionException {
    super(propertyName, (Method)null, (Method)null);
    if (propertyName == null || propertyName.length() == 0)
      throw new IntrospectionException("bad property name: " + propertyName + " on class: " + beanClass.getClass().getName()); 
    setName(propertyName);
    String base = capitalizePropertyName(propertyName);
    try {
      this.mappedReadMethod = findMethod(beanClass, "get" + base, 1, stringClassArray);
      Class[] params = { String.class, this.mappedReadMethod.getReturnType() };
      this.mappedWriteMethod = findMethod(beanClass, "set" + base, 2, params);
    } catch (IntrospectionException e) {}
    if (this.mappedReadMethod == null)
      this.mappedWriteMethod = findMethod(beanClass, "set" + base, 2); 
    if (this.mappedReadMethod == null && this.mappedWriteMethod == null)
      throw new IntrospectionException("Property '" + propertyName + "' not found on " + beanClass.getName()); 
    findMappedPropertyType();
  }
  
  public MappedPropertyDescriptor(String propertyName, Class beanClass, String mappedGetterName, String mappedSetterName) throws IntrospectionException {
    super(propertyName, (Method)null, (Method)null);
    if (propertyName == null || propertyName.length() == 0)
      throw new IntrospectionException("bad property name: " + propertyName); 
    setName(propertyName);
    this.mappedReadMethod = findMethod(beanClass, mappedGetterName, 1, stringClassArray);
    if (this.mappedReadMethod != null) {
      Class[] params = { String.class, this.mappedReadMethod.getReturnType() };
      this.mappedWriteMethod = findMethod(beanClass, mappedSetterName, 2, params);
    } else {
      this.mappedWriteMethod = findMethod(beanClass, mappedSetterName, 2);
    } 
    findMappedPropertyType();
  }
  
  public MappedPropertyDescriptor(String propertyName, Method mappedGetter, Method mappedSetter) throws IntrospectionException {
    super(propertyName, mappedGetter, mappedSetter);
    if (propertyName == null || propertyName.length() == 0)
      throw new IntrospectionException("bad property name: " + propertyName); 
    setName(propertyName);
    this.mappedReadMethod = mappedGetter;
    this.mappedWriteMethod = mappedSetter;
    findMappedPropertyType();
  }
  
  public Class getMappedPropertyType() {
    return this.mappedPropertyType;
  }
  
  public Method getMappedReadMethod() {
    return this.mappedReadMethod;
  }
  
  public void setMappedReadMethod(Method mappedGetter) throws IntrospectionException {
    this.mappedReadMethod = mappedGetter;
    findMappedPropertyType();
  }
  
  public Method getMappedWriteMethod() {
    return this.mappedWriteMethod;
  }
  
  public void setMappedWriteMethod(Method mappedSetter) throws IntrospectionException {
    this.mappedWriteMethod = mappedSetter;
    findMappedPropertyType();
  }
  
  private void findMappedPropertyType() throws IntrospectionException {
    try {
      this.mappedPropertyType = null;
      if (this.mappedReadMethod != null) {
        if ((this.mappedReadMethod.getParameterTypes()).length != 1)
          throw new IntrospectionException("bad mapped read method arg count"); 
        this.mappedPropertyType = this.mappedReadMethod.getReturnType();
        if (this.mappedPropertyType == void.class)
          throw new IntrospectionException("mapped read method " + this.mappedReadMethod.getName() + " returns void"); 
      } 
      if (this.mappedWriteMethod != null) {
        Class[] params = this.mappedWriteMethod.getParameterTypes();
        if (params.length != 2)
          throw new IntrospectionException("bad mapped write method arg count"); 
        if (this.mappedPropertyType != null && this.mappedPropertyType != params[1])
          throw new IntrospectionException("type mismatch between mapped read and write methods"); 
        this.mappedPropertyType = params[1];
      } 
    } catch (IntrospectionException ex) {
      throw ex;
    } 
  }
  
  private static String capitalizePropertyName(String s) {
    if (s.length() == 0)
      return s; 
    char[] chars = s.toCharArray();
    chars[0] = Character.toUpperCase(chars[0]);
    return new String(chars);
  }
  
  private static Hashtable declaredMethodCache = new Hashtable();
  
  private static synchronized Method[] getPublicDeclaredMethods(Class clz) {
    Class fclz = clz;
    Method[] result = (Method[])declaredMethodCache.get(fclz);
    if (result != null)
      return result; 
    result = AccessController.<Method[]>doPrivileged(new PrivilegedAction(fclz) {
          private final Class val$fclz;
          
          public Object run() {
            return this.val$fclz.getDeclaredMethods();
          }
        });
    for (int i = 0; i < result.length; i++) {
      Method method = result[i];
      int mods = method.getModifiers();
      if (!Modifier.isPublic(mods))
        result[i] = null; 
    } 
    declaredMethodCache.put(clz, result);
    return result;
  }
  
  private static Method internalFindMethod(Class start, String methodName, int argCount) {
    for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
      Method[] methods = getPublicDeclaredMethods(cl);
      for (int j = 0; j < methods.length; j++) {
        Method method = methods[j];
        if (method != null) {
          int mods = method.getModifiers();
          if (!Modifier.isStatic(mods))
            if (method.getName().equals(methodName) && (method.getParameterTypes()).length == argCount)
              return method;  
        } 
      } 
    } 
    Class[] ifcs = start.getInterfaces();
    for (int i = 0; i < ifcs.length; i++) {
      Method m = internalFindMethod(ifcs[i], methodName, argCount);
      if (m != null)
        return m; 
    } 
    return null;
  }
  
  private static Method internalFindMethod(Class start, String methodName, int argCount, Class[] args) {
    for (Class cl = start; cl != null; cl = cl.getSuperclass()) {
      Method[] methods = getPublicDeclaredMethods(cl);
      for (int j = 0; j < methods.length; j++) {
        Method method = methods[j];
        if (method == null)
          continue; 
        int mods = method.getModifiers();
        if (Modifier.isStatic(mods))
          continue; 
        Class[] params = method.getParameterTypes();
        if (method.getName().equals(methodName) && params.length == argCount) {
          boolean different = false;
          if (argCount > 0) {
            for (int k = 0; k < argCount; k++) {
              if (params[k] != args[k])
                different = true; 
            } 
            if (different)
              continue; 
          } 
          return method;
        } 
        continue;
      } 
    } 
    Class[] ifcs = start.getInterfaces();
    for (int i = 0; i < ifcs.length; i++) {
      Method m = internalFindMethod(ifcs[i], methodName, argCount);
      if (m != null)
        return m; 
    } 
    return null;
  }
  
  static Method findMethod(Class cls, String methodName, int argCount) throws IntrospectionException {
    if (methodName == null)
      return null; 
    Method m = internalFindMethod(cls, methodName, argCount);
    if (m != null)
      return m; 
    throw new IntrospectionException("No method \"" + methodName + "\" with " + argCount + " arg(s)");
  }
  
  static Method findMethod(Class cls, String methodName, int argCount, Class[] args) throws IntrospectionException {
    if (methodName == null)
      return null; 
    Method m = internalFindMethod(cls, methodName, argCount, args);
    if (m != null)
      return m; 
    throw new IntrospectionException("No method \"" + methodName + "\" with " + argCount + " arg(s) of matching types.");
  }
  
  static boolean isSubclass(Class a, Class b) {
    if (a == b)
      return true; 
    if (a == null || b == null)
      return false; 
    for (Class x = a; x != null; x = x.getSuperclass()) {
      if (x == b)
        return true; 
      if (b.isInterface()) {
        Class[] interfaces = x.getInterfaces();
        for (int i = 0; i < interfaces.length; i++) {
          if (isSubclass(interfaces[i], b))
            return true; 
        } 
      } 
    } 
    return false;
  }
  
  private boolean throwsException(Method method, Class exception) {
    Class[] exs = method.getExceptionTypes();
    for (int i = 0; i < exs.length; i++) {
      if (exs[i] == exception)
        return true; 
    } 
    return false;
  }
}
