package com.sptci.echo2;

import java.io.IOException;

import java.lang.reflect.Field;

import java.util.ArrayList;
import java.util.Date;
import java.util.Formatter;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;

import java.text.SimpleDateFormat;

import com.sptci.util.StringUtilities;

import com.sptci.CodeGenerator;
import com.sptci.ReflectionUtility;

/**
 * A JavaBean generator.  Uses reflection to create a JavaBean that
 * maps to the UI component fields in the specified class.
 *
 * <p>The following shows the invocation semantics for this class:</p>
 * <pre>
 *  java -classpath &lt;path to classes&gt;:sptecho.jar:&lt;path to Echo2_App.jar&gt; \
 *    com.sptci.echo2.ModelGenerator \
 *    &lt;fully qualified name of UI class&gt;
 * </pre>
 *
 * <p>Copyright 2006 Sans Pareil Technologies, Inc.</p>
 * @author Rakesh Vidyadharan 2006-02-07
 * @version $Id: ModelGenerator.java,v 1.3 2006/02/15 00:48:26 rakesh Exp $
 */
public class ModelGenerator extends CodeGenerator
{
  /**
   * The class for which the JavaBean is to be generated.
   */
  protected Class source;

  /**
   * A <code>List</code> of import statements for the generated class.
   */
  protected List<String> imports;

  /**
   * A <code>Map</code> of <code>fields</code> in {@link #source}.
   */
  protected Map<String, Field> uiFields;

  /**
   * Instantiates a new instance of {@link #source} from the specified
   * <code>name</code>.  Initialises code geneartor containers.
   *
   * @param name The fully qualified name of the class.  Example:
   *   <code>com.sptci.demo.InputForm</code>.
   * @throws ClassNotFoundException If the class corresponding to the
   *   <code>name</code> cannot be found in the classpath.
   */
  public ModelGenerator( String name ) throws ClassNotFoundException
  {
    super();
    source = Class.forName( name );

    this.name = source.getName().substring( 
        source.getName().lastIndexOf( "." ) + 1 ) + "Model";
    imports = new ArrayList<String>();
    imports.add( "java.beans.PropertyChangeListener" );
    imports.add( "java.beans.PropertyChangeSupport" );
    imports.add( "java.io.Serializable" );
    imports.add( "java.util.Formatter" );
    uiFields = new TreeMap<String, Field>();
  }

  /**
   * Load the class specified, and generate a JavaBean representing its
   * UI components.
   *
   * @see #processComponent
   * @see #generateSource
   * @throws Exception If errors are encountered while writing to 
   *   the source file.
   */
  @Override
  public void generate() throws Exception
  {
    for ( Map.Entry<String, Field> entry : 
        ReflectionUtility.fetchFields( source ).entrySet() )
    {
      Field field = entry.getValue();
      try
      {
        source.asSubclass( nextapp.echo2.app.Component.class );
        uiFields.put( field.getName(), field );
        processComponent( field );
      }
      catch ( ClassCastException ccex ) {}
    }

    generateSource();
  }

  /**
   * Generate a <code>Field</code> for the JavaBean class that will be
   * generated from the specified Echo2 UI component
   *
   * @param field The <code>Field</code> that represents the
   *   Echo2 component.
   */
  protected void processComponent( Field field )
  {
    try
    {
      field.getType().asSubclass( 
          nextapp.echo2.app.text.TextComponent.class );
      fields.put( field.getName(), "String" );
    }
    catch ( ClassCastException ccex ) {}

    try
    {
      field.getType().asSubclass( 
          java.util.Map.class );

      if ( ! imports.contains( "java.util.Map" ) )
      {
        imports.add( "java.util.Map" );
        imports.add( "java.util.TreeMap" );
      }

      fields.put( field.getName(), "Map<String, Boolean>" );
    }
    catch ( ClassCastException ccex ) {}

    try
    {
      field.getType().asSubclass( 
          nextapp.echo2.app.list.AbstractListComponent.class );

      if ( ! imports.contains( "java.util.ArrayList" ) )
      {
        imports.add( "java.util.ArrayList" );
        imports.add( "java.util.List" );
        imports.add( "com.sptci.echo2.ListItem" );
      }

      fields.put( field.getName(), "List<ListItem>" );
    }
    catch ( ClassCastException ccex ) {}
  }

  /**
   * Generate the source code for the JavaBean.
   *
   * @throws IOException If errors are encountered while writing the
   *   generated source file.
   */
  protected void generateSource() throws IOException
  {
    StringBuilder builder = new StringBuilder( 4096 );
    Formatter formatter = new Formatter( builder );

    formatter.format( "package %s;%n%n", source.getPackage().getName() );

    for ( String name : imports )
    {
      formatter.format( "import %s;%n", name );
    }
    formatter.format( "%n" );

    formatter.format( "/**%n" );
    formatter.format( 
        " * A JavaBean that represents the data represented by %n" );
    formatter.format( " * {@link %s}.%n", source.getName() );
    formatter.format( " *%n" );
    formatter.format( " * @author %s %s%n", getClass().getName(),
        ( new SimpleDateFormat( "yyyy-MM-dd HH:mm:ssZ" ) ).format( new Date() ) );
    formatter.format( " * @version $Id: ModelGenerator.java,v 1.3 2006/02/15 00:48:26 rakesh Exp $%n" );
    formatter.format( " */%n" );
    formatter.format( "public class %s%n", name );
    formatter.format( "  implements Cloneable, Comparable<%s>, Serializable%n",
        name );
    formatter.format( "{%n" );

    builder.append( generateFields() );
    builder.append( generateConstructors() );
    builder.append( generateMethods() );

    formatter.format( "}%n" );

    StringUtilities.toFile( builder.toString(), name + ".java" ); 
  }

