package org.apache.commons.beanutils;

import java.beans.IndexedPropertyDescriptor;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Array;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.apache.commons.collections.FastHashMap;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

public class BeanUtils {
  private static FastHashMap dummy = new FastHashMap();
  
  private static Log log = LogFactory.getLog(BeanUtils.class);
  
  private static int debug = 0;
  
  public static int getDebug() {
    return debug;
  }
  
  public static void setDebug(int newDebug) {
    debug = newDebug;
  }
  
  public static Object cloneBean(Object bean) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException {
    if (log.isDebugEnabled())
      log.debug("Cloning bean: " + bean.getClass().getName()); 
    Class clazz = bean.getClass();
    Object newBean = clazz.newInstance();
    PropertyUtils.copyProperties(newBean, bean);
    return newBean;
  }
  
  public static void copyProperties(Object dest, Object orig) throws IllegalAccessException, InvocationTargetException {
    if (dest == null)
      throw new IllegalArgumentException("No destination bean specified"); 
    if (orig == null)
      throw new IllegalArgumentException("No origin bean specified"); 
    if (log.isDebugEnabled())
      log.debug("BeanUtils.copyProperties(" + dest + ", " + orig + ")"); 
    if (orig instanceof DynaBean) {
      DynaProperty[] origDescriptors = ((DynaBean)orig).getDynaClass().getDynaProperties();
      for (int i = 0; i < origDescriptors.length; i++) {
        String name = origDescriptors[i].getName();
        if (PropertyUtils.isWriteable(dest, name)) {
          Object value = ((DynaBean)orig).get(name);
          copyProperty(dest, name, value);
        } 
      } 
    } else if (orig instanceof Map) {
      Iterator names = ((Map)orig).keySet().iterator();
      while (names.hasNext()) {
        String name = names.next();
        if (PropertyUtils.isWriteable(dest, name)) {
          Object value = ((Map)orig).get(name);
          copyProperty(dest, name, value);
        } 
      } 
    } else {
      PropertyDescriptor[] origDescriptors = PropertyUtils.getPropertyDescriptors(orig);
      for (int i = 0; i < origDescriptors.length; i++) {
        String name = origDescriptors[i].getName();
        if (!"class".equals(name))
          if (PropertyUtils.isReadable(orig, name) && PropertyUtils.isWriteable(dest, name))
            try {
              Object value = PropertyUtils.getSimpleProperty(orig, name);
              copyProperty(dest, name, value);
            } catch (NoSuchMethodException e) {}  
      } 
    } 
  }
  
  public static void copyProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException {
    if (log.isTraceEnabled()) {
      StringBuffer sb = new StringBuffer("  copyProperty(");
      sb.append(bean);
      sb.append(", ");
      sb.append(name);
      sb.append(", ");
      if (value == null) {
        sb.append("<NULL>");
      } else if (value instanceof String) {
        sb.append((String)value);
      } else if (value instanceof String[]) {
        String[] values = (String[])value;
        sb.append('[');
        for (int k = 0; k < values.length; k++) {
          if (k > 0)
            sb.append(','); 
          sb.append(values[k]);
        } 
        sb.append(']');
      } else {
        sb.append(value.toString());
      } 
      sb.append(')');
      log.trace(sb.toString());
    } 
    Object target = bean;
    int delim = name.lastIndexOf('.');
    if (delim >= 0) {
      try {
        target = PropertyUtils.getProperty(bean, name.substring(0, delim));
      } catch (NoSuchMethodException e) {
        return;
      } 
      name = name.substring(delim + 1);
      if (log.isTraceEnabled()) {
        log.trace("    Target bean = " + target);
        log.trace("    Target name = " + name);
      } 
    } 
    String propName = null;
    Class type = null;
    int index = -1;
    String key = null;
    propName = name;
    int i = propName.indexOf('[');
    if (i >= 0) {
      int k = propName.indexOf(']');
      try {
        index = Integer.parseInt(propName.substring(i + 1, k));
      } catch (NumberFormatException e) {}
      propName = propName.substring(0, i);
    } 
    int j = propName.indexOf('(');
    if (j >= 0) {
      int k = propName.indexOf(')');
      try {
        key = propName.substring(j + 1, k);
      } catch (IndexOutOfBoundsException e) {}
      propName = propName.substring(0, j);
    } 
    if (target instanceof DynaBean) {
      DynaClass dynaClass = ((DynaBean)target).getDynaClass();
      DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
      if (dynaProperty == null)
        return; 
      type = dynaProperty.getType();
    } else {
      PropertyDescriptor descriptor = null;
      try {
        descriptor = PropertyUtils.getPropertyDescriptor(target, name);
        if (descriptor == null)
          return; 
      } catch (NoSuchMethodException e) {
        return;
      } 
      type = descriptor.getPropertyType();
      if (type == null) {
        if (log.isTraceEnabled())
          log.trace("    target type for property '" + propName + "' is null, so skipping ths setter"); 
        return;
      } 
    } 
    if (log.isTraceEnabled())
      log.trace("    target propName=" + propName + ", type=" + type + ", index=" + index + ", key=" + key); 
    if (index >= 0) {
      Converter converter = ConvertUtils.lookup(type.getComponentType());
      if (converter != null) {
        log.trace("        USING CONVERTER " + converter);
        value = converter.convert(type, value);
      } 
      try {
        PropertyUtils.setIndexedProperty(target, propName, index, value);
      } catch (NoSuchMethodException e) {
        throw new InvocationTargetException(e, "Cannot set " + propName);
      } 
    } else if (key != null) {
      try {
        PropertyUtils.setMappedProperty(target, propName, key, value);
      } catch (NoSuchMethodException e) {
        throw new InvocationTargetException(e, "Cannot set " + propName);
      } 
    } else {
      Converter converter = ConvertUtils.lookup(type);
      if (converter != null) {
        log.trace("        USING CONVERTER " + converter);
        value = converter.convert(type, value);
      } 
      try {
        PropertyUtils.setSimpleProperty(target, propName, value);
      } catch (NoSuchMethodException e) {
        throw new InvocationTargetException(e, "Cannot set " + propName);
      } 
    } 
  }
  
