/*
 * Copyright (c) 2005, Stanford University. All rights reserved. 
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions
 * are met: 
 *  - Redistributions of source code must retain the above copyright 
 *    notice, this list of conditions and the following disclaimer. 
 *  - Redistributions in binary form must reproduce the above copyright 
 *    notice, this list of conditions and the following disclaimer in the 
 *    documentation and/or other materials provided with the distribution. 
 *  - Neither the name of the Stanford University nor the names of its 
 *    contributors may be used to endorse or promote products derived 
 *    from this software without specific prior written permission. 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS 
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE 
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN 
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE. 
 */

/*
 * Created on Sep 13, 2005
 * Original author: Christopher Bruns
 */
package org.simtk.isimsu;

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.*;

import javax.jnlp.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;

import org.jdom.input.*;
import org.jdom.*;

import org.simtk.pdb.*;
import org.simtk.molecularstructure.*;
import org.simtk.mvc.SimpleObservable;
import org.simtk.gui.*;
import edu.stanford.ejalbert.BrowserLauncher;

public class ISIMWrapper 
extends JFrame 
implements SaltSelectionListener
{
    protected IonSelectionDialog ionDialog;
    
    public String programName = "SimTK ISIM";
    public String interfaceName = programName + " Interface";
    public String isimInterfaceVersion = "0.9.9";
    
    public static final long serialVersionUID = 01L;
    LoadMoleculeAction loadMoleculeAction;
    // String pqrFileName;
    APBSParameters apbsParameters = null;
    
    File parentWorkingDirectory; // parent directory of the next two
    File programDirectory;
    File workingDirectory;
    
    // File pqrStructureFile;
    SaltRangeSet availableIonConditionRanges = new SaltRangeSet();
    // java.util.List availableIonConditions = new SaltRangeSet();
    
    LogDialog logDialog;
    
    public Image isimIcon = new ImageIcon(getClass().getClassLoader().getResource("resources/images/isim_logo1_small.png")).getImage();
    
    ISIMTask 
        loadMacromoleculeTask, 
        loadIonTask, 
        adjustAPBSParametersTask, 
        // computeMacromoleculePotentialTask,
        computeCombinedPotentialTask,
        apbsAndIsimTask, 
        // inspectParametersTask,
        // computeGCMCPotentialTask,
        viewIonCountsTask
        ;
    
    ISIMStepRow chooseMoleculeRow,
        apbsRow,
        simulateRow,
        isimCombinedRow,
        viewIonCountsRow;
    
    ISIMWrapper() { // Constructor
        super();

        setTitle(interfaceName);
        
        // Center of screen is better than the upper left corner
        setLocationRelativeTo(null);
        
        // Use a custom icon
        this.setIconImage(isimIcon);

        // Create a window for log messages
        logDialog = new LogDialog(this);
        logDialog.append("Progress log (" + programName + ")\n");
        logDialog.append("Started " + new Date() + "\n");
        logDialog.append("\n");

        // loadIonConditions("resources/ion_conditions/test_ion_conditions2.xml");
        loadIonConditions("resources/ion_conditions/NaMgCl.xml");
        loadIonConditions("resources/ion_conditions/CaCl.xml");

        // Create ionDialog after ion ranges are loaded, but before
        // ChooseIonsAction is created (in createStepRows())
        ionDialog = new IonSelectionDialog(this, availableIonConditionRanges);
        ionDialog.addSaltSelectionListener(this);

        // Establish task dependencies
        createTasks();
        
        // Use first ion condition to start with
        // SaltCondition initialSaltCondition = (SaltCondition) availableIonConditions.get(0);

        // apbsParameters = new APBSParameters(2.00, null, null, loadIonTask, initialSaltCondition);
        apbsParameters = new APBSParameters();
        apbsParameters.setDependentTasks(loadIonTask, adjustAPBSParametersTask, loadMacromoleculeTask);
        apbsParameters.setMoleculePermittivity(2.0);
        apbsParameters.setIons(null);

        // dime options are 65, 97, 129, and 161 (for nlev = 4)
        apbsParameters.setDimension(97, 97, 97);
        
        // Create one structure loading action object, for both button and menu activation
        loadMoleculeAction = new LoadMoleculeAction(this);

        // Make close button well... close.
        // Revert to Frame behavior, w/ actionListener etc.
        // Disable JFrame handling of close event
        setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
        // Add old way of handling close event
        addWindowListener
        (new WindowAdapter() {
            public void windowClosing(WindowEvent e) {
                cleanUpAndExit();
               }
            }
        );
        
        createMenuBar();
        
        createStepRows();
        
        createDirectories();
        
        apbsParameters.setMacroionPotentialFile(new File(workingDirectory, "macroionPotential.dx"));

//        SaltCondition noIonsCondition = new SaltCondition();
//        noIonsCondition.setName("(no ions)");
//        availableIonConditions.add(noIonsCondition);
//        apbsParameters.setIons(noIonsCondition);        
                
        pack();

    }
    
    /**
     * @param args
     */
    public static void main(String[] args) {
        // Try to get Windows look and feel on Windows platform
        // Unless a non-Java look and feel is already selected
        if (UIManager.getLookAndFeel().getClass().getName().equals(UIManager.getCrossPlatformLookAndFeelClassName()))
            try {
                UIManager.setLookAndFeel(
                    UIManager.getSystemLookAndFeelClassName() );
            } catch (Exception e) {}

        // Poorly documented new way to make sure that the GUI is created in the Event thread
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                //
                // Build and start the GUI here.
                //
                ISIMWrapper isimFrame = new ISIMWrapper();
                isimFrame.setVisible(true);
                // isimFrame.showLicenseDialog();
            }
        }); 

    }

    public void saltSelected(SaltSelectionEvent event) {
        SaltCondition condition = event.getSaltCondition();

        // Use this as the simulation condition and declare step complete
        apbsParameters.setIons(condition);
        
        System.out.println("Salt selected");
    }
    
    // Load precomputed excess chemical potential data for ion conditions
    private void loadIonConditions(String resourceString) {
        logDialog.append("Loading ion conditions...\n");

        ClassLoader classLoader = getClass().getClassLoader();
        URL ionXMLURL = classLoader.getResource(resourceString);
        if (ionXMLURL == null) {
            JOptionPane.showMessageDialog(this, "Unable to load ion conditions resource XML file " + resourceString);
            return;
        }

        try {
            availableIonConditionRanges.addRanges(ionXMLURL);
        } 
        catch (JDOMException exc) {
            exc.printStackTrace();
        }
        catch (IOException exc) {
            exc.printStackTrace();
            JOptionPane.showMessageDialog(this, "Problem loading ion conditions resource XML file " + resourceString);
        }
        
        logDialog.append("Ion conditions loaded.\n");
    }
    
    /**
     * This method encodes the task dependencies of the kind that might
     * be specified in a makefile.
     *
     */
    void createTasks() {
        loadMacromoleculeTask = new ISIMTask();
        loadIonTask = new ISIMTask();
        adjustAPBSParametersTask = new ISIMTask();
        apbsAndIsimTask = new ISIMTask();
        // computeMacromoleculePotentialTask = new ISIMTask();
        // inspectParametersTask = new ISIMTask();
        computeCombinedPotentialTask = new ISIMTask();
        // computeGCMCPotentialTask = new ISIMTask();
        viewIonCountsTask = new ISIMTask();
        
        // We need a macromolecule before we compute the macromolecule potential
        apbsAndIsimTask.addPrerequisite(loadMacromoleculeTask);
        // computeMacromoleculePotentialTask.addPrerequisite(inspectParametersTask);
        // Since temperature and permittivity are now part of the ion dialog, APBS depends upon ion step
        apbsAndIsimTask.addPrerequisite(adjustAPBSParametersTask);
        // and upon the load Ion task
        apbsAndIsimTask.addPrerequisite(loadIonTask);

        // We need a macromolecule before we compute the macromolecule potential
        // computeMacromoleculePotentialTask.addPrerequisite(loadMacromoleculeTask);
        // computeMacromoleculePotentialTask.addPrerequisite(inspectParametersTask);
        // Since temperature and permittivity are now part of the ion dialog, APBS depends upon ion step
        // computeMacromoleculePotentialTask.addPrerequisite(adjustAPBSParametersTask);
        // and upon the load Ion task
        // computeMacromoleculePotentialTask.addPrerequisite(loadIonTask);

        // The second potential calculation requires both macromolecule and ions
        computeCombinedPotentialTask.addPrerequisite(loadMacromoleculeTask);
        computeCombinedPotentialTask.addPrerequisite(adjustAPBSParametersTask);
        computeCombinedPotentialTask.addPrerequisite(loadIonTask);
        
        // ISIM requires the ions, macromolecule, and the first macromolecule potential
        // computeGCMCPotentialTask.addPrerequisite(loadMacromoleculeTask);
        // computeGCMCPotentialTask.addPrerequisite(adjustAPBSParametersTask);
        // computeGCMCPotentialTask.addPrerequisite(loadIonTask);
        // computeGCMCPotentialTask.addPrerequisite(computeMacromoleculePotentialTask);
        
        // viewIonCountsTask.addPrerequisite(computeGCMCPotentialTask);
        viewIonCountsTask.addPrerequisite(apbsAndIsimTask);
        
        // Some tasks are good enough at the start, others need to be performed...
        loadIonTask.setStatus(DependentTask.READY_FOR_UPDATE, "(no ions)"); // Lack of ions is a reasonable state...
        adjustAPBSParametersTask.setStatus(DependentTask.CURRENT_AND_COMPLETE, "");
        loadMacromoleculeTask.setStatus(DependentTask.READY_FOR_UPDATE, "(no molecule loaded)");
        // computeMacromoleculePotentialTask.setStatus(DependentTask.WAITING_FOR_PREREQUISITES, "");
        computeCombinedPotentialTask.setStatus(DependentTask.WAITING_FOR_PREREQUISITES, "");
        // computeGCMCPotentialTask.setStatus(DependentTask.WAITING_FOR_PREREQUISITES, "");
        apbsAndIsimTask.setStatus(DependentTask.WAITING_FOR_PREREQUISITES, "");
        viewIonCountsTask.setStatus(DependentTask.WAITING_FOR_PREREQUISITES, "");
    }

    void createDirectories() {
        // Create a top level directory
        String topDirectoryName = "ISIM";
        try {
            File tempDir = File.createTempFile(topDirectoryName, "");
            
            // Turn the normal temp file into a directory
            if (!tempDir.delete())
                throw new IOException("Unable to replace temporary file " + tempDir);
            if (!tempDir.mkdir())
                throw new IOException("Unable to create temporary directory " + tempDir);
            
            parentWorkingDirectory = tempDir;
            workingDirectory = new File(tempDir, "working");
            programDirectory = workingDirectory;
            
            if (! programDirectory.exists())
                if (! programDirectory.mkdir()) throw new IOException("Unable to create program directory " + programDirectory);
            if (! workingDirectory.exists())
                if (! workingDirectory.mkdir()) throw new IOException("Unable to create working directory " + workingDirectory);
        }
        catch (IOException exc) {
            exc.printStackTrace();
            System.exit(1);
        }

    }
    
    void createMenuBar() {
        // Menus
        JMenuBar menuBar = new JMenuBar();
        
        JMenu menu = new JMenu("File");
        menuBar.add(menu);
        
        JMenuItem menuItem;

        // menuItem = new JMenuItem("Load PQR Structure File...");
        // menuItem.addActionListener(loadMoleculeAction);
        // menu.add(menuItem);
        
        menuItem = new JMenuItem("About " + interfaceName + "...");
        menuItem.addActionListener(new AboutAction(this));
        menu.add(menuItem);
        
        menu.add(new JSeparator());

        menuItem = new JMenuItem("Quit");
        menuItem.addActionListener(new QuitAction(this));
        menu.add(menuItem);
        
        menu = new JMenu("View");
        menuBar.add(menu);
        
        menuItem = new JMenuItem("View Log");
        menuItem.addActionListener(new ViewLogAction(this));
        menu.add(menuItem);
        
        menu = new JMenu("Help");
        try {
            menuBar.setHelpMenu(menu); // Not yet implemented error!?!?
        } catch (java.lang.Error error) {
            // setHelpMenu seems to be "not yet implemented in java 1.4.2_06
            menuBar.add(menu);
        }
        
        menuItem = new JMenuItem("About " + interfaceName + "...");
        menuItem.addActionListener(new AboutAction(this));
        menu.add(menuItem);
        
        menu.add(new JSeparator());
        menuItem = new JMenuItem("Web Links:");
        menuItem.setEnabled(false);
        menu.add(menuItem);
        
        menuItem = new JMenuItem("  Report a program problem (bug)...");
        menuItem.addActionListener(new BrowserLaunchAction
                ("https://simtk.org/tracker/?func=add&atid=310&group_id=85"));
        menu.add(menuItem);

        menuItem = new JMenuItem("  Request a new program feature...");
        menuItem.addActionListener(new BrowserLaunchAction
                ("https://simtk.org/tracker/?func=add&atid=311&group_id=85"));
        menu.add(menuItem);


        setJMenuBar(menuBar);        
    }

    void createStepRows() {
        // One white container for all step rows
        JPanel stepsPanel = new JPanel();
        stepsPanel.setBackground(Color.WHITE);

        GridBagLayout layout = new GridBagLayout();
        stepsPanel.setLayout(layout);
        // Small border around outer edge for better white space feel
        stepsPanel.setBorder(new EmptyBorder(10,10,10,10));
        
        // Put top row of header information
        ISIMStepRow headerRow = new ISIMStepRow(null, "");
        headerRow.setLabels("", "", "Press buttons to perform tasks", "Status");
        headerRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.HEADER_ROW);
        
        // Create "choose molecule" row
        chooseMoleculeRow = new ISIMStepRow(loadMacromoleculeTask, "Choose Molecule...");
        // chooseMoleculeRow.setLabels("", "(no molecule loaded)", "Choose Molecule...", "No");
        chooseMoleculeRow.button.addActionListener(loadMoleculeAction);
        chooseMoleculeRow.button.setToolTipText("Click this button to begin choosing a macromolecule structure.");
        chooseMoleculeRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        chooseMoleculeRow.updateRow();
                
