/*
 * @(#)GeneralViewSupport.java    2.3  3 December 2003
 *
 * Copyright 2004
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.gui;

import edu.neu.ccs.*;
import edu.neu.ccs.filter.*;
import edu.neu.ccs.util.*;
import java.awt.*;
import java.awt.event.*;
import java.math.*;
import java.text.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.event.*;

/**
 * <P><CODE>GeneralViewSupport</CODE> contains the common code to support
 * the features of a <CODE>{@link GeneralView GeneralView}</CODE>.</P>
 *
 * @author  Richard Rasala
 * @version 2.3
 * @since   2.3
 */
public class GeneralViewSupport implements JPTConstants 
{
    /** 
     * Bound property name for the view state of a
     * <CODE>Displayable</CODE> object. 
     */
    public static final String VIEW_STATE = Displayable.VIEW_STATE;
    
    
    /** 
     * Bound property name for the default view state of a
     * <CODE>Displayable</CODE> object. 
     */
    public static final String DEFAULT_VIEW_STATE = Displayable.DEFAULT_VIEW_STATE;
    
    
    /** 
     * Bound property name for the <CODE>Stringable</CODE> type returned
     * by a <CODE>TypedView</CODE> or <CODE>GeneralView</CODE>.
     */
    public static final String DATA_TYPE = TypedView.DATA_TYPE;
    
    
    /** Bound property name for the filter property. */
    public static final String FILTER = "filter";
    
    
    /** The GeneralView that uses this GeneralViewSupport object. */
    private GeneralView view = null;
    
    /** The EventListenerList for the GeneralView. */
    private EventListenerList listenerList = null;
    
    /** The GeneralView seen as a JComponent. */
    private JComponent component = null;
    
    /** The factory for creating Stringable objects from input data. */
    private StringableFactory factory = new StringableFactory(XString.class);
    
    /** The property list for this view object. */
    private InputProperties properties = new InputProperties();
    
    /** The filter used by this view object. */
    private StringableFilter filter = null;
    
    /** The default view state string. */
    private String defaultViewState = "";
    
    
    /**
     * <P>The constructor.</P>
     *
     * <P>The given <CODE>GeneralView</CODE> view is the view that uses
     * this <CODE>GeneralViewSupport</CODE> object and must not be
     * <CODE>null</CODE>.  In addition, the view must be a derived
     * object from <CODE>JComponent</CODE></P>
     *
     * <P>The given <CODE>EventListenerList</CODE> list is the listener
     * list of the given <CODE>GeneralView</CODE> view and must not be
     * <CODE>null</CODE>.</P>
     *
     * <P>The given data type is the <CODE>Stringable</CODE> data type
     * for the view.  If this parameter is <CODE>null</CODE> it is set
     * to <CODE>XString.class</CODE>.
     *
     * <P>The parameters error prompt, dialog title, and suggestion set
     * defaults for the error dialog box and may be <CODE>null</CODE>.</P>
     *
     * @param  view the view that uses this support class
     * @param  listenerList the listener list of the view
     * @param  dataType the Stringable data type
     * @param  errorPrompt the error prompt of an error dialog
     * @param  dialogTitle the dialog title of an error dialog
     * @param  suggestion the suggestion for an error dialog
     * @throws <CODE>NullPointerException</CODE> if the view or listener
     *         list is null
     * @throws <CODE>IllegalArgumentException</CODE> if the view is not
     *         derived from <CODE>JComponent</CODE>
     */
    public GeneralViewSupport(
        GeneralView         view,
        EventListenerList   listenerList,
        Class               dataType,
        String              errorPrompt,
        String              dialogTitle,
        String              suggestion
    ) {
        if ((view == null) || (listenerList == null))
            throw new NullPointerException
                ("Invalid null parameter to GeneralViewSupport constructor");
        
        if (!(view instanceof JComponent))
            throw new IllegalArgumentException
                ("View in GeneralViewSupport constructor is not JComponent");
        
        this.view = view;
        this.listenerList = listenerList;
        this.component = (JComponent) view;
        
        setDataType(dataType);
        
        setErrorPromptTitleSuggestion(errorPrompt, dialogTitle, suggestion);
    }
    
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * Returns the view that uses this GeneralViewSupport object.
     *
     * @return the encapsulated view
     */
    public GeneralView getView() {
        return view;
    }
    
    
    ///////////////////////////////
    // GeneralView and TypedView //
    ///////////////////////////////
    
