package com.sptci;

import java.beans.PropertyChangeEvent;
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.text.NumberFormat;
import java.text.ParseException;
import java.util.HashMap;
import java.util.logging.Logger;

/**
 * An implementation of <code>PropertyChangeListener</code> used
 * to synchronise changes made to properties in UI components to the
 * {@link #bean} java bean.
 *
 * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
 * @author Rakesh Vidyadharan 2006-01-22
 * @version $Id: PropertyChangeListener.java,v 1.3 2006/02/08 23:31:16 rakesh Exp $
 */
public class PropertyChangeListener 
  implements java.beans.PropertyChangeListener
{
  /**
   * The logger used to log errors/warnings.
   */
  private static final Logger logger =
    Logger.getLogger( "com.sptci.PropertyChangeListener" );

  /**
   * A <code>HashMap</code> that provides the mapping for primitives
   * to their object wrappers.
   */
  protected static final HashMap<String, String> typeMapping;

  /**
   * Initialise the {@link #typeMapping} map.
   */
  static
  {
    typeMapping = new HashMap<String, String>();
    typeMapping.put( "boolean", "java.lang.Boolean" );
    typeMapping.put( "byte", "java.lang.Byte" );
    typeMapping.put( "char", "java.lang.Character" );
    typeMapping.put( "double", "java.lang.Double" );
    typeMapping.put( "float", "java.lang.Float" );
    typeMapping.put( "int", "java.lang.Integer" );
    typeMapping.put( "long", "java.lang.Long" );
    typeMapping.put( "short", "java.lang.Short" );
  }

  /**
   * The java bean which is to be kept synchronised with updates
   * made to UI components.
   */
  protected Object bean;

  /**
   * A <code>HashMap</code> of all the properties defined in the {@link
   * #bean}.
   */
  protected HashMap<String, PropertyDescriptor> properties;

  /**
   * Default constructor.  Initialises {@link #properties}.
   */
  protected PropertyChangeListener() 
  {
    properties = new HashMap<String, PropertyDescriptor>();
  }

  /**
   * Create a new instance of the class using the specified java bean.
   *
   * @see #setBean
   * @param bean The data object which is to be managed.
   * @throws IntrospectionException If errors are encountered while
   *   introspecting the {@link #bean} class.
   */
  public PropertyChangeListener( Object bean )
    throws IntrospectionException
  {
    this();
    setBean( bean );
  }

  /**
   * Implementation of the method defined in <code>
   * PropertyChangeListener</code>.
   *
   * @see #modifyPrimitive
   * @see #modifyObject
   * @param event A <code>PropertyChangeEvent</code> object describing 
   *   the event source and the property that has changed.
   */
  public void propertyChange( PropertyChangeEvent event )
  {
    PropertyDescriptor descriptor = 
      properties.get( event.getPropertyName() );
    if ( descriptor != null )
    {
      if ( descriptor.getWriteMethod() == null )
      {
        throw new RuntimeException( "No write method for property: " +
            event.getPropertyName() + " in bean: " +
            bean.getClass().getName() );
      }

      if ( descriptor.getPropertyType().isPrimitive() )
      {
        modifyPrimitive( event, descriptor );
      }
      else
      {
        modifyObject( event, descriptor );
      }
    }
    else
    {
      logger.warning( "No PropertyDescriptor for " +
          event.getPropertyName() );
    }
  }

  /**
   * Populate {@link #properties} by introspecting the {@link #bean}.
   *
   * @throws IntrospectionException If errors are encountered while
   *   introspecting the {@link #bean} class.
   */
  protected void initProperties() throws IntrospectionException
  {
    properties = new HashMap<String, PropertyDescriptor>();
    BeanInfo beanInfo = Introspector.getBeanInfo( bean.getClass() );
    for ( PropertyDescriptor descriptor : beanInfo.getPropertyDescriptors() )
    {
      properties.put( descriptor.getName(), descriptor );
    }
  }

  /**
   * Modify a primitive property in {@link #bean}.
   *
   * @param event A <code>PropertyChangeEvent</code> object that
   *   contains the property to change and the old and new values.
   * @param descriptor A <code>PropertyDescriptor</code> object that
   *   describes the property
   * @throws RuntimeException If the appropriate mutator method
   *   for the property cannot be accessed, or if the specified value
   *   does not directly match a primitive type or is not a string.
   */
  protected void modifyPrimitive( PropertyChangeEvent event,
      PropertyDescriptor descriptor ) throws RuntimeException
  {
    String type = descriptor.getPropertyType().getName();

    try
    {
      if ( event.getNewValue() instanceof java.lang.String )
      {
        descriptor.getWriteMethod().invoke( 
            bean, objectFromString( event, descriptor ) );
      }
      else if ( ( type.equals( "boolean" ) && 
            event.getOldValue() instanceof java.lang.Boolean ) || 
          ( type.equals( "byte" ) && 
            event.getOldValue() instanceof java.lang.Byte ) || 
          ( type.equals( "char" ) && 
            event.getOldValue() instanceof java.lang.Character ) || 
          ( type.equals( "double" ) && 
            event.getOldValue() instanceof java.lang.Double ) ||
          ( type.equals( "float" ) && 
            event.getOldValue() instanceof java.lang.Float ) ||
          ( type.equals( "int" ) &&
            event.getOldValue() instanceof java.lang.Integer ) ||
          ( type.equals( "long" ) && 
            event.getOldValue() instanceof java.lang.Long ) ||
          ( type.equals( "short" ) &&
            event.getOldValue() instanceof java.lang.Short ) )
      {
        descriptor.getWriteMethod().invoke( bean, event.getNewValue() );
      }
      else
      {
        throw new RuntimeException( 
            "Unable to properly convert new value type " +
            event.getNewValue().getClass().getName() + 
            " to primitive: " + type );
      }
    }
    catch ( IllegalAccessException iaex )
    {
      throw new RuntimeException( iaex.getMessage(), iaex );
    }
    catch ( java.lang.reflect.InvocationTargetException itex )
    {
      throw new RuntimeException( itex.getMessage(), itex );
    }
  }

  /**
   * Convert the given String value to an Object wrapper of the
   * appropriate kind.
   *
   * @param event A <code>PropertyChangeEvent</code> object that
   *   contains the property to change and the old and new values.
   * @param descriptor A <code>PropertyDescriptor</code> object that
   *   describes the property
   * @throws RuntimeException If errors are encountered while trying
   *   to initialise the wrapper object.
   */
  protected Object objectFromString( PropertyChangeEvent event,
      PropertyDescriptor descriptor ) throws RuntimeException
  {
    String type = descriptor.getPropertyType().getName();
    String value = (String) event.getNewValue();
    if ( value != null && value.length() > 0 )
    {
      try
      {
        Number number = NumberFormat.getInstance().parse( value );
        value = number.toString();
      }
      catch ( ParseException pex ) {}
    }

    try
    {
      Class cls = Class.forName( 
          typeMapping.get( descriptor.getPropertyType().getName() ) );
      Class[] args = new Class[]{ java.lang.String.class };
      return ( cls.getConstructor( args ).newInstance( value ) );
    }
    catch ( Throwable t )
    {
      throw new RuntimeException( t.getMessage(), t );
    }
  }

  /**
   * Modify an object property in {@link #bean}.
   *
   * @param event A <code>PropertyChangeEvent</code> object that
   *   contains the property to change and the old and new values.
   * @param descriptor A <code>PropertyDescriptor</code> object that
   *   describes the property
   * @throws RuntimeException If errors are encountered while trying
   *   to invoke the mutator method.
   */
  protected void modifyObject( PropertyChangeEvent event,
      PropertyDescriptor descriptor ) throws RuntimeException
  {
    if ( event.getNewValue() == null ||
        event.getNewValue().getClass() == descriptor.getPropertyType() )
    {
      try
      {
        descriptor.getWriteMethod().invoke( bean, event.getNewValue() );
      }
      catch ( Throwable t )
      {
        throw new RuntimeException( t.getMessage(), t );
      }
    }
    else
    {
      throw new RuntimeException( "Value specified for (" +
          event.getPropertyName() + ") has type: " +
          event.getNewValue().getClass().getName() + 
          " while property (" + descriptor.getName() +
          ") has type: " + descriptor.getPropertyType().getName() );
    }
  }
  
  /**
   * Returns {@link #bean}.
   *
   * @return Object The value/reference of/to bean.
   */
  public final Object getBean()
  {
    return bean;
  }
  
  /**
   * Set {@link #bean}. Re-initialises {@link #properties}.
   *
   * @see #initProperties
   * @param bean The value to set.
   * @throws IntrospectionException If errors are encountered while
   *   introspecting the {@link #bean} class.
   */
  public void setBean( Object bean ) throws IntrospectionException
  {
    boolean init = false;
    if ( this.bean == null )
    {
      init = true;
    }
    else if ( ! this.bean.getClass().equals( bean.getClass() ) )
    {
      init = true;
    }

    this.bean = bean;
    if ( init ) initProperties();
  }
  
  /**
   * Returns a shallow-clone of {@link #properties}.
   *
   * @return HashMap The value/reference of/to properties.
   */
  public final HashMap<String, PropertyDescriptor> getProperties()
  {
    return ( (HashMap<String, PropertyDescriptor>) properties.clone() );
  }
}