//        // Create "inspect parameters" row
//        ISIMStepRow paramsRow = new ISIMStepRow(inspectParametersTask, "Inspect parameters...");
//        // paramsRow.setLabels("Temperature, Permittivity, etc. ", "", "Inspect parameters...", "Yes");
//        // paramsRow.insertWithButton(stepsPanel, layout);
//        paramsRow.button.addActionListener(new InspectParametersAction(apbsParameters, this));
//        paramsRow.updateRow();
//
        ClassLoader classLoader = getClass().getClassLoader();
        Icon computerIcon = new ImageIcon(classLoader.getResource("resources/images/ComputerIcon2Clock.png"));

        // Create "choose ions" row
        ISIMStepRow ionsRow = new ISIMStepRow(loadIonTask, "Choose ion conditions...");
        // ionsRow.setLabels("", "(no ions selected)", "Choose ion conditions...", "Yes");
        ionsRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        ionsRow.button.addActionListener(new ChooseIonsAction(ionDialog));
        ionsRow.button.setToolTipText("Click this button to begin choosing dissolved ions.");
        ionsRow.updateRow();

        // Create "compute Poisson-Boltzman potential" row
        simulateRow = new ISIMStepRow(apbsAndIsimTask, "Simulate Ions");
        simulateRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        simulateRow.button.addActionListener(new SimulateIons(this));
        simulateRow.button.setIcon(computerIcon);
        simulateRow.button.setToolTipText("Simulate ions by running APBS then ISIM.\n WARNING: This might take several minutes.");
        simulateRow.updateRow();

        // Create "compute Poisson-Boltzman potential" row
        // apbsRow = new ISIMStepRow(computeMacromoleculePotentialTask, "Compute macromolecule field (without ions)");
        // apbsRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        // apbsRow.button.addActionListener(new ComputeMoleculeAPBS(this));
        // apbsRow.button.setIcon(computerIcon);
        // apbsRow.button.setToolTipText("Compute nonlinear Poisson-Boltzmann potential (APBS).\nWARNING: This might take a few minutes.");
        // apbsRow.updateRow();

        // Create "compute Poisson-Boltzman potential" row
        ISIMStepRow apbsCombinedRow = new ISIMStepRow(computeCombinedPotentialTask, "Compute combined field");
        // apbsCombinedRow.setLabels("Poisson-Boltzmann Potential (APBS)", "", "Compute combined field", "No");
        // apbsCombinedRow.insertWithButton(stepsPanel, layout);
        // apbsCombinedRow.button.addActionListener(new ComputeMoleculeAPBS(this));
        apbsCombinedRow.button.setIcon(computerIcon);
        apbsCombinedRow.updateRow();

        // Create "compute GCMC potential" row
        // isimCombinedRow = new ISIMStepRow(computeGCMCPotentialTask, "Compute GCMC combined field");
        // isimCombinedRow.setLabels("", "", "Compute GCMC combined field", "No");
        // isimCombinedRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        // isimCombinedRow.button.addActionListener(new ComputeISIM(this));
        // isimCombinedRow.button.setIcon(computerIcon);
        // isimCombinedRow.button.setToolTipText("Compute Grand Canonical Monte Carlo ion distribution (ISIM).\nWARNING: This might take a few minutes.");
        // isimCombinedRow.updateRow();

        // Create "view ion counts" row
        viewIonCountsRow = new ISIMStepRow(viewIonCountsTask, "View results");
        viewIonCountsRow.insertButtonRow(stepsPanel, layout, ISIMStepRow.BUTTON_ROW);
        viewIonCountsRow.button.addActionListener(new ViewIonCountsAction(this));
        viewIonCountsRow.button.setToolTipText("Show a table summarizing the ion simulation.");
        viewIonCountsRow.updateRow();

        getContentPane().add(stepsPanel);
        
        // Create help for all step rows
        chooseMoleculeRow.setHelpText(
                "" + programName + " Help: Load Molecule step"
                ,
                "You must choose a molecule about which to compute the ionic\n" +
                "environment.  Usually this molecule is RNA, DNA, or protein\n" +
                "\n"+
                "Press the \"Choose Molecule\" Button to load a macromolecular\n" +
                "structure file.  The file can be either in Protein Data Bank\n" +
                "(PDB) format, or in PQR format.  PQR format contains partial\n" +
                "charge information for each atom, while PDB format does not.\n"
                ,
                this
                );

        ionsRow.setHelpText(
                "" + programName + " Help: Selected dissolved ions step"
                ,
                "Select the set of dissolved ions in the solution.  Concentrations\n" +
                "are specified in units of millimols per liter (mM).  Other parameters\n" +
                "include temperature, permittivity of the solvent, and permittivity of\n" +
                "the macroion.  Excess chemical potentials for each ionic condition\n" +
                "are expensive to calculate, so they are precomputed.\n"                
                ,
                this
                );
        
        simulateRow.setHelpText(
                "" + programName + " Help: Compute macromolecule field step"
                , 
                "First, computes the macromolecule electrostatic field using\n" +
                "APBS; then simulates ion positions using ISIM.\n"
                ,
                this
                );

