/*
 * @(#)FileView.java    2.4.0   24 August 2005
 *
 * 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.util.*;
import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.text.ParseException;
import javax.swing.*;
import javax.swing.filechooser.FileFilter;

/**
 * <P>A <CODE>{@link TypedView TypedView}</CODE> 
 * for input of a filename that either represents 
 * a path to an existing file 
 * or a path to which a file could be written.  
 *
 * Provides a button that brings up a <CODE>JFileChooser</CODE> 
 * for easy choice of an existing file.</P>
 *
 * @author  Jeff Raab
 * @author  Richard Rasala
 * @version 2.4.0
 * @since   1.0
 * @see Fragile
 */
public class FileView 
    extends DisplayPanel 
    implements TypedView, Fragile 
{
    /** Bound property name for the last path property. */
    public static final String LAST_PATH =
        "last.path";

    /** Default filename. */
    public static final String DEFAULT_FILENAME = "";

    /** Default button label text. */
    public static final String DEFAULT_BUTTON_LABEL = "Browse";

    /** Default alignment of browse button relative to field. */
    public static final int DEFAULT_ALIGNMENT = RIGHT;

    /** File filter that accepts all files. */
    protected static final FileFilter ALL_FILES_FILTER =
        new FileFilter() {
            public boolean accept(File f) {
                return true;
            }
            public String getDescription() {
                return "All files (*.*)";
            }
        };

    /** Path to which a <CODE>FileView</CODE> was last navigated. */
    protected static String lastPath = null;
    
    /** Alignment of the browse button relative to the field. */
    protected int align = DEFAULT_ALIGNMENT;
    
    /** Text field containing the currently selected filename. */
    protected TextFieldView field = null;
    
    /** Button that brings up the file chooser. */
    protected JButton browse = null;
    
    /** Property list for this view object. */
    protected InputProperties properties = new InputProperties();
    
    //////////////////
    // Constructors //
    //////////////////
    
    /**
     * Constructs a file view 
     * with the default initial filename 
     * and the default alignment.
     *
     * @see #FileView(String)
     * @see #FileView(String, int)
     */
    public FileView() {
        this(DEFAULT_FILENAME, DEFAULT_ALIGNMENT);
    }
    
    /**
     * Constructs a file view 
     * with the given default filename
     * and the default alignment.
     *
     * @param name the default filename for the view
     * @see #FileView()
     * @see #FileView(String, int)
     */
    public FileView(String name) {
        this(name, DEFAULT_ALIGNMENT);
    }
    
    /**
     * Constructs a file view 
     * with the given default filename 
     * and the given alignment value.
     *
     * @param name the default filename for the view
     * @param align the alignment of the browse button
     *      relative to the filename field
     * @see #FileView()
     * @see #FileView(String)
     */
    public FileView(String name, int align) {

        // try to set the initial path to the current user path
        try {
            lastPath = (String)System.getProperty("user.dir");
        } catch (SecurityException ex) {
            // otherwise start at default filechooser path
        }
    
        // build the filename field and add it to this panel
        field = new TextFieldView(name);
        field.setPreferredWidth(100);

        setLayout(new BorderLayout(5, 5));
        add(field, BorderLayout.CENTER);
        
        setAlignment(align);
       
        addMalformedDataListener(this);
    }
    
    ///////////////
    // TypedView //
    ///////////////

    /**
     * Returns an <CODE>XString</CODE> representing 
     * the currently selected path.
     *
     * The returned representation may not be a path 
     * to an existing file, or may not be a valid filename at all.
     *
     * @see #demandFile()
     * @see #demandExistingFile()
     * @see #demandFilename()
     * @see #demandExistingFilename()
     * @see #requestObject()
     * @see TypedView
     */
    public Stringable demandObject() {
        return new XString(getViewState());
    }

    /**
     * Returns an <CODE>XString</CODE> representing 
     * the currently selected path.
     *
     * The returned representation may not be a path 
     * to an existing file, or may not be a valid filename at all.
     *
     * @see #requestFile()
     * @see #requestExistingFile()
     * @see #requestFilename()
     * @see #requestExistingFilename()
     * @see #demandObject()
     * @see TypedView
     */
    public Stringable requestObject() throws CancelledException {
        return demandObject();
    }

    public void setInputProperties(InputProperties p) {
        if (p == null)
            p = InputProperties.BASE_PROPERTIES;
    
        InputProperties oldProperties = getInputProperties();

        properties = p;
        
        // if input properties have changed
        if ((getInputProperties() != null) &&
            !getInputProperties().equals(oldProperties)) {
            
            // notify listeners of property change
            firePropertyChange(
                INPUT_PROPERTIES,
                oldProperties,
                getInputProperties());
        }
    }

    public InputProperties getInputProperties() {
        return properties;
    }

    /**
     * Returns the <CODE>XString</CODE> class object.
     *
     * @see TypedView
     */ 
    public Class getDataType() {
        return XString.class;
    }

    /////////////////
    // Displayable //
    /////////////////
    
    /**
     * Sets the currently selected path 
     * to the given <CODE>String</CODE>.  
     *
     * The provided filename need not point to an existing file, 
     * or represent a valid filename at all.
     *
     * @param data the new selected path
     * @see #getViewState()
     * @see Displayable
     */
    public void setViewState(String data) {
        field.setViewState(data);

        // notify listeners of property change
        firePropertyChange(
            VIEW_STATE, 
            null, 
            data);
    }
    
    /**
     * Returns the currently selected path.
     *
     * @see #setViewState(String)
     * @see Displayable
     */
    public String getViewState() {
        return field.getViewState();
    }

    /**
     * Sets the default path
     * to the given <CODE>String</CODE>.
     *
     * @param data the new default path
     * @see #getDefaultViewState()
     * @see #reset()
     * @see Displayable
     */
    public void setDefaultViewState(String data) {
        field.setDefaultViewState(data);

        // notify listeners of property change
        firePropertyChange(
            DEFAULT_VIEW_STATE, 
            null, 
            data);
    }
    
    /**
     * Returns the default path.
     *
     * @see #setDefaultViewState(String)
     * @see #reset()
     */
    public String getDefaultViewState(String data) {
        return field.getDefaultViewState();
    }
    
    public void reset() {
        field.reset();
    }
    
    public void setEnabled(boolean isEnabled) {
        field.setEnabled(isEnabled);
        browse.setEnabled(isEnabled);
        super.setEnabled(isEnabled);
    }
    
    /////////////
    // Fragile //
    /////////////
    
    public void addMalformedDataListener(
        MalformedDataListener l) 
    {
        listenerList.add(
            MalformedDataListener.class, 
            (MalformedDataListener)l);
    }
    
    public void removeMalformedDataListener(
        MalformedDataListener l) 
    {
        listenerList.remove(
            MalformedDataListener.class, 
            (MalformedDataListener)l);
    }
    
    ////////////////
    // Public API //
    ////////////////

    /**
     * Returns a <CODE>File</CODE> object 
     * representing a path to an existing file.
     * 
     * This method enforces the mandatory input model,
     * and uses a <CODE>JFileChooser</CODE> to implement
     * an error recovery strategy.
     *
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public File demandExistingFile() {
        return new File(demandExistingFilename());
    }
    
    /**
     * Returns a <CODE>File</CODE> object 
     * representing a valid path for the file system.  
     *
     * The returned <CODE>File</CODE> may or may not 
     * point to an already existing file.  
     *
     * This method enforces the mandatory input model,
     * and uses a <CODE>JFileChooser</CODE> to implement
     * an error recovery strategy.
     * 
     * @see #demandExistingFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public File demandFile() {
        return new File(demandFilename());
    }

    /**
     * Returns a <CODE>String</CODE> 
     * representing a path to an existing file.
     * 
     * This method enforces the mandatory input model,
     * and uses a <CODE>JFileChooser</CODE> to implement
     * an error recovery strategy.
     *
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public String demandExistingFilename() {
        try {
            return performRequest(false, true);
        }
        
        // can not occur, but must be caught anyway
        catch (CancelledException ex) {
            return null;
        }
    }

    /**
     * Returns a <CODE>String</CODE>
     * representing a valid path for the file system.  
     *
     * The returned path may or may not 
     * point to an already existing file.  
     *
     * This method enforces the mandatory input model,
     * and uses a <CODE>JFileChooser</CODE> to implement
     * an error recovery strategy.
     * 
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public String demandFilename() {
        try {
            return performRequest(true, true);
        }
        
        // can not occur, but must be caught anyway
        catch (CancelledException ex) {
            return null;
        }
    }

    /**
     * Returns a <CODE>File</CODE> object 
     * representing a path to an existing file.
     * 
     * This method enforces the optional input model,
     * and uses a dismissable <CODE>JFileChooser</CODE> 
     * to implement an error recovery strategy.
     *
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public File requestExistingFile() throws CancelledException {
        return new File(requestExistingFilename());
    }
    
    /**
     * Returns a <CODE>File</CODE> object 
     * representing a valid path for the file system.  
     *
     * The returned <CODE>File</CODE> may or may not 
     * point to an already existing file.  
     *
     * This method enforces the optional input model,
     * and uses a dismissable <CODE>JFileChooser</CODE> 
     * to implement an error recovery strategy.
     *
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestExistingFilename()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public File requestFile() throws CancelledException {
        return new File(requestFilename());
    }

    /**
     * Returns a <CODE>String</CODE> 
     * representing a path to an existing file.
     * 
     * This method enforces the optional input model,
     * and uses a dismissable <CODE>JFileChooser</CODE> 
     * to implement an error recovery strategy.
     *
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public String requestExistingFilename() 
        throws CancelledException 
    {
        return performRequest(false, false);
    }

    /**
     * Returns a <CODE>String</CODE>
     * representing a valid path for the file system.  
     *
     * The returned path may or may not 
     * point to an already existing file.  
     *
     * This method enforces the optional input model,
     * and uses a dismissable <CODE>JFileChooser</CODE> 
     * to implement an error recovery strategy.
     *
     * @see #demandExistingFile()
     * @see #demandFile()
     * @see #demandExistingFilename()
     * @see #demandFilename()
     * @see #demandExistingFile()
     * @see #requestFile()
     * @see #requestExistingFilename()
     * @see #demandObject()
     * @see #requestObject()
     */
    public String requestFilename() throws CancelledException {
        return performRequest(true, false);
    }

    /**
     * Sets the alignment of the browse button 
     * relative to the filename field.
     *
     * If the given alignment value is invalid,
     * the current alignment is not changed.
     *
     * @param alignment the new alignment of the browse button
     *      relative to the filename field
     * @see #getAlignment()
     * @see #ABOVE
     * @see #BELOW
     * @see #LEFT
     * @see #RIGHT
     * @see #DEFAULT
     */
    public void setAlignment(int alignment) {
        int oldAlignment = getAlignment();
    
        switch (alignment) {

            // check for alignment within bounds
            case LEFT:
            case RIGHT:
            case ABOVE:
            case BELOW:
                align = alignment;
                break;
        
            // check for request for default alignment
            case DEFAULT:
                align = DEFAULT_ALIGNMENT;
                break;
                
            // ignore invalid alignment values
            default:
                return;
        }
        
        // create a new button if necessary            
        if (browse == null) {
            browse = new JButton(DEFAULT_BUTTON_LABEL);
            browse.addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent evt) {
                    JFileChooser fc = 
                        new JFileChooser(getLastPath());
                    fc.setFileFilter(ALL_FILES_FILTER);

                    if (fc.showOpenDialog(null) == 
                        JFileChooser.APPROVE_OPTION)
                    {
                        setViewState(
                            fc.getSelectedFile().getPath());
                        setLastPath(
                            fc.getSelectedFile().getPath());
                    }
                }
            });
        }
        
        // otherwise remove the existing button from the layout
        else remove(browse);

        // add the browse button to the layout appropriately
        add(browse, JPTUtilities.getBorderLayoutLocation(alignment));

        // if the alignment has changed
        if (getAlignment() != oldAlignment) {

            // notify listeners of property change
            firePropertyChange(
                ALIGNMENT,
                oldAlignment,
                getAlignment());
        }
    }
    
    /**
     * Returns the value for the current alignment 
     * of the browse button relative to the editable field.
     *
     * @see #setAlignment(int)
     * @see #ABOVE
     * @see #BELOW
     * @see #LEFT
     * @see #RIGHT
     * @see #DEFAULT
     */
    public int getAlignment() {
        return align;
    }

    /**
     * Sets the last navigated path to the given path.
     *
     * @param path the desired directory to start browsing in
     * @see #getLastPath()
     */
    public void setLastPath(String path) {
        String oldPath = getLastPath();
    
        lastPath = path;
        
        // if the last path has changed
        if ((getLastPath() != null) &&
            !getLastPath().equals(oldPath)) {

            // notify listeners of property change
            firePropertyChange(
                LAST_PATH,
                oldPath,
                getLastPath());
        }
    }
    
    /**
     * Returns the path to which a file view last navigated.
     *
     * @see #setLastPath(String)
     */
    public String getLastPath() {
        return lastPath;
    }
    
    ///////////////////////
    // Protected methods //
    ///////////////////////

    /**
     * Returns the browse button component for this view.
     */
    protected JButton getBrowseButton() {
        return browse;
    }
    
    /**
     * Delivers <CODE>MalformedDataEvent</CODE>s 
     * to registered listeners.
     *
     * @param evt the <CODE>MalformedDataEvent</CODE> to deliver
     * @see #addMalformedDataListener(MalformedDataListener)
     * @see #removeMalformedDataListener(MalformedDataListener)
     */
    protected void fireDataMalformed(MalformedDataEvent evt) {
        Object[] obj = listenerList.getListenerList();
        for (int i = obj.length - 2; i >= 0; i -= 2) {
            if ((obj[i] != null) &&
                (obj[i].equals(MalformedDataListener.class)))
            {
                MalformedDataListener m = 
                    (MalformedDataListener)obj[i];
                m.dataMalformed(evt);
            }
        }
    } 
    
    /**
     * Returns a <CODE>String</CODE> path that was either
     * input into the filename field directly,
     * selected using the file chooser brought up by browsing,
     * or selected using the file chooser brought up
     * as part of the error recovery strategy.
     *
     * @param anyValid whether or not any valid filename is enough
     * @param mandatory whether or not the operation is mandatory
     * @throws CancelledException if the user cancels the action
     *      and the operation is not mandatory
     * @see #demandFile()
     * @see #demandExistingFile()
     * @see #demandFilename()
     * @see #demandExistingFilename()
     * @see #requestFile()
     * @see #requestExistingFile()
     * @see #requestFilename()
     * @see #requestExistingFilename()
     */
    protected String performRequest(
        boolean anyValid, 
            boolean mandatory) 
        throws CancelledException 
    {
        boolean alert = false;
        
        // create a file object from the path data
        File file = new File(field.getViewState());
        
        // ensure that the filename represents a valid path, 
        // using a filechooser for error recovery
        JFileChooser fc = null;
        while (!file.exists()) {

            // perform creation/deletion test 
            // on file object if appropriate
            try {
                if (anyValid && file.createNewFile()) {
                    file.delete();
                    break;
                }
            } catch (IOException ex) {
                // means that the file could not be created
            }
            
            // alert listeners of malformed data if appropriate
            if (!alert) {
                alert = true;
                fireDataMalformed(
                    new MalformedDataEvent(
                        this, 
                        field.getViewState(),
                        new ParseException(
                            "Path/filename is not valid.", -1)));
            }

            // build a filechooser object only once
            if (fc == null) {
                fc = new JFileChooser(file);
                fc.setMultiSelectionEnabled(false);
                fc.setFileFilter(ALL_FILES_FILTER);
            }
        
            // show the filechooser dialog
            int selection = fc.showDialog(this, "Select");
            
            // if a file was selected, 
            // prepare for existence of selected file
            if (selection == JFileChooser.APPROVE_OPTION) {
                file = fc.getSelectedFile();
                field.setViewState(file.getPath());
                setLastPath(file.getPath());
            }
            
            // if the user chose to cancel 
            // and the operation is not mandatory,
            // throw an exception
            else if ((selection == JFileChooser.CANCEL_OPTION) && 
                     !mandatory) 
            {
                // remove alert status if appropriate
                if (alert)
                    fireDataMalformed(
                        new MalformedDataEvent(this, null));
                    
                throw new CancelledException();
            }

            // otherwise, prepare to show dialog again
            else fc.setSelectedFile(file);
        }
        
        // remove alert status if appropriate
        if (alert)
            fireDataMalformed(
                new MalformedDataEvent(this, field.getViewState()));

        // return the appropriate filename String
        return field.getViewState();        
    }
    
    ///////////////////
    // Inner classes //
    ///////////////////
    
    /**
     * <P>A file filter that accepts all files 
     * with a specific extension.</P>
     *
     * <p>This filter is case insensitive.</p>
     *
     * <p>This inner class has been generalized by the class
     * <code>FileExtensionFilter</code> in the package
     * <code>edu.neu.ccs.util</code>.  To minimize confusion
     * and to support backward compatibility, this class has
     * been retained.</p>
     *
     * @author  Jeff Raab
     * @author  Richard Rasals
     * @version 2.4.0
     * @since   1.0
     */
    public static class ExtensionFileFilter 
        extends FileFilter 
        implements Cloneable, Serializable 
    {
        /** The extension accepted by this filter. */
        private String ext = null;
        
        /** The extension in lower case accepted by this filter. */
        private String extInLowerCase = null;
        
        /** The extension length. */
        private int extLength = 0;
        
        /**
         * <p>Constructor for a filter accepting all files
         * with the given extension.</p>
         *
         * <p>Extension match is case insensitive.</p>
         *
         * <p>If the given <CODE>String</CODE> is <CODE>null</CODE>
         * or of length 0, all files are acceptable to this filter.</p>
         *
         * @param extension the desired extension
         */
        public ExtensionFileFilter(String extension) {
            ext = extension;
            
            if (ext != null) {
                extLength = ext.length();
                extInLowerCase = ext.toLowerCase();
            }
        }
        
        /**
         * Returns whether or not the given file 
         * is acceptable to this filter.
         *
         * @param f the file to be tested
         */
        public boolean accept(File f) {
            // false since there is no file object
		    if (f == null)
			    return false;
		
            // true since there is no extension constraint
		    if (extLength == 0)
			    return true;
		
            // true to allow directory navigation
		    if (f.isDirectory())
			    return true;
		    
		    String name = f.getName();
		    
		    int offset = name.length() - extLength;
		    
		    // false since file name is too short
		    if (offset < 0)
		        return false;
		    
		    // decide using the String regionMatches method
		    return name.regionMatches(true, offset, ext, 0, extLength);
        }
        
        /**
         * The description of this filter.
         */
        public String getDescription() {
            if (ext == null)
                return "All files: *.*";
                
            return ext + " files: *." + extInLowerCase;
        }
    }
}
