/* Copyright (c) 2005 Stanford University and Christopher Bruns
 * 
 * Permission is hereby granted, free of charge, to any person obtaining
 * a copy of this software and associated documentation files (the
 * "Software"), to deal in the Software without restriction, including 
 * without limitation the rights to use, copy, modify, merge, publish, 
 * distribute, sublicense, and/or sell copies of the Software, and to
 * permit persons to whom the Software is furnished to do so, subject
 * to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included 
 * in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
 * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

/*
 * Created on Dec 7, 2005
 * Original author: Christopher Bruns
 */
package org.simtk.isimsu;

import java.io.*;
import java.text.*;
import java.util.*;
import org.simtk.gui.*;

public class RunISIMThread extends JarDefinedExecutableThread 
implements MonitoredProcess
{
    APBSParameters apbsParameters;
    ISIMStepParameters stepParameters;
    ISIMWrapper isimWrapper;

    // Constructor
    RunISIMThread(ISIMStepParameters s, APBSParameters p, ISIMWrapper isimWrapper) {
        stepParameters = s;
        apbsParameters = p;
        this.isimWrapper = isimWrapper;
    }

    public void run() {
        
        // Make sure another version of this task is not already running
        ISIMTask task = stepParameters.task;
        
        // Don't do anything if it appears that someone else is already doing this task
        if (task.isRunning()) return;        
        
        // Unpack executable
        isimWrapper.appendToLog("Unpacking ISIM executable...\n");
        Vector initialArgs = new Vector();
        try {
            initialArgs = unpackExecutable("dir_isim", 
                    "isim_executable_name",
                    stepParameters.programDirectory);
        } catch (IOException exc) {
            reportFailure(exc, "error unpacking ISIM executable");
            return;
        }
        
        File isimExecutableFile = new File(stepParameters.programDirectory, (String) initialArgs.elementAt(0));

        // Create input data file
        
        File workDir = stepParameters.workingDirectory;
        
        File isimInputFile = new File(workDir, "isim.in");

        String isimInputString = createISIMInput(apbsParameters);

        isimWrapper.appendToLog("ISIM input file:\n");
        isimWrapper.appendToLog("\n\n********************\n\n");
        isimWrapper.appendToLog(isimInputString);
        isimWrapper.appendToLog("\n\n******************\n\n");        
        
        task.setStatus(DependentTask.ACTIVELY_UPDATING, "running ISIM...");
        
        try {
            BufferedWriter apbsInWriter = new BufferedWriter(new FileWriter(isimInputFile));
            apbsInWriter.write(isimInputString);
            apbsInWriter.close();
            
            
            // System.out.println(isimInputString);
        } catch (IOException exc) {
            exc.printStackTrace();
            task.setStatus(DependentTask.FAILED, "I/O error creating ISIM input file");
            return;
        }
        
        isimWrapper.appendToLog("Creating ISIM excess potential files...\n");

        // Create input ion files
        // SaltCondition saltCondition = apbsParameters.getIons();

        // Need a SaltConditionRange here
        SaltConditionRange saltConditionRange = apbsParameters.getIons().getParentRange();
        
        List<IonConcentration> sortedConcentrations = 
            new Vector<IonConcentration>(apbsParameters.getIons());
        Collections.sort(sortedConcentrations);

        // TODO sort ions
        
        for (IonConcentration ion : sortedConcentrations) {
            String excessPotentialFileName = ion.getIonId() + "_EXCESS";            
            
            // Create the excess potential file for this one ion
            File excessPotentialFile = new File(stepParameters.workingDirectory, excessPotentialFileName);

            try {
                BufferedWriter out = new BufferedWriter(new FileWriter(excessPotentialFile));
                
                // Header row
                out.write("#  "); // Header line starts like a comment
                
                for (ConstIonType titleSpecies : sortedConcentrations) {
                    out.write(titleSpecies.getIonId());
                    out.write("        "); // Column separator
                }
                out.write("        kcal/mol\n"); // end of header line
                
                // Concentration lines
                List<SaltCondition> sortedConditions = new Vector<SaltCondition>(saltConditionRange);
                Collections.sort(sortedConditions);
                
                for (SaltCondition saltConditionForExcessFile : sortedConditions) {
                    // TODO might need to make sure these are sorted if they are not just read from an "official" excess file

                    float excessPotential = Float.NaN;

                    for (IonConcentration ionConcentration : saltConditionForExcessFile) {
                        // Take excess chemical potential from official range structure, not from "ion"
                        if (ionConcentration.getIonSpecies().equals(ion.getIonSpecies())) {
                            excessPotential = ionConcentration.getExcessChemicalPotential();
                        }

                        // TODO might need to sort these also in a consistent ordering

                        out.write("  " + ionConcentration.getConcentration());
                        out.write("      "); // column separator
                    }
                    
                    // float excessPotential = ion.getExcessChemicalPotential();
                    // Final column is chemical potential of main ion
                    out.write("  " + excessPotential + "\n");
                }

                out.close();

            } catch (IOException exc) {
                reportFailure(exc, "error creating excess potential file " + excessPotentialFileName);
                // TODO
            }
            
        }
        
        // Run the program
        String isimCommand = isimExecutableFile.getAbsolutePath();

        String isimArg1 = isimInputFile.getName();
        String[] isimArgs = {isimCommand, isimArg1};
        
        isimWrapper.appendToLog("Running ISIM...\n");

        // Process isimProcess = Runtime.getRuntime().exec(isimArgs, null, tempDir);
        ExternalProcessManager isimProcess = new ExternalProcessManager(isimArgs, workDir);
        long startSeconds = new Date().getTime() / 1000;
        isimProcess.start();

        // Wait for process to end
        try {
            isimProcess.join();

            // Test whether the process worked
            if (isimProcess.exception != null) reportFailure(isimProcess.exception, "");
            else {
                // This is the only point of success
                // Report the time taken
                long finishSeconds = new Date().getTime() / 1000;
                long elapsedSeconds = finishSeconds - startSeconds;

                long elapsedMinutes = elapsedSeconds / 60;
                elapsedSeconds = elapsedSeconds % 60;

                long elapsedHours = elapsedMinutes / 60;
                elapsedMinutes = elapsedMinutes % 60;

                NumberFormat twoDigits = new DecimalFormat();
                twoDigits.setMinimumIntegerDigits(2);
                twoDigits.setMaximumIntegerDigits(2);
                twoDigits.setMaximumFractionDigits(0);
                
                String elapsedTime = 
                    "" + twoDigits.format(elapsedHours) + 
                    ":" + twoDigits.format(elapsedMinutes) + 
                    ":" + twoDigits.format(elapsedSeconds);

                task.setStatus(DependentTask.CURRENT_AND_COMPLETE, elapsedTime);
                isimWrapper.appendToLog("ISIM Process succeeded.\n");
            }
        }
        catch (InterruptedException exc) {
            reportFailure(exc, "ISIM process interrupted");
        }
        
        logProcessOutput(isimProcess);
        
        // Also put results of isim.log and isim.err into the log window
        // TODO - ensure that the file name case is correct on Mac, Linux, and Windows

        File isimErrFile = new File(isimWrapper.workingDirectory, "isim.ERR");
        isimWrapper.appendToLog("Contents of ISIM.Err file:\n");
        try {
            BufferedReader reader = new BufferedReader(new FileReader(isimErrFile));
            String lineString = reader.readLine();
            while (lineString != null) {
                isimWrapper.appendToLog(lineString + "\n");
                lineString = reader.readLine();
            }
        } catch (IOException exc) {
            reportFailure(exc, "Problem reading ISIM Err file " + isimErrFile.getName());
        }        

        File isimLogFile = new File(isimWrapper.workingDirectory, "isim.LOG");
        isimWrapper.appendToLog("Contents of ISIM.LOG file:\n");
        try {
            BufferedReader reader = new BufferedReader(new FileReader(isimLogFile));
            String lineString = reader.readLine();
            while (lineString != null) {
                isimWrapper.appendToLog(lineString + "\n");
                lineString = reader.readLine();
            }
        } catch (IOException exc) {
            reportFailure(exc, "Problem reading ISIM log file " + isimLogFile.getName());
        }        
        
        return;
        
    }
    
    private void logProcessOutput(ExternalProcessManager process) {
        isimWrapper.appendToLog("Standard error stream: \n\n");
        isimWrapper.appendToLog("************************************ \n\n");
        isimWrapper.appendToLog(process.getStdErr());
        isimWrapper.appendToLog("\n\n************************************ \n\n");
        
        isimWrapper.appendToLog("Standard output stream: \n\n");
        isimWrapper.appendToLog("************************************ \n\n");
        isimWrapper.appendToLog(process.getStdOut());
        isimWrapper.appendToLog("\n\n************************************ \n\n");    
    }
    
    private String createISIMInput(APBSParameters p) {
        String answer = "";

        String pqrFileName = apbsParameters.getPqrInputFile().getName();
        String moleculePotentialFileName = apbsParameters.getPotentialOutputFile().getName();

        // TODO - review which values may need to be modified
        
        // Determine longest grid axis used in APBS computation
        double minGridLength = Math.min(p.getGridLength()[0], p.getGridLength()[1]);
        minGridLength = Math.min(minGridLength, p.getGridLength()[2]);

        double maxGridLength = Math.max(p.getGridLength()[0], p.getGridLength()[1]);
        maxGridLength = Math.max(maxGridLength, p.getGridLength()[2]);
        
        // double sphereRadius = maxGridLength * 0.5 * 1.10; // Half of max grid length plus a little slop
        // isim.err file accumulates off-grid errors if the sphere extends past the apbs rectangular solid
        double sphereRadius = minGridLength / 2.0; // Half of max grid length plus a little slop
        
        // TODO equilibration versus total steps
        int equilibrationStepCount = 100;
        int runningStepCount = 500;
        int neverHappensStepCount = equilibrationStepCount + runningStepCount + 1000;
        
        // General section
        answer +=
            "#GENERAL\n" +
            "\n" +
            "FORM                          S\n" + // S -> sphere
            "RADIUS                       " + sphereRadius + "  \n" + 
            "LENGTH                        180.0   /*redundant, since 'S' is selected*/\n" +
            "TEMPERATURE                  " + p.getTemperature() + "\n" +
            "MEDIUM_PERMITTIVITY          " + p.getSolventPermittivity() + "\n" +
            "GENERAL_MODE                  N\n" + // N -> not parameterization mode
            "\n" +
            "#ENDGENERAL\n\n";

        // Technical section
        answer +=
            "#TECHNICAL\n" +
            "\n" +
            "TOTAL_STEPS                   " + (equilibrationStepCount + runningStepCount) + "\n" +
            "MINIMIZATION_STEPS            35\n" +
            "CREATION_DESTRUCTION_CYCLES   10    \n" +
            "SHUFFLING_STEPS               10\n" +
            "SHUFFLING_MODE                N\n" +  // N -> one particle at a time
            "SO_MANY_ARE_SOME              40      /*redundant, since 'N' is selected*/\n" +
            "MAXIMUM_DISPLACEMENT          2.0\n" +
            "PARAMETRIZATION_MINIMUM       50\n" +
            "\n" +
            "#ENDTECHNICAL\n\n";
            
        // Analysis section
        answer +=
            "#ANALYSIS\n" +
            "\n" +
            "EQUILIBRATION_STEPS           " + equilibrationStepCount + "\n" +
            "GRID_RESOLUTION               60           \n" +
            "COORDINATES_OUTPUT            " + neverHappensStepCount + "\n" +
            "NUMBER_DENSITIES_OUTPUT       " + neverHappensStepCount + "\n" +
            "CHARGE_DENSITY_OUTPUT         " + neverHappensStepCount + "\n" +
            "POTENTIAL_OUTPUT              " + neverHappensStepCount + "\n" +
            "PAIR_CORRELATION_OUTPUT       " + neverHappensStepCount + "\n" +
            "POTENTIAL_FREQUENCY           " + neverHappensStepCount + "\n" +
            "PAIR_CORRELATION_FREQUENCY    " + neverHappensStepCount + "\n" +  
            "PAIR_CORRELATION_RESOLUTION   0.3\n" +
            "PAIR_CORRELATION_SHELLS       5\n" +
            "\n" +
            "#ENDANALYSIS\n\n";
        
        // Ion section
        // TODO if ion section is empty, there is an error
        answer += 
            "#IONS\n" +
            "\n" +
            
            apbsParameters.getIons().toInputText() +

            "\n" +
            "#ENDIONS\n\n";

        // Macromolecule section
        answer += 
            "#MACROMOLECULE\n" +
            "\n" +
            "TYPE_ID                               P    \n" +
            "STERIC_MODE                           S    \n" +
            "BORN_ION_CHARGE                       30.0                /*redundant, since 'P' is selected*/\n" +
            "BORN_ION_RADIUS                       10.0                /*redundant, since 'P' is selected*/\n" +

            "PQR_FILENAME                          " +
            pqrFileName + "\n" +
            
            // File names
            "PQR_DX_POTENTIAL_FILENAME             " + moleculePotentialFileName + "\n" +

            // TODO - incorporate  reference potential, if desired
            // "PQR_DX_REFERENCE_POTENTIAL_FILENAME   yetanothername.dx\n" +

            "PQR_GRID_RESOLUTION                   60\n" +
            "PQR_OUTSIDE_MESH_MODE                 1\n" +
            "\n" +
            "#ENDMACROMOLECULE\n\n";

        
        return answer;
    }

    void reportFailure(Exception exc, String msg) {
        exc.printStackTrace();
        stepParameters.task.setStatus(DependentTask.FAILED, msg);
        isimWrapper.appendToLog("ISIM process failed: " + msg + "\n");;
        return;        
    }
}