//        apbsRow.setHelpText(
//                "" + programName + " Help: Compute macromolecule field step"
//                , 
//                "Computes the electrostatic field around the macromolecule using\n" +
//                "the Poisson Boltzmann equation.  This step\n" +
//                "does not include the dissolved ions in the field calculation, but is\n" +
//                "required for the later GCMC calculation step.  The field caculation is\n" +
//                "performed using the program APBS.\n"
//                ,
//                this
//                );

//        isimCombinedRow.setHelpText(
//                "" + programName + " Help: Compute complete field step"
//                , 
//                "Computes the electrostatic field around the macromolecule using\n" +
//                "a Grand Canonical Monte Carlo (GCMC) method.  This step\n" +
//                "includes both the macromolecule and the dissolved ions in the field\n"+
//                "calculation.  The calculation is performed using the program ISIM.\n"
//                ,
//                this
//                );

        viewIonCountsRow.setHelpText(
                "" + programName + " Help: View ion counts"
                , 
                "'View ion counts' displays a table summarizing the average number\n" +
                "of ions in the simulation, compared to the number expected had the\n" +
                "macroion been absent.  The difference counts can be thought of as\n" +
                "'salt ions associated with the macromolecule'\n"
                ,
                this
                );
        
    }

    String errorMessage;
    
    MoleculeAcquisitionMethodDialog moleculeAcquisitionMethodDialog = null;
    void askUserToChooseAMolecule() {        
        if (moleculeAcquisitionMethodDialog == null) 
            moleculeAcquisitionMethodDialog = new ISIMLoadMoleculeDialog(this, null);
        moleculeAcquisitionMethodDialog.setVisible(true);
    }
    
    void convertPDBToPQR(File pdbFile) {
        
        String convertMessage = "Converting PDB format structure file "+pdbFile+" to PQR format...\n";
        appendToLog(convertMessage);
        
        // Run this outside of the event thread
        PDBToPQRThread thread = 
            new PDBToPQRThread(
                    new ISIMStepParameters(programDirectory, 
                            workingDirectory,
                            loadMacromoleculeTask
                            ),
                    apbsParameters,
                    pdbFile,
                    this
                    );
        thread.start();
        
        // Attach progress monitor to conversion thread
        ProgressDialog progressDialog = chooseMoleculeRow;
        loadMacromoleculeTask.setStatus(DependentTask.ACTIVELY_UPDATING, "Converting to PQR format");
        
        ProgressManager progressManager;
        
        assert(progressDialog != null);
        
        if (progressDialog == null)
            progressManager = 
                new ProgressManager(
                        thread, 
                        this, 
                        convertMessage);
        else 
            progressManager = new ProgressManager(thread, progressDialog);        
        progressManager.start();        
    }
    
    void measureStructureSize (File pqrFile) {
        
        String convertMessage = "Measuring size of molecule in file "+pqrFile+"...\n";
        appendToLog(convertMessage);
        
        // Run this outside of the event thread
        RunPsizeThread thread = 
            new RunPsizeThread(
                    new ISIMStepParameters(programDirectory, 
                            workingDirectory,
                            loadMacromoleculeTask
                            ),
                    apbsParameters,
                    pqrFile,
                    this
                    );
        thread.start();
        
        // Attach progress monitor to conversion thread
        ProgressDialog progressDialog = chooseMoleculeRow;
        loadMacromoleculeTask.setStatus(DependentTask.ACTIVELY_UPDATING, "Measuring molecule size");
        
        ProgressManager progressManager;
        
        assert(progressDialog != null);
        
        if (progressDialog == null)
            progressManager = 
                new ProgressManager(
                        thread, 
                        this, 
                        convertMessage);
        else 
            progressManager = new ProgressManager(thread, progressDialog);        
        progressManager.start();        
    }

    public void setPqrFile(File pqrFile) {

        // Run psize.py on the molecule to get the molecule dimensions
        measureStructureSize(pqrFile);

        // moleculeFile should already be in PQR format
        apbsParameters.setPqrInputFile(pqrFile);        
        // loadMacromoleculeTask.dataDescription = pqrFile.getName();
        // loadMacromoleculeTask.setStatus(DependentTask.CURRENT_AND_COMPLETE, pqrFile.getName());
        
        appendToLog("Using molecule structure file " + pqrFile + "\n");
    }
    
    /**
     * Routine to recursively delete a directory and its contents.
     * Adapted from http://joust.kano.net/weblog/archives/000071.html
     * Note that this routine could be very dangerous.
     * 
     * @param dir
     * @return Returns "true" on success, "false" otherwise
     */
    private static boolean recursiveDeleteDir(File dir) {
        
        if (dir == null) return false;
        
        // to see if this directory is actually a symbolic link to a directory,
        // we want to get its canonical path - that is, we follow the link to
        // the file it's actually linked to
        File candir;
        try {
            candir = dir.getCanonicalFile();
        } catch (IOException e) {
            return false;
        }
  
        // TODO - but this does not work on Windows
        // (distinguishing between symbolic links and real files)
        // getAbsoluteFile() returns a File with shortened names, e.g. "C:\DOCUME~1\CHRIST~1\LOCALS~1\Temp\ISIM58047"
        // getCanonicalFile() returns expanded names, e.g. "C:\Documents and Settings\Christopher Bruns\Local Settings\Temp\ISIM58047"
        // The File.equals(File) method returns *false* between these two cases
        // (It should return *true*)
        
        // File absdir = dir.getAbsoluteFile();
        
        // a symbolic link has a different canonical path than its actual path,
        // unless it's a link to itself
        // if (! (candir.equals(absdir)) ) {
            // this file is a symbolic link, and there's no reason for us to
            // follow it, because then we might be deleting something outside of
            // the directory we were told to delete
            // return false;
        // }
  
        // Warning: there is a possible race condition here, if the file becomes
        // a symbolic link after the above check, but before the delete.
        
        // now we go through all of the files and subdirectories in the
        // directory and delete them one by one
        File[] files = candir.listFiles();
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File file = files[i];
  
                // in case this directory is actually a symbolic link, or it's
                // empty, we want to try to delete the link before we try
                // anything
                boolean deleted = file.delete();
                if (!deleted) {
                    // deleting the file failed, so maybe it's a non-empty
                    // directory
                    if (file.isDirectory()) recursiveDeleteDir(file);
  
                    // otherwise, there's nothing else we can do
                }
            }
        }
  
        // now that we tried to clear the directory out, we can try to delete it
        // again
        return dir.delete();  
    }
    
    /**
     * Routine to delete a directory and its contents.
     * Adapted from http://joust.kano.net/weblog/archives/000071.html
     * This is meant to be safer than the recursiveDeleteDir() method, because
     * It does not recursively follow subdirectories.
     * It is intended to remove symbolic links in the dirctory, but not the
     * targets of those links.
     * This method will fail (and return false) if the argument directory
     * returns false.  In this case the contents of the directory may only
     * be partially removed.
     * 
     * @param dir
     * @return Returns "true" on success, "false" otherwise
     */
    private static boolean deleteDir(File dir) {
        
        if (dir == null) return false;
        
        File absdir = dir.getAbsoluteFile();
        
        // now we go through all of the files and subdirectories in the
        // directory and delete them one by one
        File[] files = absdir.listFiles();
        if (files != null) {
            for (int i = 0; i < files.length; i++) {
                File file = files[i];
  
                // in case this directory is actually a symbolic link, or it's
                // empty, we want to try to delete the link before we try
                // anything
                // boolean deleted = file.delete();
                file.delete();
                
                // Note: non-empty directories do not get deleted. This is the price
                // of relative safety compared to recursiveDeleteDir
            }
        }
  
        // now that we tried to clear the directory out, we can try to delete it
        // again
        return dir.delete();  
    }
    

    /**
     * Delete working area and all files therein
     *
     */
    private void destroyAllOfMyBeautifulWork() {
        boolean deleteSucceeded = true; // Start optimistic
         
        // First remove inner working directory
        logDialog.append("Deleting " + workingDirectory + "...\n");
        if (! deleteDir(workingDirectory)) deleteSucceeded = false;
        
        // Then remove top level working directory, i.e. the resulte of createTempFile()
        logDialog.append("Deleting " + parentWorkingDirectory + "...\n");
        if (! deleteDir(parentWorkingDirectory)) deleteSucceeded = false;

        if (deleteSucceeded) logDialog.append("Delete succeeded.\n");
        else logDialog.append("Delete failed.\n");
    }
    
    void cleanUpAndExit() {
        destroyAllOfMyBeautifulWork();
        System.exit(0);
    }
    
    public void appendToLog(String logMessage) {
        logDialog.append(logMessage);
    }
    
    public void showLicenseDialog() {
        
        // TODO - put actual licenses in help menu
        final String licenseText = ""

            +interfaceName + " version " + isimInterfaceVersion + "\n"
            +"\n"
            +interfaceName + " is a Java graphical user interface that runs \n"
            +"the programs ABPS and ISIM.\n"
            +"\n"
            +"The " + interfaceName + " Java application is distributed under an \n"
            +"MIT open source license (see Documentation).\n"
            +"\n"
            +"APBS (Adaptive Poisson Boltzmann Solver) is available from \n"
            +"http://apbs.sourceforge.net/\n"
            +"\n"
            +"ISIM (Ion Simulator) is available from \n"
            +"http://mccammon.ucsd.edu/isim/"
           
            ;
        
        JOptionPane.showMessageDialog(this,
                licenseText,
                interfaceName + " Licence",
                JOptionPane.INFORMATION_MESSAGE);
    }

    class BrowserLaunchAction implements ActionListener {
        String urlString;
        BrowserLaunchAction(String u) {urlString = u;}
        public void actionPerformed(ActionEvent e) {
            URL url;
            try {url = new URL(urlString);}            
            catch (MalformedURLException exc) {
                // Show information dialog, so the savvy user will be able to
                // go to the url manually, in case the browser open fails.
                JOptionPane.showConfirmDialog(
                        ISIMWrapper.this, 
                        "Problem opening browser to page " + urlString + "\n" + exc, 
                        "!!! Error opening web browser !!!",
                        JOptionPane.DEFAULT_OPTION, 
                        JOptionPane.ERROR_MESSAGE
                        );
                return;
            }
            try {
                // This only works when started in a web start application
                BasicService bs = (BasicService)ServiceManager.lookup("javax.jnlp.BasicService");
                bs.showDocument(url);
            } 
            catch (UnavailableServiceException exc) {
                // launchErrorConfirmDialog("Problem opening browser to page " + urlString + "\n" + exc,
                // "JNLP Error!");
                try {BrowserLauncher.openURL(urlString);}
                catch (IOException exc2) {
                    JOptionPane.showConfirmDialog(
                            ISIMWrapper.this, 
                            "Problem opening browser to page " + urlString + "\n" + exc, 
                            "!!! Error opening web browser !!!",
                            JOptionPane.DEFAULT_OPTION, 
                            JOptionPane.ERROR_MESSAGE
                            );
                }        
            }
        }
    }
}


