//=====================================================================
// File:    MultiFileDialog.java
// Class:   MultiFileDialog
// Package: AFLPgui
//
// Author:  James J. Benham
// Date:    January 4, 1999
// Contact: james_benham@hmc.edu
//
// Genographer v1.0 - Computer assisted scoring of gels.
// Copyright (C) 1998  Montana State University
// 
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License
// as published by the Free Software Foundation; version 2
// of the License.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
//
// The GNU General Public License is distributed in the file GPL
//=====================================================================

package AFLPgui;

import java.awt.Button;
import java.awt.Choice;
import java.awt.Dialog;
import java.awt.FileDialog;
import java.awt.Frame;
import java.awt.Label;
import java.awt.List;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;
import java.io.File;
import java.util.Vector;
import AFLPcore.FeatureList;
import AFLPcore.ImportFilter;

/**
 * This class allows the user to select multiple files. It provides
 * a directory strucute which can be navigated on one side and a list
 * of files to open on the other.
 *
 * @author James J. Benham
 * @version 1.1.0
 * @date January 4, 1999
 */

public class MultiFileDialog extends Dialog implements ActionListener,
                                                       WindowListener
{
  // Layout Constants
  private static final int BUTTON_WIDTH     =  70;
  private static final int BUTTON_HEIGHT    =  22;
  private static final int LIST_HEIGHT      = 250;
  private static final int DIRECTORY_WIDTH  = 180;
  private static final int IMPORT_WIDTH     = 180;
  private static final int COMPONENT_HSPACE =  10;
  private static final int COMPONENT_VSPACE =   5;
  private static final int H_INSET          =   5;
  private static final int V_INSET          =  25;
  private static final int PATH_HEIGHT      =  16;
  private static final int TYPE_LABEL_WIDTH =  80;
  private static final int FILE_TYPE_WIDTH  = 200;
  private static final int FILE_TYPE_HEIGHT =  22;
  private static final int PATH_WIDTH = (IMPORT_WIDTH + DIRECTORY_WIDTH +
                          BUTTON_WIDTH);
  private static final int WIDTH = (H_INSET + DIRECTORY_WIDTH + BUTTON_WIDTH +
                        IMPORT_WIDTH + BUTTON_WIDTH +
                        5*COMPONENT_HSPACE);
  private static final int HEIGHT = (V_INSET + 2*PATH_HEIGHT + 
                         3*COMPONENT_VSPACE + LIST_HEIGHT + 
                         FILE_TYPE_HEIGHT + 5);

  // GUI components
  private Label pathLabel;
  private List directoryList;
  private List openList;
  private Choice fileType;
  private Button addButton;
  private Button removeButton;
  private Button addAllButton;
  private Button clearButton;
  private Button importButton;
  private Button cancelButton;
  private String parentItem;

  private String directory;
  private String file;

  private File constantFiles[];  // files to always display, like hard drives

  private File currentFile;
  private File fileList[];
  private Vector importFiles;
  private File importList[];
  private ImportFilter filter;
  
  /**
   * Creates a new dialog with the specified parameters
   *
   * @param parent   the owner of the dialog
   */
  public MultiFileDialog(Frame parent)
  {
    this(parent, "");
  }

  /**
   * Creates a new dialog with the specified parameters
   *
   * @param parent   the owner of the dialog
   * @param title    the name of the dialog box
   */
  public MultiFileDialog(Frame parent, String title)
  {
    super(parent, title, true);

    importFiles = new Vector();

    // find the constantFiles. For Windows/DOS drives
    try {
	File [] fileRoots = File.listRoots();

	if(fileRoots.length > 1) {
	    constantFiles = fileRoots;
	} else {
	    // Don't bother if there is only one root
	    constantFiles = new File[0];
	}
    } catch(NoSuchMethodError e) {
	// Ok, we're on an older version of Java, the listRoots method
	// was added for Java version 1.2. Just go back to the old
	// way of doing things. This may cause Windows to bring up annoying
	// dialogs, but deal.
	System.out.println("Older version of Java detected. Upgrading to" +
			   " Java 2 or later is recommended.");

	Vector tempFileList = new Vector();
	File tempFile;
	for(char letter = 'A'; letter <= 'Z'; letter++) {
	    tempFile = new File("" + letter + ":\\");
	    if(tempFile.exists())
		tempFileList.addElement(tempFile);
	}
	
	constantFiles = new File[tempFileList.size()];
	
	for(int i=0; i < tempFileList.size(); i++)
	    constantFiles[i] = (File) tempFileList.elementAt(i);
    }
   
    // set the directory by using a normal file dialog and getting
    // its directory;
    directory = ".";

    // Find the real name of "."
    File dirFile = new File(".");
    directory = dirFile.getAbsolutePath();
    if(directory.endsWith("."))
      directory = directory.substring(0, directory.length() - 1);

    parentItem = ".. / (move up a directory)";

    addWindowListener(this);

    componentLayout();
  }

  /**
   * Gives a list of files selected by the user.
   *
   * @return the files to use.
   */
  public File[] getFiles()
  {
    return importList;
  }

  /**
   * Gives the filter selected in the dialog box.
   *
   * @return the filter to read in the files selected.
   */
  public ImportFilter getFilter()
  {
    return filter;
  }

  /**
   * Controls the visibility of this dialog. If it is set to be visible,
   * it will perform some initialization stuff, like clearing the old
   * list of files.
   *
   * @param b <code>true</code> to display the dialog.
   */
  public void setVisible(boolean b)
  {
    if(b)
      {
     // initialize some of the stuff
     openList.removeAll();
     importFiles = new Vector();
     currentFile = new File(directory);
     String tempSt = currentFile.getAbsolutePath();
     if(tempSt.endsWith("."))
       tempSt = tempSt.substring(0, tempSt.length() - 1);
     directory = tempSt;
     currentFile = new File(directory);
     displayDirectory(currentFile);
      }

    // let the superclass do the rest
    super.setVisible(b);
  }

  /**
   * Displays the specified directory on the left side of the dialog. All of
   * the files and subdirectories in the directory will be entered in the
   * list. It will also enter all of the files/subdirectories into an
   * array so they can be accessed by looking at the index of a selected
   * item in the list.
   *
   * @param dir  the directory to display.
   */
  protected void displayDirectory(File dir)
  {
    File temp;

    if(!currentFile.isDirectory())
      System.err.println("Invalid directory specification.");

    directoryList.removeAll();
    directoryList.add(parentItem);

    String contents[] = dir.list();
    fileList = new File[contents.length + constantFiles.length];

    try
      {
     directory = dir.getCanonicalPath();
      }
    catch(java.io.IOException e)
      {
     directory = "IO Error";
      }

    pathLabel.setText("Current Path: " + directory);

    directoryList.setVisible(false);
    for(int i=0; i < constantFiles.length; i++)
      {
     fileList[i] = constantFiles[i];
     directoryList.add("[" + constantFiles[i].getPath() + "]");
      }

    File listing[] = new File[contents.length];
    for(int i=0; i < contents.length; i++)
      {
	listing[i] = new File(directory + File.separator + contents[i]);

	if(listing[i].isDirectory())
	  contents[i] += "/";
      }
       
    quicksort(contents, listing, 0, contents.length - 1);

    for(int i=0; i < contents.length; i++)
      {
	fileList[i + constantFiles.length] = listing[i];
	directoryList.add(contents[i]);
      }
    directoryList.setVisible(true);
  }

  /**
   * This will sort the file listing. It uses the compare method to
   * compare two entries, taking into account directories, which are
   * placed first. It uses the standard quicksort algorithm. The code
   * was copied from an example of Robert Keller. The only difference
   * is that is sorts two arrays which are kept in parrallel and 
   * comparisons occur between arrays as well as within the arrays.
   *
   * @param str    the names of all of the files, as they will appear in 
   *               the list
   * @param files  a list of file objects that represents the names in 
   *               <code>str</code> Used to see if a name represents
   *               a directory or not.
   * @param low    the bottom index for the portion of the array to be sorted.
   * @param high   the upper index for the portion of the array to be sorted.
   */
  private static void quicksort(String str[], File files[], int low, int high)
    {
      String tempStr;
      File tempFile;

      if( low >= high )
	return;                           // basis case, <= 1 element

      String pivotStr = str[(low + high)/2];    // select pivot
      File pivotFile = files[(low + high)/2];

      // shift elements <= pivot to bottom
      // shift elements >= pivot to top

      int i = low-1;
      int j = high+1;

      for( ; ; )
	{                             // find two elements to exchange
	  do { i++; } // slide i up
	  while( compare(str[i], files[i], pivotStr, pivotFile) < 0);
	  
	  do { j--; } // slide j down
	  while( compare(str[j], files[j], pivotStr, pivotFile) > 0);

	  if( i >= j )               // if sliders meet or cross
	    break; 
	  
	  // swap the two elementes in both arrays
	  tempStr = str[i];
	  str[i] = str[j];
	  str[j] = tempStr;

	  tempFile = files[i];
	  files[i] = files[j];
	  files[j] = tempFile;
	}

      quicksort(str, files, low, i-1);     // sort lower half
      quicksort(str, files, j+1, high);    // sort upper half
    }

  /**
   * Compare two string/file pairs. This is a standard ordering comparison
   * used by the sorting function. Directories are "less than" standard
   * files. If the two pairs are of the same type (both directories or
   * both files) then their names are compared using standard string
   * comparisons. File objects are used to determine if a file is a standard
   * file or a directroy.
   *
   *@param str1   the name of the first file
   *@param f1     a file or directory whose name is <code>str1</code>
   *@param str2   the name of the second file/direcotry
   *@param f2     the second file. 
   *
   *@return a negative value if the first pair is less than the second,
   *        a positive value if it is greater, and zero if they are equal.
   */
  private static int compare(String str1, File f1, String str2, File f2)
    {
      if(!f1.isDirectory() && !f2.isDirectory())
	return str1.compareTo(str2);
      else if(!f1.isDirectory()) // but j.isDirecory
	return 1;
      else if(!f2.isDirectory())
	return -1;
      else
	return str1.compareTo(str2);
    }

  /**
   * Adds an entry to the list of files to be opened/imported. The entry
   * is specified by the index. It will be shown in the list and in
   * an internal list which will contain the actual file reference.
   *
   * @param selection  the index of the file in the array of currently
   *                   displayed files.
   */
  protected void addFile(int selection)
  {
    if(selection >= 0)  // make sure something is actually selected
      {
	openList.add(fileList[selection - 1].getName());
	importFiles.addElement(fileList[selection - 1]);
      }
  }

  /**
   * Adds entries to the list of files to be opened/imported. The entry
   * is specified by the index. It will be shown in the list and in
   * an internal list which will contain the actual file reference.
   *
   * @param selection  a list of indicies of files in the array of currently
   *                   displayed files.
   */
  protected void addFile(int selection[])
  {
    openList.setVisible(false);
    for(int i=0; i < selection.length; i++)
      {
     addFile(selection[i]);
      }
    openList.setVisible(true);
  }

  /**
   * Removes the specified file from the list of files to open/import.
   *
   * @param index  the index of the file in the list.
   */
  protected void removeFile(int index)
  {
    if(index >= 0)  // make sure something is actually selected
      {
	openList.remove(index);
	importFiles.removeElementAt(index);
      }
  }

  /**
   * Handle the button clicks as well as double clicks on the list.
   * Double clicking on a directory will open it and double-clicking on
   * a file will add it to the list to import.
   */
  public void actionPerformed(ActionEvent e)
  {
    if(e.getSource() == directoryList)
      {
     int index = directoryList.getSelectedIndex();
     // look for moving up to the parent first
     if(index == 0)
       {
         if(currentFile.getParent() != null)
           currentFile = new File(currentFile.getParent());
         displayDirectory(currentFile);
       }
     else
       {
         // see what we selected, subtract 1 form the index since the
         // file list does not include the parent ".."
         if(fileList[index - 1].isDirectory())
           {
          currentFile = fileList[index - 1];
          displayDirectory(currentFile);
           }
         else
           {
          // it must be a file to read in, so add it to the list
          addFile(index);
           }
       }
      }
    else if(e.getSource() == addButton)
      {
     addFile(directoryList.getSelectedIndex());
      }
    else if(e.getSource() == removeButton)
      {
     removeFile(openList.getSelectedIndex());
      }
    else if(e.getSource() == addAllButton)
      {
     openList.setVisible(false);
     for(int i=(1 + constantFiles.length); i <= fileList.length; i++)
       addFile(i);
     openList.setVisible(true);
      }
    else if(e.getSource() == clearButton)
      {
     importFiles = new Vector();
     openList.removeAll();
      }
    else if(e.getSource() == importButton)
      {
     // get the files
     importList = new File[importFiles.size()];
     for(int i=0; i < importList.length; i++)
       importList[i] = (File) importFiles.elementAt(i);
     // set the filter
     filter = (ImportFilter) 
       FeatureList.getImportMgr().get(fileType.getSelectedItem());
     dispose();
     setVisible(false);
      }
    else if(e.getSource() == cancelButton)
      {
     importList = null;
     dispose();
     setVisible(false);
      }
  }

  /** 
   * Closes the dialog and is equivalent to clicking cancel.
   */
  public void windowClosing(WindowEvent e)
  {
    importList = null;
    dispose();
    setVisible(false);
  }

  /**
   * Lays out all of the components for the dialog box
   */
  private void componentLayout()
  {
    setLayout(null);
    setSize(WIDTH, HEIGHT);
    setResizable(false);

    int x = H_INSET;
    int y = V_INSET;

    pathLabel = new Label("Current Path: ");
    add(pathLabel);
    pathLabel.setBounds(x + COMPONENT_HSPACE, y, PATH_WIDTH, PATH_HEIGHT);
    x += COMPONENT_HSPACE;
    y += PATH_HEIGHT + COMPONENT_VSPACE;

    Label filesLabel = new Label("Files: ");
    add(filesLabel);
    filesLabel.setBounds(x, y, DIRECTORY_WIDTH, PATH_HEIGHT);
    y += PATH_HEIGHT;

    directoryList = new List();
    add(directoryList);
    directoryList.addActionListener(this);
    directoryList.setBounds(x, y, DIRECTORY_WIDTH, LIST_HEIGHT);
    y += LIST_HEIGHT + COMPONENT_VSPACE;

    Label fileTypeLabel = new Label("Files of Type:");
    add(fileTypeLabel);
    fileTypeLabel.setBounds(x, y, TYPE_LABEL_WIDTH, FILE_TYPE_HEIGHT);
    
    //=============== File type Choice ====================
    fileType = new Choice();

    // add the items
    String types[] = FeatureList.getImportMgr().getNames();
    for(int i=0; i < types.length; i++)
      fileType.add(types[i]);

    fileType.select(FeatureList.getImportMgr().getDefaultName());

    add(fileType);
    fileType.setBounds(x + TYPE_LABEL_WIDTH, y, FILE_TYPE_WIDTH,
                 FILE_TYPE_HEIGHT);
    x += DIRECTORY_WIDTH + COMPONENT_HSPACE;
    y = y - LIST_HEIGHT - COMPONENT_VSPACE;

    addButton = new Button("Add");
    add(addButton);
    addButton.addActionListener(this);
    addButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
    y += COMPONENT_VSPACE + BUTTON_HEIGHT;

    removeButton = new Button("Remove");
    add(removeButton);
    removeButton.addActionListener(this);
    removeButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
    y += COMPONENT_VSPACE + BUTTON_HEIGHT;

    addAllButton = new Button("Add All");
    add(addAllButton);
    addAllButton.addActionListener(this);
    addAllButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
    y += COMPONENT_VSPACE + BUTTON_HEIGHT;

    clearButton = new Button("Clear");
    add(clearButton);
    clearButton.addActionListener(this);
    clearButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);

    x += BUTTON_WIDTH + COMPONENT_HSPACE;
    y = V_INSET + PATH_HEIGHT + COMPONENT_VSPACE;

    Label importLabel = new Label("Import Files:");
    add(importLabel);
    importLabel.setBounds(x, y, IMPORT_WIDTH, PATH_HEIGHT);
    y += PATH_HEIGHT;
    
    openList = new List();
    add(openList);
    openList.setBounds(x, y, IMPORT_WIDTH, LIST_HEIGHT);
    x += IMPORT_WIDTH + COMPONENT_HSPACE;

    importButton = new Button("Import");
    add(importButton);
    importButton.addActionListener(this);
    importButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
    y += BUTTON_HEIGHT + COMPONENT_VSPACE;

    cancelButton = new Button("Cancel");
    add(cancelButton);
    cancelButton.addActionListener(this);
    cancelButton.setBounds(x, y, BUTTON_WIDTH, BUTTON_HEIGHT);
  }

  // ==================Unused methods required by interfaces=================
  /**Unused*/ public void windowOpened(WindowEvent e) {}
  /**Unused*/ public void windowClosed(WindowEvent e) {}
  /**Unused*/ public void windowIconified(WindowEvent e) {}
  /**Unused*/ public void windowDeiconified(WindowEvent e) {}
  /**Unused*/ public void windowActivated(WindowEvent e) {}
  /**Unused*/ public void windowDeactivated(WindowEvent e) {}
}