  public static Map describe(Object bean) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    if (bean == null)
      return new HashMap(); 
    if (log.isDebugEnabled())
      log.debug("Describing bean: " + bean.getClass().getName()); 
    Map description = new HashMap();
    if (bean instanceof DynaBean) {
      DynaProperty[] descriptors = ((DynaBean)bean).getDynaClass().getDynaProperties();
      for (int i = 0; i < descriptors.length; i++) {
        String name = descriptors[i].getName();
        description.put(name, getProperty(bean, name));
      } 
    } else {
      PropertyDescriptor[] descriptors = PropertyUtils.getPropertyDescriptors(bean);
      for (int i = 0; i < descriptors.length; i++) {
        String name = descriptors[i].getName();
        if (descriptors[i].getReadMethod() != null)
          description.put(name, getProperty(bean, name)); 
      } 
    } 
    return description;
  }
  
  public static String[] getArrayProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getProperty(bean, name);
    if (value == null)
      return null; 
    if (value instanceof Collection) {
      ArrayList values = new ArrayList();
      Iterator items = ((Collection)value).iterator();
      while (items.hasNext()) {
        Object item = items.next();
        if (item == null) {
          values.add((String)null);
          continue;
        } 
        values.add(item.toString());
      } 
      return values.<String>toArray(new String[values.size()]);
    } 
    if (value.getClass().isArray()) {
      int n = Array.getLength(value);
      String[] arrayOfString = new String[n];
      for (int i = 0; i < n; i++) {
        Object item = Array.get(value, i);
        if (item == null) {
          arrayOfString[i] = null;
        } else {
          arrayOfString[i] = item.toString();
        } 
      } 
      return arrayOfString;
    } 
    String[] results = new String[1];
    results[0] = value.toString();
    return results;
  }
  
  public static String getIndexedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getIndexedProperty(bean, name);
    return ConvertUtils.convert(value);
  }
  
  public static String getIndexedProperty(Object bean, String name, int index) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getIndexedProperty(bean, name, index);
    return ConvertUtils.convert(value);
  }
  
  public static String getMappedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getMappedProperty(bean, name);
    return ConvertUtils.convert(value);
  }
  
  public static String getMappedProperty(Object bean, String name, String key) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getMappedProperty(bean, name, key);
    return ConvertUtils.convert(value);
  }
  
  public static String getNestedProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getNestedProperty(bean, name);
    return ConvertUtils.convert(value);
  }
  
  public static String getProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    return getNestedProperty(bean, name);
  }
  
  public static String getSimpleProperty(Object bean, String name) throws IllegalAccessException, InvocationTargetException, NoSuchMethodException {
    Object value = PropertyUtils.getSimpleProperty(bean, name);
    return ConvertUtils.convert(value);
  }
  
  public static void populate(Object bean, Map properties) throws IllegalAccessException, InvocationTargetException {
    if (bean == null || properties == null)
      return; 
    if (log.isDebugEnabled())
      log.debug("BeanUtils.populate(" + bean + ", " + properties + ")"); 
    Iterator names = properties.keySet().iterator();
    while (names.hasNext()) {
      String name = names.next();
      if (name == null)
        continue; 
      Object value = properties.get(name);
      setProperty(bean, name, value);
    } 
  }
  
  public static void setProperty(Object bean, String name, Object value) throws IllegalAccessException, InvocationTargetException {
    if (log.isTraceEnabled()) {
      StringBuffer sb = new StringBuffer("  setProperty(");
      sb.append(bean);
      sb.append(", ");
      sb.append(name);
      sb.append(", ");
      if (value == null) {
        sb.append("<NULL>");
      } else if (value instanceof String) {
        sb.append((String)value);
      } else if (value instanceof String[]) {
        String[] values = (String[])value;
        sb.append('[');
        for (int k = 0; k < values.length; k++) {
          if (k > 0)
            sb.append(','); 
          sb.append(values[k]);
        } 
        sb.append(']');
      } else {
        sb.append(value.toString());
      } 
      sb.append(')');
      log.trace(sb.toString());
    } 
    Object target = bean;
    int delim = name.lastIndexOf('.');
    if (delim >= 0) {
      try {
        target = PropertyUtils.getProperty(bean, name.substring(0, delim));
      } catch (NoSuchMethodException e) {
        return;
      } 
      name = name.substring(delim + 1);
      if (log.isTraceEnabled()) {
        log.trace("    Target bean = " + target);
        log.trace("    Target name = " + name);
      } 
    } 
    String propName = null;
    Class type = null;
    int index = -1;
    String key = null;
    propName = name;
    int i = propName.indexOf('[');
    if (i >= 0) {
      int k = propName.indexOf(']');
      try {
        index = Integer.parseInt(propName.substring(i + 1, k));
      } catch (NumberFormatException e) {}
      propName = propName.substring(0, i);
    } 
    int j = propName.indexOf('(');
    if (j >= 0) {
      int k = propName.indexOf(')');
      try {
        key = propName.substring(j + 1, k);
      } catch (IndexOutOfBoundsException e) {}
      propName = propName.substring(0, j);
    } 
    if (target instanceof DynaBean) {
      DynaClass dynaClass = ((DynaBean)target).getDynaClass();
      DynaProperty dynaProperty = dynaClass.getDynaProperty(propName);
      if (dynaProperty == null)
        return; 
      type = dynaProperty.getType();
    } else {
      PropertyDescriptor descriptor = null;
      try {
        descriptor = PropertyUtils.getPropertyDescriptor(target, name);
        if (descriptor == null)
          return; 
      } catch (NoSuchMethodException e) {
        return;
      } 
      if (descriptor instanceof MappedPropertyDescriptor) {
        if (((MappedPropertyDescriptor)descriptor).getMappedWriteMethod() == null) {
          if (log.isDebugEnabled())
            log.debug("Skipping read-only property"); 
          return;
        } 
        type = ((MappedPropertyDescriptor)descriptor).getMappedPropertyType();
      } else if (descriptor instanceof IndexedPropertyDescriptor) {
        if (((IndexedPropertyDescriptor)descriptor).getIndexedWriteMethod() == null) {
          if (log.isDebugEnabled())
            log.debug("Skipping read-only property"); 
          return;
        } 
        type = ((IndexedPropertyDescriptor)descriptor).getIndexedPropertyType();
      } else {
        if (descriptor.getWriteMethod() == null) {
          if (log.isDebugEnabled())
            log.debug("Skipping read-only property"); 
          return;
        } 
        type = descriptor.getPropertyType();
      } 
    } 
    Object newValue = null;
    if (type.isArray() && index < 0) {
      if (value == null) {
        String[] values = new String[1];
        values[0] = (String)value;
        newValue = ConvertUtils.convert(values, type);
      } else if (value instanceof String) {
        String[] values = new String[1];
        values[0] = (String)value;
        newValue = ConvertUtils.convert(values, type);
      } else if (value instanceof String[]) {
        newValue = ConvertUtils.convert((String[])value, type);
      } else {
        newValue = value;
      } 
    } else if (type.isArray()) {
      if (value instanceof String) {
        newValue = ConvertUtils.convert((String)value, type.getComponentType());
      } else if (value instanceof String[]) {
        newValue = ConvertUtils.convert(((String[])value)[0], type.getComponentType());
      } else {
        newValue = value;
      } 
    } else if (value instanceof String || value == null) {
      newValue = ConvertUtils.convert((String)value, type);
    } else if (value instanceof String[]) {
      newValue = ConvertUtils.convert(((String[])value)[0], type);
    } else if (ConvertUtils.lookup(value.getClass()) != null) {
      newValue = ConvertUtils.convert(value.toString(), type);
    } else {
      newValue = value;
    } 
    try {
      if (index >= 0) {
        PropertyUtils.setIndexedProperty(target, propName, index, newValue);
      } else if (key != null) {
        PropertyUtils.setMappedProperty(target, propName, key, newValue);
      } else {
        PropertyUtils.setProperty(target, propName, newValue);
      } 
    } catch (NoSuchMethodException e) {
      throw new InvocationTargetException(e, "Cannot set " + propName);
    } 
  }
}