class ViewLogAction implements ActionListener {
    ISIMWrapper parentFrame;
    public ViewLogAction(ISIMWrapper parentFrame) {
        this.parentFrame = parentFrame;
    }
    public void actionPerformed(ActionEvent e) {
        parentFrame.logDialog.setLocationRelativeTo(parentFrame);
        parentFrame.logDialog.setVisible(true);
    }
}

class AboutAction implements ActionListener {
    ISIMWrapper parentFrame;
    public AboutAction(ISIMWrapper parentFrame) {
        this.parentFrame = parentFrame;
    }
    public void actionPerformed(ActionEvent e) {
        JOptionPane.showMessageDialog(
                parentFrame,

                "" + parentFrame.interfaceName + " version " + 
                parentFrame.isimInterfaceVersion + "\n" +
                "created by Christopher Bruns at SimTK.org\n" +
                "Includes binary executables for APBS and ISIM (see documentation).",

                "About " + parentFrame.interfaceName,

                JOptionPane.PLAIN_MESSAGE);
    }
}

//class ComputeISIM implements ActionListener {
//    ISIMWrapper parentFrame;
//    
//    public ComputeISIM(ISIMWrapper parent) {
//        this.parentFrame = parent;
//    }
//    
//    public void actionPerformed(ActionEvent e) {
//        RunISIMThread thread = 
//            new RunISIMThread(
//                    new ISIMStepParameters(parentFrame.programDirectory, 
//                            parentFrame.workingDirectory,
//                            parentFrame.computeGCMCPotentialTask),
//                    parentFrame.apbsParameters,
//                    parentFrame
//                    );
//        thread.start();
//
//        // And another process to monitor the download process
//        ProgressDialog progressDialog = parentFrame.isimCombinedRow;
//        ProgressManager  progressManager = new ProgressManager(thread, progressDialog);
//        progressManager.start();        
//    }
//}