  /**
   * Generate the fields for the JavaBean. Over-ridden to add code
   * specific to the JavaBean.
   *
   * @see #generatePropertyChangeFields
   * @return String The field declarations for the JavaBean.
   */
  protected String generateFields()
  {
    StringBuilder builder = new StringBuilder( 4096 );
    Formatter formatter = new Formatter( builder );

    builder.append( super.generateFields() );

    for ( Map.Entry<String, String> entry : fields.entrySet() )
    {
      formatter.format( "  /**%n" );
      formatter.format( 
          "   * Field that represents the %s field of type%n",
          entry.getKey() );
      formatter.format( "   * %s%n", 
          uiFields.get( entry.getKey() ).getName() );
      formatter.format( "   */%n" );
      formatter.format( "  private %s %s;%n%n",
          entry.getValue(), entry.getKey() );
    }

    builder.append( generatePropertyChangeFields() );

    return builder.toString();
  }

  /**
   * Generate the fields necessary to support PropertyChangeListener
   * ability for the bean.
   *
   * @return String The field declarations necessary for property
   *   change support
   */
  protected String generatePropertyChangeFields()
  {
    StringBuilder builder = new StringBuilder( 512 );
    Formatter formatter = new Formatter( builder );

    formatter.format( "  /**%n" );
    formatter.format( "   * The property change event dispatcher.  This object%n" );
    formatter.format( "   * is lazily loaded.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  private transient PropertyChangeSupport propertyChangeSupport;%n%n" );

    return builder.toString();
  }

  /**
   * Generate the constructors for the JavaBean.
   *
   * @see #generateDefaultConstructor
   * @see #generateDesignatedConstructor
   */
  protected String generateConstructors()
  {
    StringBuilder builder = new StringBuilder( 4096 );

    builder.append( generateDefaultConstructor() );
    builder.append( generateDesignatedConstructor() );

    return builder.toString();
  }

  /**
   * Generate the default constructor for the JavaBean
   */
  protected String generateDefaultConstructor()
  {
    StringBuilder builder = new StringBuilder( 1024 );
    Formatter formatter = new Formatter( builder );

    formatter.format( "  /**%n" );
    formatter.format( "   * Default constructor.  Uses the designated constructor.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  public %s()%n", name );
    formatter.format( "  {%n" );
    formatter.format( "    this( " );

    int count = 0;
    for ( Map.Entry<String, String> entry : fields.entrySet() )
    {
      if ( count != 0 )
      {
        formatter.format( ",%n    " );
      }

      if ( entry.getValue().equals( "Map<String, Boolean>" ) )
      {
        formatter.format( " new Tree%s()", entry.getValue() );
      }
      else if ( entry.getValue().equals( "List<ListItem>" ) )
      {
        formatter.format( " new Array%s()", entry.getValue() );
      }
      else
      {
        formatter.format( " null" );
      }

      ++count;
    }
    formatter.format( " );%n" );
    formatter.format( "  }%n%n" );

    return builder.toString();
  }

  /**
   * Generate the designated constructor for the JavaBean
   */
  protected String generateDesignatedConstructor()
  {
    StringBuilder builder = new StringBuilder( 1024 );
    Formatter formatter = new Formatter( builder );

    formatter.format( "  /**%n" );
    formatter.format( "   * Designated constructor.  Initialises the%n" );
    formatter.format( "   * fields with the specified values.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  public %s( ", name );

    int count = 0;
    for ( Map.Entry<String, String> entry : fields.entrySet() )
    {
      if ( count != 0 )
      {
        formatter.format( ",%n    " );
      }

      formatter.format( "%s %s", entry.getValue(), entry.getKey() );
      ++count;
    }
    formatter.format( " )%n" );

    formatter.format( "  {%n" );
    for ( Map.Entry<String, String> entry : fields.entrySet() )
    {
      formatter.format( "    set%s%s( %s );%n",
          entry.getKey().substring( 0, 1 ).toUpperCase(),
          entry.getKey().substring( 1 ),
          entry.getKey() );
    }
    formatter.format( "  }%n%n" );

    return builder.toString();
  }