    /**
     * Sets the current class of objects returned when a model object is
     * demanded or requested.
     *
     * @param  dataType the new class of objects for model objects
     * @see    #getDataType()
     * @throws Error if the given data type is not assignable from
     *         the <CODE>Stringable</CODE> reference type
     */
    public void setDataType(Class dataType) {
        Class oldType = getDataType();
    
        factory.setDataType(dataType);
        
        if (oldType != getDataType()) {
            // notify listeners of property change
            component.firePropertyChange(DATA_TYPE, 0, 1);
        }
    }
    
    
    /**
     * Returns the current class of objects returned when a model object
     * is demanded or requested.
     *
     * @return the current class of a model object
     * @see    #setDataType(Class)
     */    
    public Class getDataType() {
        return factory.getDataType();
    }
    
    
    /**
     * Sets the current filter used by the view to the given StringableFilter.
     *
     * @param filter the filter to be used
     */
    public void setFilter(StringableFilter filter) {
        StringableFilter oldFilter = getFilter();
    
        this.filter = filter;
        
        // if the filter has changed
        if (filter != oldFilter) {
            // notify listeners of property change
            component.firePropertyChange(FILTER, 0, 1);
        }
    }
    
    
    /**
     * Returns the current filter used by this view.
     *
     * @return the current filter
     */
    public StringableFilter getFilter() {
        return filter;
    }
    
    
    /**
     * <P>Returns a <CODE>Stringable</CODE> object based on the view state
     * data type, and filter or throws a <CODE>ParseException</CODE> to be
     * handled by other methods.</P>
     *
     * <P>Normally, this method is not called directly, but it is provided
     * for the convenience of callers that want to implement their own
     * error handler for <CODE>ParseException</CODE>s.
     *
     * @param  object the initial default object
     * @return the object as adjusted by the view state and filter
     * @throws <CODE>ParseException</CODE> if an error occurs
     */
    public Stringable obtainObject(Stringable object)
        throws ParseException
    {
        // handle null
        if (object == null)
            throw new ParseException("Null object in obtainObject", -1);
        
        // check for numerical error
        try {
            object.fromStringData(view.getViewState());

            // apply filter
            if (filter != null) {
                try {
                    object = filter.filterStringable(object);
                }
                catch (FilterException ex) {
                    // translate filter exception to a parse exception
                    throw new ParseException(ex.getMessage(), -1);
                }
            }
        }
        
        // translate number format exception to a parse exception
        catch (NumberFormatException ex) {
            throw new ParseException(ex.getMessage(), -1);
        }
        
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the current data type and filter, and the mandatory
     * model.
     *
     * @return a <CODE>Stringable</CODE> model object
     * @see    #requestObject()
     * @see    #obtainObject(Stringable)
     */
    public Stringable demandObject() {
        // create the default object
        Stringable object = factory.getDefaultInstance();

        // attempt to create the object from view state
        try {
            object = obtainObject(object);
        }
        // handle any error encountered
        catch (ParseException ex) {
            handleError(object, JPTConstants.MANDATORY, ex);
            
            view.setViewState(object.toStringData());
            component.repaint();
        }

        // return initialized object
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the current data type and filter, and the optional
     * model.
     *
     * @return a <CODE>Stringable</CODE> model object
     * @see    #demandObject()
     * @see    #obtainObject(Stringable)
     * @throws <CODE>CancelledException</CODE> if the user cancelled
     *         after an error was detected
     */
    public Stringable requestObject()
        throws CancelledException
    {
        // create the default object
        Stringable object = factory.getDefaultInstance();

        // attempt to create the object from view state
        try {
            object = obtainObject(object);
        }
        // handle an error if encountered
        catch (ParseException ex) {
            if (!handleError(object, JPTConstants.OPTIONAL, ex))
                throw new CancelledException();
            
            view.setViewState(object.toStringData());
            component.repaint();
        }

        // return initialized object
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the temporary filter, and the mandatory model.
     *
     * @param  filter the temporary filter to use
     * @return a <CODE>Stringable</CODE> model object
     * @see    #demandObject()
     * @see    #requestObject(Class, StringableFilter)
     */
    public Stringable demandObject(StringableFilter filter)
    {
        // temporarily set the stringable filter
        StringableFilter oldFilter = getFilter();
        setFilter(filter);
        
        // extract the desired value
        Stringable object = demandObject();
        
        // restore former settings
        setFilter(oldFilter);

        // return initialized object
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the temporary filter, and the optional model.
     *
     * @param  filter the temporary filter to use
     * @return a <CODE>Stringable</CODE> model object
     * @see    #requestObject()
     * @see    #demandObject(Class, StringableFilter)
     * @throws <CODE>CancelledException</CODE> if the user cancelled
     *         after an error was detected
     */
    public Stringable requestObject(StringableFilter filter)
        throws CancelledException
    {
        // temporarily set the stringable filter
        StringableFilter oldFilter = getFilter();
        setFilter(filter);
        
        Stringable object = null;
        
        // attempt to extract the desired value
        try {
            object = requestObject();
        }
        // ensure settings are restored
        finally {
            setFilter(oldFilter);
        }

        // return initialized object
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the temporary data type and filter, and the mandatory
     * model.
     *
     * @param  dataType the temporary data type to use
     * @param  filter   the temporary filter to use
     * @return a <CODE>Stringable</CODE> model object
     * @see    #demandObject()
     * @see    #requestObject(Class, StringableFilter)
     */
    public Stringable demandObject(Class dataType, StringableFilter filter)
    {
        // temporarily set the class for the factory
        Class oldClass = factory.getDataType();
        factory.setDataType(dataType);
        
        // temporarily set the stringable filter
        StringableFilter oldFilter = getFilter();
        setFilter(filter);
        
        // extract the desired value
        Stringable object = demandObject();
        
        // restore former settings
        factory.setDataType(oldClass);
        setFilter(oldFilter);

        // return initialized object
        return object;
    }
    
    
    /**
     * Returns a <CODE>Stringable</CODE> object based on the view state
     * of the view, the temporary data type and filter, and the optional
     * model.
     *
     * @param  dataType the temporary data type to use
     * @param  filter   the temporary filter to use
     * @return a <CODE>Stringable</CODE> model object
     * @see    #requestObject()
     * @see    #demandObject(Class, StringableFilter)
     * @throws <CODE>CancelledException</CODE> if the user cancelled
     *         after an error was detected
     */
    public Stringable requestObject(Class dataType, StringableFilter filter)
        throws CancelledException
    {
        // temporarily set the class for the factory
        Class oldClass = factory.getDataType();
        factory.setDataType(dataType);
        
        // temporarily set the stringable filter
        StringableFilter oldFilter = getFilter();
        setFilter(filter);
        
        Stringable object = null;
        
        // attempt to extract the desired value
        try {
            object = requestObject();
        }
        // ensure settings are restored
        finally {
            factory.setDataType(oldClass);
            setFilter(oldFilter);
        }

        // return initialized object
        return object;
    }
    
    
    /**
     * <P>Sets the input properties for this view to the provided input
     * properties.</P>
     *
     * <P>If the given input properties list is <CODE>null</CODE>, the
     * property list for this view is set to the base property list
     * containing default property values.</P>
     *
     * @param properties the new input properties for the view
     * @see #getInputProperties()
     */
    public void setInputProperties(InputProperties properties) {
        if (properties == null)
            properties = InputProperties.BASE_PROPERTIES;
    
        InputProperties oldProperties = getInputProperties();
    
        this.properties = properties;
        
        // if the input properties has changed
        if ((properties != null) && !properties.equals(oldProperties)) {
            // notify listeners of property change
            component.firePropertyChange(INPUT_PROPERTIES, 0, 1);
        }
    }
    
    
    /**
     * Returns the input properties for this view.
     *
     * @return the input properties
     * @see #setInputProperties(InputProperties)
     */
    public InputProperties getInputProperties() {
        return properties;
    }
    
    
    /**
     * Sets the three input property <CODE>String</CODE>s for an error
     * dialog in a single method.
     *
     * @param  errorPrompt the error prompt of an error dialog
     * @param  dialogTitle the dialog title of an error dialog
     * @param  suggestion the suggestion for an error dialog
     */
    public void setErrorPromptTitleSuggestion(
        String errorPrompt,
        String dialogTitle,
        String suggestion )
    {
        properties.setInputPrompt(errorPrompt);
        properties.setDialogTitle(dialogTitle);
        properties.setSuggestion(suggestion);
    }
    
    
    // makeCopy is a responsibity of the view class //
    
    
    /////////////////
    // Displayable //
    /////////////////
    
    
    /**
     * Sets the default view state for this object to the data state
     * represented by the given <CODE>String</CODE> data.
     *
     * @param data the new default data state for this object
     * @see #getDefaultViewState()
     * @see #reset()
     */
    public void setDefaultViewState(String data) {
        String oldDefaultViewState = defaultViewState;
        
        data = (data == null) ? "" : data;

        defaultViewState = data;
        
        if (! oldDefaultViewState.equals(defaultViewState)) {
            // notify listeners of property change
            component.firePropertyChange(DEFAULT_VIEW_STATE, 0, 1);
        }
    }
    
    
    /**
     * Returns a <CODE>String</CODE> representation of the default view
     * state for this object.
     *
     * @return the default view state as a <CODE>String</CODE>
     * @see #setDefaultViewState(String)
     * @see #reset()
     */
    public String getDefaultViewState() {
        return defaultViewState;
    }
    
    
    /**
     * Resets the view state of this object to the default view state for
     * this object.
     */
    public void reset() {
        view.setViewState(getDefaultViewState());
    }
    
    
    // setViewState is a responsibity of the view class //
    
    // getViewState is a responsibity of the view class //
    
    // setEnabled is a responsibity of the view class //
    
    
    /////////////
    // Fragile //
    /////////////
    
    
    /**
     * Registers the given <CODE>MalformedDataListener</CODE> 
     * to receive <CODE>MalformedDataEvent</CODE>s 
     * when the view state data is found to be malformed and
     * when the view state data is returned to a well-formed
     * value.
     *
     * @param l the listener to register
     * @see #removeMalformedDataListener(MalformedDataListener)
     * @see Fragile
     * @see EventListenerList
     */
    public void addMalformedDataListener(MalformedDataListener l) {
        listenerList.add(MalformedDataListener.class, l);
    }
    
    
    /**
     * De-registers the given <CODE>MalformedDataListener</CODE> 
     * from receiving <CODE>MalformedDataEvent</CODE>s 
     * when the view state data is found to be malformed and
     * when the view state data is returned to a well-formed
     * value.
     *
     * @param l the listener to de-register
     * @see #addMalformedDataListener(MalformedDataListener)
     * @see Fragile
     * @see EventListenerList
     */
    public void removeMalformedDataListener(MalformedDataListener l) {
        listenerList.remove(MalformedDataListener.class, l);
    }
    
    
    ///////////////////////
    // Protected methods //
    ///////////////////////

    /**
     * <P>Performs the error handling for an input operation.</P>
     *
     * <P>Derived classes of support class may override this method
     * and implement their own error handling strategy.</P>
     *
     * <P>This method must either provide a correct value for the
     * <CODE>Stringable</CODE> object and return <CODE>true</CODE>,
     * or must return <CODE>false</CODE> if the object is still in
     * error or if the user cancelled the error handling procedure.</P>
     *
     * <P>The input model must be either
     * <CODE>JPTConstants.MANDATORY</CODE>
     * or <CODE>JPTConstants.OPTIONAL</CODE>.
     *
     * @param  object an object of the appropriate type whose state
     *         could not be set from the view state of this view
     *         and must therefore be set to a valid view state by
     *         this error handler
     * @param  inputModel the input model for the input operation
     *         that resulted in the error
     * @param  exception the <CODE>ParseException</CODE> that was
     *         thrown to signal the error condition
     * @return whether or not the error was corrected through this
     *         error handling strategy
     */
    protected boolean handleError(
        Stringable     object, 
        int            inputModel,
        ParseException exception )
    {
        // alert that the view has an error
        fireDataMalformed
            (new MalformedDataEvent(view, view.getViewState(), exception));
        
        // clone the input properties for use in error recovery
        InputProperties errorProperties = new InputProperties(properties);
        
        if (inputModel != JPTConstants.OPTIONAL)
            inputModel  = JPTConstants.MANDATORY;
        
        errorProperties.setInputModel(inputModel);
        
        // clone the view for use in error recovery
        GeneralView copy = view.makeCopy();
        copy.setInputProperties(errorProperties);
        
        // use dialog box input for error recovery
        ErrorDialog dialog = new ErrorDialog(object, copy, filter, exception);
        dialog.setVisible(true);

        // notify that alert status has been removed
        fireDataMalformed
            (new MalformedDataEvent(view, view.getViewState()));

        // return true if the error was corrected and false if the dialog
        // was cancelled before the error correction was complete
        return !dialog.wasCancelled();
    }
    
    
    /**
     * Delivers <CODE>MalformedDataEvent</CODE>s to registered listeners.
     *
     * @param event the <CODE>MalformedDataEvent</CODE> to deliver
     * @see #addMalformedDataListener(MalformedDataListener)
     * @see #removeMalformedDataListener(MalformedDataListener)
     * @see Fragile
     * @see EventListenerList
     */
    protected void fireDataMalformed(MalformedDataEvent event) {
        Object[] objects = listenerList.getListenerList();
        
        for (int i = objects.length - 2; i >= 0; i -= 2) {
            if ((objects[i] != null) && objects[i].equals(MalformedDataListener.class)) 
            {
                MalformedDataListener listener = (MalformedDataListener)objects[i + 1];
                listener.dataMalformed(event);
            }
        }
    }
    
}