/**
 *  
  * @author Christopher Bruns
  * 
  * SimulateIons combines functions of ComputeMoleculeAPBS and ComputeISIM
 */
class SimulateIons implements ActionListener {
    ISIMWrapper parentFrame;
    
    public SimulateIons(ISIMWrapper parent) {
        parentFrame = parent;
    }
    
    public void actionPerformed(ActionEvent e) {

        SimulateIonsThread thread = 
            new SimulateIonsThread(
                    new ISIMStepParameters(parentFrame.programDirectory, 
                            parentFrame.workingDirectory,
                            parentFrame.apbsAndIsimTask),
                    parentFrame.apbsParameters,
                    parentFrame);
        thread.start();

        // And another process to monitor the download process
        ProgressDialog progressDialog = parentFrame.simulateRow;
        ProgressManager  progressManager = new ProgressManager(thread, progressDialog);
        progressManager.start();        
        
    }
}

//class ComputeMoleculeAPBS implements ActionListener {
//    ISIMWrapper parentFrame;
//    
//    public ComputeMoleculeAPBS(ISIMWrapper parent) {
//        parentFrame = parent;
//    }
//    
//    public void actionPerformed(ActionEvent e) {
//
//        RunAPBSThread thread = 
//            new RunAPBSThread(
//                    new ISIMStepParameters(parentFrame.programDirectory, 
//                            parentFrame.workingDirectory,
//                            parentFrame.computeMacromoleculePotentialTask),
//                    parentFrame.apbsParameters,
//                    parentFrame);
//        thread.start();
//
//        // And another process to monitor the download process
//        ProgressDialog progressDialog = parentFrame.apbsRow;
//        ProgressManager  progressManager = new ProgressManager(thread, progressDialog);
//        progressManager.start();        
//        
//    }
//}