  /**
   * Generate the methods for the JavaBean.
   *
   * @see #generateToStringMethod
   * @see #generateEqualsMethod
   * @see #generateHashCodeMethod
   * @see #generateCompareToMethod
   * @see #generateCloneMethod
   * @see #generatePropertyChangeMethods
   * @see #generateBeanMethods
   * @return String The source code for the methods to be included in
   *  the JavaBean.
   */
  protected String generateMethods()
  {
    StringBuilder builder = new StringBuilder( 4096 );
    builder.append( generateToStringMethod() );
    builder.append( generateEqualsMethod() );
    builder.append( generateHashCodeMethod() );
    builder.append( generateCompareToMethod() );
    builder.append( generateCloneMethod() );
    builder.append( generatePropertyChangeMethods() );
    builder.append( generateBeanMethods() );

    return builder.toString();
  }

  /**
   * Generate the methods to support property change events.
   *
   * @return String The source code for property change event
   *   handling.
   */
  protected String generatePropertyChangeMethods()
  {
    StringBuilder builder = new StringBuilder( 1024 );
    Formatter formatter = new Formatter( builder );

    formatter.format( "  /**%n" );
    formatter.format( "   * Adds a property change listener to this <code>bean</code>.%n" );
    formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
    formatter.format( "   *%n" );
    formatter.format( "   * @param listener The listener to add.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  public void addPropertyChangeListener( PropertyChangeListener listener )%n" );
    formatter.format( "  {%n" );
    formatter.format( "    if ( propertyChangeSupport == null )%n" );
    formatter.format( "    {%n" );
    formatter.format( "      propertyChangeSupport = new PropertyChangeSupport( this );%n" );
    formatter.format( "    }%n%n" );
    formatter.format( "    propertyChangeSupport.addPropertyChangeListener( listener );%n" );
    formatter.format( "  }%n%n" );

    formatter.format( "  /**%n" );
    formatter.format( "   * Removes a property change listener from this <code>bean</code>.%n" );
    formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
    formatter.format( "   *%n" );
    formatter.format( "   * @param listener The listener to be removed.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  public void removePropertyChangeListener(%n" );
    formatter.format( "      PropertyChangeListener listener )%n" );
    formatter.format( "  {%n" );
    formatter.format( "    if ( propertyChangeSupport != null )%n" );
    formatter.format( "    {%n" );
    formatter.format( "      propertyChangeSupport.removePropertyChangeListener( listener );%n" );
    formatter.format( "    }%n" );
    formatter.format( "  }%n%n" );

    formatter.format( "  /**%n" );
    formatter.format( "   * Reports a bound property change to the registerd%n" );
    formatter.format( "   * <code>PropertyChangeListener</code>s.%n" );
    formatter.format( "   * Delegates handling to {@link #propertyChangeSupport}%n" );
    formatter.format( "   *%n" );
    formatter.format( "   * @param propertyName The name of the changed property.%n" );
    formatter.format( "   * @param oldValue The previous value of the property.%n" );
    formatter.format( "   * @param newValue The present value of the property.%n" );
    formatter.format( "   */%n" );
    formatter.format( "  protected void firePropertyChange( String propertyName,%n" );
    formatter.format( "      Object oldValue, Object newValue )%n" );
    formatter.format( "  {%n" );
    formatter.format( "    if ( propertyChangeSupport != null )%n" );
    formatter.format( "    {%n" );
    formatter.format( "      propertyChangeSupport.firePropertyChange(%n" );
    formatter.format( "          propertyName, oldValue, newValue );%n" );
    formatter.format( "    }%n" );
    formatter.format( "  }%n%n" );

    return builder.toString();
  }

  /**
   * Over-ridden to fire <code>PropertyChangeEvent</code>s for 
   * mutator methods.
   *
   * @return String The default accessor and mutator methods for
   *   the {@link #fields}.
   */
  @Override
  protected String generateBeanMethods()
  {
    StringBuilder methods = 
      new StringBuilder( super.generateBeanMethods() );
    String mutator = "public final void set";
    int index = 0;
    while ( ( index = methods.indexOf( mutator, index + 1 ) ) != -1 )
    {
      String propertyName = methods.substring( index + mutator.length(),
          methods.indexOf( "(", index ) ).trim();
      propertyName = propertyName.substring( 0, 1 ).toLowerCase() +
          propertyName.substring( 1 );

      StringBuilder builder = new StringBuilder( 32 );
      Formatter formatter = new Formatter( builder );
      formatter.format( "    firePropertyChange( \"%s\", oldValue, %s );%n",
          propertyName, propertyName );

      int end = methods.indexOf( "  }", index );
      methods.insert( end, builder.toString() );
    }

    return methods.toString();
  }

  /**
   * Main method.
   */
  public static void main( String[] args )
  {
    if ( args.length != 1 )
    {
      System.err.println( 
          "Usage: java -classpath <classpath> com.sptci.echo2.ModelGenerator <UI class>" );
      System.exit( 2 );
    }
    else
    {
      try
      {
        ModelGenerator generator = new ModelGenerator( args[0] );
        generator.generate();
      }
      catch ( Throwable t )
      {
        t.printStackTrace();
        System.exit( 1 );
      }
    }

    System.exit( 0 );
  }
}
