package com.sptci.echo2;

import java.beans.PropertyChangeEvent;
import java.io.Serializable;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import java.util.logging.Logger;

import nextapp.echo2.app.ListBox;
import nextapp.echo2.app.SelectField;
import nextapp.echo2.app.button.ToggleButton;
import nextapp.echo2.app.list.DefaultListModel;

import com.sptci.ReflectionUtility;

/**
 * The <code>PropertyChangeListener</code> for change events fired
 * by java beans to update <code>Echo2</code> UI container classes
 * (represented by {@link #bean}).
 *
 * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
 * @author Rakesh Vidyadharan 2006-01-25
 * @version $Id: ViewPropertyChangeListener.java,v 1.2 2006/02/15 00:48:26 rakesh Exp $
 */
public class ViewPropertyChangeListener extends PropertyChangeListener 
  implements Serializable
{
  /**
   * The logger used to log errors/warnings to.
   */
  private static transient final Logger logger =
    Logger.getLogger( "com.sptci.echo2.ViewPropertyChangeListener" );

  /**
   * A map of all the <code>Field</code>s in the {@link #bean}.
   */
  private transient HashMap<String, Field> fields;

  /**
   * A map of the appropriate methods to be invoked for the elements
   * in {@link #fields}.
   */
  private transient HashMap<Field, Method> methods;

  /**
   * Create a new instance of the listener for the specified object.
   *
   * @see #setBean
   * @param bean The object whose fields are to be updated by
   *   change events.
   */
  public ViewPropertyChangeListener( Object bean )
  {
    super();
    setBean( bean );
  }

  /**
   * Implementation of the event handling method.
   *
   * @see #parseName
   * @see #processList
   * @see #processMap
   * @param event A <code>PropertyChangeEvent</code> object describing
   * the event source and the property that has changed.
   * @throws BindingException If errors are encountered while
   *   attempting to modify the property in {@link #bean}.
   */
  public void propertyChange( PropertyChangeEvent event )
    throws BindingException
  {
    try
    {
      String name = parseName( event.getPropertyName() );
      Field field = fields.get( name );
      if ( field == null ) 
      {
        throw new BindingException( "No field in bean " + 
            bean.getClass() + " with name " + name );
      }

      Object object = field.get( bean );
      Method method = methods.get( field );
      if ( method == null )
      {
        processList( object, event, field );
      }
      else
      {
        if ( object instanceof java.util.Map )
        {
          processMap( (Map) object, event );
        }
        else
        {
          method.invoke( object, event.getNewValue() );
        }
      }
    }
    catch ( Throwable t )
    {
      throw new BindingException( t.getMessage(), t );
    }
  }

  /**
   * Introspect the fields of {@link #bean} and populate the
   * {@link #fields} and {@link #methods} fields.
   *
   * @throws BindingException If errors are encountered while
   *   introspecting the {@link #bean}.
   */
  private void introspect() throws BindingException
  {
    if ( bean == null ) return;
    fields = new HashMap<String, Field>();
    methods = new HashMap<Field, Method>();

    try
    {
      for ( Field field : bean.getClass().getDeclaredFields() ) 
      {
        fields.put( field.getName(), field );

        if ( ! Modifier.isPublic( field.getModifiers() ) )
        {
          field.setAccessible( true );
        }

        Object object = field.get( bean );
        if ( object instanceof  nextapp.echo2.app.text.TextComponent )
        {
          Method method = ReflectionUtility.fetchMethod( field, 
              "setText", new Class[] { java.lang.String.class } );
          methods.put( field, method );
        }
        else if ( object instanceof  
            nextapp.echo2.app.button.ToggleButton )
        {
          Method method = ReflectionUtility.fetchMethod( field, 
              "setSelected", new Class[] { java.lang.String.class } );
          methods.put( field, method );
        }
        else if ( object instanceof java.util.Map )
        {
          Method method = ReflectionUtility.fetchMethod( field, 
              "get", new Class[] { java.lang.Object.class } );
          methods.put( field, method );
        }
        else 
        { 
          logger.fine( "No method for object type: " + field.getType() ); 
        }
      }
    }
    catch ( Throwable t )
    {
      throw new BindingException( t );
    }
  }

  /**
   * Update <code>AbstractList</code> UI components.
   *
   * @param object The UI component that is to be updated.
   * @param event A <code>PropertyChangeEvent</code> object describing
   *   the event source and the property that has changed.
   * @param field The field that represents the UI component that is
   *   to be updated.
   * @throws BindingException If errors are encountered while
   *   attempting to modify the property in {@link #bean}.
   */
  protected void processList( Object object, PropertyChangeEvent event,
      Field field ) throws BindingException
  {
    if ( object instanceof nextapp.echo2.app.SelectField )
    {
      updateSelectField( (SelectField) object, event );
    }
    else if ( object instanceof nextapp.echo2.app.ListBox )
    {
      updateListBox( (ListBox) object, event );
    }
    else
    {
      throw new BindingException( "No method found for field " + field );
    }
  }

  /**
   * Process a <code>SelectField</code> UI component.
   *
   * @param selectField The UI component that is to be updated.
   * @param event A <code>PropertyChangeEvent</code> object describing
   * the event source and the property that has changed.
   */
  protected void updateSelectField( SelectField selectField,
      PropertyChangeEvent event )
  {
    DefaultListModel model = new DefaultListModel();
    selectField.setSelectedIndex( 0 );
    int count = 0;
    for ( ListItem item : (List<ListItem>) event.getNewValue() )
    {
      model.add( count, item.getValue() );
      if ( item.isSelected() )
      {
        selectField.setSelectedIndex( count );
      }
      ++count;
    }

    selectField.setModel( model );
  }

  /**
   * Update a <code>ListBox</code> UI component.
   *
   * @param listBox The ListBox UI component that is to be updated.
   * @param event A <code>PropertyChangeEvent</code> object describing
   *   the event source and the property that has changed.
   */
  protected void updateListBox( ListBox listBox, 
      PropertyChangeEvent event )
  {
    DefaultListModel model = new DefaultListModel();
    int count = 0;
    ArrayList<Integer> indices = new ArrayList<Integer>();

    for ( ListItem item : (List<ListItem>) event.getNewValue() )
    {
      model.add( count, item.getValue() );
      if ( item.isSelected() )
      {
        indices.add( count );
      }
      ++count;
    }

    listBox.setModel( model );

    int[] selectedIndices = new int[ indices.size() ];
    count = 0;
    for ( int i : indices )
    {
      selectedIndices[ count++ ] = i;
    }

    listBox.setSelectedIndices( selectedIndices );
  }

  /**
   * Update <code>ToggleButton</code> UI components.
   *
   * @param map The map that contains the name and selected state
   *   of the toggle button.
   */
  protected void processMap( Map map, PropertyChangeEvent event )
  {
    for ( Iterator iterator = map.entrySet().iterator();
        iterator.hasNext(); )
    {
      Map.Entry entry = (Map.Entry) iterator.next();
      Map newValue = (Map) event.getNewValue();
      String key = (String) entry.getKey();

      if ( key != null && key.indexOf( ">" ) != -1 )
      {
        key = key.substring( key.indexOf( ">" ) + 1 );
      }

      if ( newValue.containsKey( key ) )
      {
        if ( entry.getValue() instanceof 
            nextapp.echo2.app.button.ToggleButton )
        {
          ToggleButton button = (ToggleButton) entry.getValue();
          button.setSelected( 
              ( (Boolean) newValue.get( key ) ).booleanValue() );
        }
      }
      else 
      { 
        logger.info( newValue.toString() ); 
        logger.info( "No key " + key + " found in map" );
      }
    }
  }
  
  /**
   * Set {@link #bean}.
   *
   * @see #introspect
   * @param bean The value to set.
   */
  public void setBean( Object bean )
  {
    boolean introspect = false;
    if ( this.bean == null )
    {
      introspect = true;
    }
    else if ( ! this.bean.getClass().equals( bean.getClass() ) )
    {
      introspect = true;
    }

    this.bean = bean;
    if ( introspect ) introspect();
  }
}