class ViewIonCountsAction implements ActionListener {
    ISIMWrapper parentFrame;
    IonCountDialog ionCountDialog;
    
    ViewIonCountsAction(ISIMWrapper parentFrame) {
        this.parentFrame = parentFrame;
        ionCountDialog = new IonCountDialog(parentFrame);
    }
    
    public void actionPerformed(ActionEvent e) {
        // Show the user the ion table
        try {
            // Set start time of view counts action
            parentFrame.viewIonCountsTask.setStatus(DependentTask.ACTIVELY_UPDATING,
                    "");

            ionCountDialog.loadAllIonCounts();
            ionCountDialog.setVisible(true);

            // Mark view action complete
            parentFrame.viewIonCountsTask.setStatus(DependentTask.CURRENT_AND_COMPLETE,
                    "");
        } catch (IOException exc) {
            exc.printStackTrace();
            // TODO problem reading ion information files
        }
    }
}

class LoadMoleculeAction implements ActionListener {
    ISIMWrapper parentFrame;

    LoadMoleculeAction(ISIMWrapper parent) {
        parentFrame = parent;
    }

    public void actionPerformed(ActionEvent e) {
        parentFrame.askUserToChooseAMolecule();
    }

}

class QuitAction implements ActionListener {
    ISIMWrapper isimWrapper;
    QuitAction(ISIMWrapper isimWrapper) {
        this.isimWrapper = isimWrapper;
    }
    public void actionPerformed(ActionEvent e) {
        isimWrapper.cleanUpAndExit();
    }
}

//class InspectParametersAction implements ActionListener {
//    APBSParameters parameters;
//    ParameterDialog dialog;
//    JFrame parentFrame;
//    
//    InspectParametersAction(APBSParameters p, JFrame parent) {
//        parameters = p;
//        parentFrame = parent;
//    }
//
//    public void actionPerformed(ActionEvent e) {
//        if (dialog == null) {
//            dialog = new ParameterDialog(parameters, parentFrame);
//        }
//        dialog.setVisible(true);
//    }
//}

class ChooseIonsAction implements ActionListener {
    IonSelectionDialog ionDialog;
    ChooseIonsAction(IonSelectionDialog ionDialog) {
        this.ionDialog = ionDialog;
    }
    public void actionPerformed(ActionEvent e) {
        ionDialog.setVisible(true);
    }    
}

//// Interface in which to view and control the parameters
//class ParameterDialog extends JDialog implements ActionListener {
//    public static final long serialVersionUID = 01L;
//    APBSParameters parameters;
//
//    JButton undoButton = new JButton("Undo");
//    JButton redoButton = new JButton("Redo");
//    JButton doneButton = new JButton("Done");
//
//    java.util.Vector parameterHistory = new java.util.Vector();    
//    int currentHistoryPosition = -1;
//    
//    JComboBox temperatureBox, solventPBox, moleculePBox;
//    
//    ParameterDialog(APBSParameters p, JFrame parentFrame) {
//        super(parentFrame, "Parameters for simulation");
//
//        parameters = p;
//
//        parameterHistory.add(new APBSParameters(parameters));
//        currentHistoryPosition = 0;
//
//        this.getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.Y_AXIS));
//        
//        // Text at the top of the dialog 
//        JPanel headerPanel = new JPanel();
//        headerPanel.add(new JLabel("Set simulation parameters below:"));
//        getContentPane().add(headerPanel);
//        
//        temperatureBox = addPanel("Temperature (\u00B0K)", parameters.temperature);
//        solventPBox = addPanel("Solvent permittivity/dielectric (unitless)", parameters.solventPermittivity);
//        moleculePBox = addPanel("Macromolecule permittivity/dielectric (unitless)", parameters.moleculePermittivity);
//        
//        // Buttons
//        JPanel buttonPanel = new JPanel();
//        
//        buttonPanel.add(undoButton);
//        buttonPanel.add(redoButton);
//        buttonPanel.add(doneButton);
//        
//        undoButton.addActionListener(this);
//        redoButton.addActionListener(this);
//        doneButton.addActionListener(this);
//        
//        getContentPane().add(buttonPanel);
//        
//        this.pack();
//        checkButtons();
//    }
//    
//    JComboBox addPanel(String label, double currentValue) {
//        JPanel panel = new JPanel();
//        panel.setLayout(new BoxLayout(panel, BoxLayout.X_AXIS));
//        panel.add(new JLabel(label));
//
//        JComboBox pullDownMenu = new JComboBox();
//        pullDownMenu.setEditable(true);
//        pullDownMenu.addItem((new Double(currentValue)).toString());
//        pullDownMenu.addActionListener(this);
//
//        panel.add(pullDownMenu);
//        getContentPane().add(panel); 
//        
//        return pullDownMenu;
//    }
//    
//    // Capture button events and parameter change events here
//    boolean automaticDisplayUpdating = false;
//    public void actionPerformed(ActionEvent event) {
//
//        if (event.getSource() == doneButton) {
//            setVisible(false);
//        }
//
//        else if (event.getSource() == undoButton) {
//            checkButtons();
//            if (currentHistoryPosition < 1) return; // should not happen
//
//            currentHistoryPosition --;
//            updateParameters();
//        }
//
//        else if (event.getSource() == redoButton) {
//            checkButtons();
//            if (currentHistoryPosition >= (parameterHistory.size() - 1) ) return; // should not happen
//
//            currentHistoryPosition ++;
//            updateParameters();
//        }
//        
//        else if (event.getSource() instanceof JComboBox) {
//            // One of the values changed?
//            if (automaticDisplayUpdating) return; // Don't race condition with auto update
//            
//            // Have any values really changed?
//            APBSParameters displayedParameters = getDisplayedParameters();
//            if (! (displayedParameters.equals(parameters))) {
//                System.out.println("Parameter changed");
//                
//                // Add new parameter set
//                parameters.copyFrom(displayedParameters);
//                currentHistoryPosition ++;
//                parameterHistory.insertElementAt(displayedParameters, currentHistoryPosition);
//                // Remove old future elements
//                while (parameterHistory.size() > (currentHistoryPosition + 1)) {
//                    // Remove terminal element
//                    parameterHistory.removeElementAt(parameterHistory.size() - 1); // pop
//                }
//                System.out.println("Current history position = " + currentHistoryPosition);
//                checkButtons();
//            }
//        }
//    }
//    
////    APBSParameters getDisplayedParameters() {
////        APBSParameters p = new APBSParameters(parameters);
////
////        p.temperature = (new Double((String) temperatureBox.getSelectedItem())).doubleValue();
////        p.solventPermittivity = (new Double((String) solventPBox.getSelectedItem())).doubleValue();
////        p.moleculePermittivity = (new Double((String) moleculePBox.getSelectedItem())).doubleValue();
////        
////        return p;
////    }
//    
//    // Enable or disable redo/undo buttons
//    void checkButtons() {
//        if (currentHistoryPosition < 1) undoButton.setEnabled(false);
//        else undoButton.setEnabled(true);
//
//        if ( currentHistoryPosition < (parameterHistory.size() - 1) ) redoButton.setEnabled(true);
//        else redoButton.setEnabled(false);
//    }
//
//    // Put current values into model
//    void updateParameters() {
//        automaticDisplayUpdating = true;
//
//        checkButtons();
//        APBSParameters currentParameters = (APBSParameters) parameterHistory.get(currentHistoryPosition);
//        parameters.copyFrom(currentParameters);
//        
//        temperatureBox.setSelectedItem((new Double(parameters.temperature)).toString());
//        solventPBox.setSelectedItem((new Double(parameters.solventPermittivity)).toString());
//        moleculePBox.setSelectedItem((new Double(parameters.moleculePermittivity)).toString());
//        
//        automaticDisplayUpdating = false;        
//    }
//}
//

class ISIMLoadMoleculeDialog extends MoleculeAcquisitionMethodDialog {
    ISIMWrapper parentFrame;

    ISIMLoadMoleculeDialog(ISIMWrapper frame, ProgressDialog progressDialog) {
        super(frame, progressDialog);

        this.parentFrame = frame;
        setTitle("Choose Molecule (" + parentFrame.programName + ")");

        setLocationRelativeTo(parentFrame);
        this.setDefaultPdbId("1GRZ"); // Default to an RNA structure
    }
    static final long serialVersionUID = 01L;
    
    public void readStructureFromMoleculeCollection(MoleculeCollection molecules) {
        // Do nothing, we want files, not molecules, for this wrapper
        // This function should never even be called, if we override handleMoleculeURL properly
    }
    
    // Override handleMoleculeURL to manipulate files instead of loading molecules
    protected void handleMoleculeUrl(URL url) throws IOException {
        parentFrame.loadMacromoleculeTask.setStatus(DependentTask.ACTIVELY_UPDATING, "Loading molecule...");

        String loadingMessage = "Loading structure from " + url + "...";
        parentFrame.appendToLog("\n" + loadingMessage + "\n");
        
        CopyPdbFileProcess copyFileProcess = new CopyPdbFileProcess(url, fileLoadObservable);
        copyFileProcess.start();
        
        // And another process to monitor the download process
        ProgressDialog progressDialog = parentFrame.chooseMoleculeRow;
        
        ProgressManager progressManager;
        if (progressDialog == null)
            progressManager = 
                new ProgressManager(
                        copyFileProcess, 
                        this, 
                        loadingMessage);
        else 
            progressManager = new ProgressManager(copyFileProcess, progressDialog);        

        progressManager.start();        

    }

    boolean isPDBFile(String fileName) {
        // Determine file type from the name
        if (fileName == null) return false;
        
        String name = fileName.toLowerCase();
        if ( name.endsWith(".pdb") ) return true;
        if ( name.endsWith(".pdb1") ) return true;
        if ( name.endsWith(".pdb2") ) return true;
        if ( name.endsWith(".ent") ) return true;
        if ( name.endsWith(".brk") ) return true;

        if ( name.toLowerCase().endsWith(".pqr") ) return false;
        if ( name.toLowerCase().indexOf(".pqr") >= 0 ) return false;

        if ( name.toLowerCase().indexOf(".pdb") >= 0 ) return true;

        return false;
    }
    
    class CopyPdbFileProcess extends LoadPdbUrlProcess {

        CopyPdbFileProcess(URL url, SimpleObservable loadMoleculeObservable) {
            super(url, loadMoleculeObservable);
        }

        // Override loadMolecules to create a file instead of loading a molecule
        protected void loadMolecules() throws IOException, InterruptedException {
            InputStream inStream = getInputStream();
            
            // Determine file name
            String fileName = this.url.getFile();
            // strip off all but the file name (no path)
            int pathEnd = fileName.lastIndexOf("/");
            if (pathEnd >= 0) fileName = fileName.substring(pathEnd + 1);

            // Remove compression suffixes from file name
            fileName = fileName.replaceAll("\\.Z$", "");
            fileName = fileName.replaceAll("\\.gz$", "");
            
            // Create local file of molecular structure
            File outputFile = new File(parentFrame.workingDirectory, fileName);
            OutputStream outStream = new FileOutputStream(outputFile);

            StreamCopy.copy(inStream, outStream);
            
            // 2 - if PDB format, convert to PQR
            if (isPDBFile(fileName)) {
                parentFrame.convertPDBToPQR(outputFile);            
            }
            else {
                parentFrame.setPqrFile(outputFile);
            }
            
            reactivate();
            
        }
    }
    
}
