/*
 * File:     PraatConnection.java
 * Project:  MPI Linguistic Application
 * Date:     03 April 2006
 *
 * Copyright (C) 2001-2006  Max Planck Institute for Psycholinguistics
 *
 * 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; either version 2 of the License, or
 * (at your option) any later version.
 *
 * 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
 */

package mpi.eudico.client.annotator.viewer;

import mpi.eudico.client.annotator.Constants;
import mpi.eudico.client.annotator.ElanLocale;

import mpi.library.util.LogUtil;

import java.io.BufferedInputStream;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

import java.net.URL;

import java.util.Hashtable;
import java.util.logging.Logger;

import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;


/**
 * A class to start the Praat executable and open the .wav file that  has been
 * loaded into Elan.<br>
 * Opening a file and (possibly) selecting a part from that file is a two step
 * process. First we have to make sure that Praat is/has been started, next we
 * have to use the sendpraat executable to let Praat execute a script with the
 * right arguments. At least on Windows it is not possible to start Praat
 * with  the script etc. as arguments. The Windows praatcon.exe Praat console
 * executable  can start Praat with a script etc., but it can not load a 'long
 * sound' and create  a view and editor for it.<br>
 * Since elan only loads sound files that are on a local file system it is
 * save to expect that a provided url is on the local file system.
 *
 * @author Han Sloetjes
 * @version jul 2004
 */
public class PraatConnection {
    /** constant for the name of the praat sript */
    private static final String PRAAT_SCRIPT = "openpraat.praat";
    private static String scriptFileName;

    /** constant for the Praat application path property */
    private static final String PRAAT_APP = "Praat app";

    /** constant for the sendpraat application path property */
    private static final String SENDPRAAT_APP = "Sendpraat app";

    /** Holds value of property DOCUMENT ME! */
    private static final String PRAAT_PREFS_FILE = Constants.ELAN_DATA_DIR +
        Constants.FILESEPARATOR + "praat.pfs";

    /**
     * a non-thread-safe way of temporarily storing the result of a check on
     * the  running state of Praat (Unix systems)
     */
    static boolean isPraatRunning = false;

    /** the logger */
    private static final Logger LOG = Logger.getLogger(PraatConnection.class.getName());
    private static Hashtable preferences;

    /**
     * Creates a new PraatConnection instance
     */
    private PraatConnection() {
    }

    /**
     * Asynchronously opens a file in Praat.
     *
     * @param fileName the media file
     * @param begintime the selection begintime
     * @param endtime the selection end time
     */
    public static void openInPraat(final String fileName, final long begintime,
        final long endtime) {
        new Thread() {
                public void run() {
                    if (!checkScript()) {
                        JOptionPane.showMessageDialog(new JFrame(),
                            ElanLocale.getString(
                                "PraatConnection.Message.NoScript"),
                            ElanLocale.getString("Message.Warning"),
                            JOptionPane.WARNING_MESSAGE);
                        LOG.warning("Praat script could not be created");

                        return;
                    }

                    if (System.getProperty("os.name").toLowerCase().indexOf("windows") > -1) {
                        openWindowsPraat(fileName, begintime, endtime);
                    } else {
                        openOtherPraat(fileName, begintime, endtime);
                    }
                }
            }.start();
    }

    /**
     * For windows flavors some processing of path names should be done etc.
     *
     * @param fileName the media file
     * @param begintime the begintime of the selection
     * @param endtime the end time of the selection
     */
    private static void openWindowsPraat(String fileName, long begintime,
        long endtime) {
        if (fileName != null) {
            // Praat can handle files on samba shares too
            if (fileName.startsWith("///")) {
                fileName = fileName.substring(3);
            }

            fileName = fileName.replace('/', '\\');

            // sendpraat has problems with filepaths containing spaces
            // single or double quotes don't seem to help
            // if the file name itself contains spaces there seems to be no solution
            if (fileName.indexOf(' ') > 0) {
                fileName = spacelessWindowsPath(fileName);
            }

            //System.out.println("file: " + fileName);
        } else {
            LOG.warning("Praat: media file is null");

            return;
        }

        // first make sure praat is running
        String praatExe = getPreference(PRAAT_APP);

        if ((praatExe == null) || (praatExe.length() == 0)) {
            praatExe = "Praat.exe";
        }

        String[] praatCom = new String[] { praatExe };

        try {
            Runtime.getRuntime().exec(praatCom);

            // give praat a moment to start
            try {
                Thread.sleep(2000);
            } catch (InterruptedException ie) {
                LOG.info(LogUtil.formatStackTrace(ie));
            }

            //
        } catch (SecurityException se) {
            JOptionPane.showMessageDialog(new JFrame(),
                ElanLocale.getString("PraatConnection.Message.Security"),
                ElanLocale.getString("Message.Warning"),
                JOptionPane.WARNING_MESSAGE);
            LOG.warning(LogUtil.formatStackTrace(se));

            return;
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));

            // not found, prompt
            String path = locatePraat();

            if (path == null) {
                return;
            } else {
                // retry
                openWindowsPraat(fileName, begintime, endtime);

                return;
            }
        }

        // next execute sendpraat
        // (on windows we cannot use praatcon.exe for this task)
        String sendpraatExe = getPreference(SENDPRAAT_APP);

        if ((sendpraatExe == null) || (sendpraatExe.length() == 0)) {
            sendpraatExe = "sendpraat.exe";
        }

        String executeCom = "execute " + scriptFileName + " " + fileName + " " +
            String.valueOf(begintime) + " " + String.valueOf(endtime);

        String[] sendpraatCom = new String[3];
        sendpraatCom[0] = sendpraatExe;
        sendpraatCom[1] = "Praat";
        sendpraatCom[2] = executeCom;

        try {
            Runtime.getRuntime().exec(sendpraatCom);
        } catch (SecurityException se) {
            JOptionPane.showMessageDialog(new JFrame(),
                ElanLocale.getString("PraatConnection.Message.Security"),
                ElanLocale.getString("Message.Warning"),
                JOptionPane.WARNING_MESSAGE);
            LOG.warning(LogUtil.formatStackTrace(se));

            return;
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));

            // not found, prompt
            String path = locateSendpraat();

            if (path == null) {
                return;
            } else {
                // retry
                openWindowsPraat(fileName, begintime, endtime);
            }
        }
    }

    /**
     * Open Praat on non-windows systems; MacOS X, Unix, Linux. <br>
     * Note: on Mac OS X every call to runtime.exec to start Praat opens a new
     * instance of Praat: it is not detected that it is already open (this is
     * a result  of the fact that you can not use the Praat.app "folder" to
     * start Praat).  Maybe we should not try to start Praat but let the user
     * be responsible to  start Praat.
     *
     * @param fileName the media file
     * @param begintime the begintime of the selection
     * @param endtime the end time of the selection
     */
    private static void openOtherPraat(String fileName, long begintime,
        long endtime) {
        if (fileName != null) {
            // Praat can handle files on samba shares too
            // on Mac: when the url starts with 3 slashes, remove 2 slashes
            // TO DO: this needs testing on other platforms
            if (fileName.startsWith("///")) {
                fileName = fileName.substring(2);
            }
        } else {
            LOG.warning("Praat: media file is null");

            return;
        }

        // first make sure praat is running
        String praatExe = getPreference(PRAAT_APP);

        if ((praatExe == null) || (praatExe.length() == 0)) {
            praatExe = "praat";
        }

        // check whether praat is running already; this is not a thread safe way to check
        isPraatRunning = false;
        checkUnixPraatProcess();

        try {
            Thread.sleep(700);
        } catch (InterruptedException ie) {
        }

        if (!isPraatRunning) {
            String[] praatCom = new String[] { praatExe };

            try {
                Runtime.getRuntime().exec(praatCom);

                // give praat a moment to start
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException ie) {
                    LOG.info(LogUtil.formatStackTrace(ie));
                }
            } catch (SecurityException se) {
                JOptionPane.showMessageDialog(new JFrame(),
                    ElanLocale.getString("PraatConnection.Message.Security"),
                    ElanLocale.getString("Message.Warning"),
                    JOptionPane.WARNING_MESSAGE);
                LOG.warning(LogUtil.formatStackTrace(se));

                return;
            } catch (IOException ioe) {
                LOG.warning(LogUtil.formatStackTrace(ioe));

                // not found, prompt
                String path = locatePraat();

                if (path == null) {
                    return;
                } else {
                    // retry
                    openOtherPraat(fileName, begintime, endtime);

                    return;
                }
            }
        }

        // next execute sendpraat
        String sendpraatExe = getPreference(SENDPRAAT_APP);

        if ((sendpraatExe == null) || (sendpraatExe.length() == 0)) {
            sendpraatExe = "sendpraat";
        }

        // test this om other platforms...
        String executeCom = "execute " + scriptFileName + " " + fileName + " " +
            String.valueOf(begintime) + " " + String.valueOf(endtime);
        String[] sendpraatCom = new String[3];
        sendpraatCom[0] = sendpraatExe;
        sendpraatCom[1] = "praat";
        sendpraatCom[2] = executeCom;

        try {
            Runtime.getRuntime().exec(sendpraatCom);
        } catch (SecurityException se) {
            JOptionPane.showMessageDialog(new JFrame(),
                ElanLocale.getString("PraatConnection.Message.Security"),
                ElanLocale.getString("Message.Warning"),
                JOptionPane.WARNING_MESSAGE);
            LOG.warning(LogUtil.formatStackTrace(se));

            return;
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));

            // not found, prompt
            String path = locateSendpraat();

            if (path == null) {
                return;
            } else {
                // retry
                openOtherPraat(fileName, begintime, endtime);
            }
        }
    }

    /**
     * Checks whether or not the praatscript file already exists.  When not it
     * is created in the elan directory in the user's home directory.
     *
     * @return true if the file already existed or could be created, false
     *         otherwise
     */
    private static boolean checkScript() {
        if (!checkHome()) {
            return false;
        }

        scriptFileName = Constants.ELAN_DATA_DIR + Constants.FILESEPARATOR +
            PRAAT_SCRIPT;

        if (System.getProperty("os.name").toLowerCase().indexOf("windows") > -1) {
            // Praat on Windows does not like spaces in the path 
            if (scriptFileName.indexOf(' ') > -1) {
                String dir = System.getProperty("java.io.tmpdir");

                if (dir != null) {
                    scriptFileName = dir + Constants.FILESEPARATOR +
                        PRAAT_SCRIPT;
                }

                // or
                // scriptFileName = spacelessWindowsPath(scriptFileName);
            }
        }

        File file = new File(scriptFileName);

        if (file.exists()) {
            return true;
        } else {
            // first try to copy the file from the .jar
            if (copyScriptFromJar(file)) {
                return true;
            } else {
                // fallback: create the file programmatically
                return createScriptFile(file);
            }
        }
    }

    /**
     * Checks whether the elan home dir exists. When not create it.
     *
     * @return true if the dir already existed or could be created, false
     *         otherwise
     */
    private static boolean checkHome() {
        File dataDir = new File(Constants.ELAN_DATA_DIR);

        if (!dataDir.exists()) {
            try {
                dataDir.mkdir();
            } catch (Exception e) {
                LOG.warning(LogUtil.formatStackTrace(e));

                return false;
            }
        }

        return true;
    }

    /**
     * Tries to copy the script file from the jar.
     *
     * @param copyFile the copy of the script
     *
     * @return true if the operation succeeded, false otherwise
     */
    private static boolean copyScriptFromJar(File copyFile) {
        BufferedInputStream inStream;
        FileOutputStream out;

        try {
            copyFile.createNewFile();

            URL scriptSrc = PraatConnection.class.getResource(
                    "/mpi/eudico/client/annotator/resources/openpraat.praat");

            if (scriptSrc == null) {
                LOG.warning("No script source file found");

                return false;
            }

            inStream = new BufferedInputStream(scriptSrc.openStream());

            byte[] buf = new byte[512];
            int n;
            out = new FileOutputStream(copyFile);

            while ((n = inStream.read(buf)) > -1) {
                out.write(buf, 0, n);
            }

            out.flush();
            out.close();
            inStream.close();

            return true;
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));

            return false;
        }
    }

    /**
     * Writes the script file to the elan home dir.
     *
     * @param scriptFile the file to write to
     *
     * @return true if everything went allright, false otherwise
     */
    private static boolean createScriptFile(File scriptFile) {
        try {
            if (!scriptFile.exists()) {
                scriptFile.createNewFile();
            }

            String contents = createScriptContents();
            FileWriter writer = new FileWriter(scriptFile);
            writer.write(contents);
            writer.close();
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));

            return false;
        }

        return true;
    }

    /**
     * Create the script. Temp. could be copied from the jar...
     *
     * @return the scriptcontents
     */
    private static String createScriptContents() {
        StringBuffer scriptBuffer = new StringBuffer();
        scriptBuffer.append("form Segment info\n");
        scriptBuffer.append("\ttext Filename \"\"\n");
        scriptBuffer.append("\tpositive Start 0\n");
        scriptBuffer.append("\tpositive End 10\n");
        scriptBuffer.append("endform\n");
        scriptBuffer.append("len = length( filename$)\n");
        scriptBuffer.append("dot = rindex (filename$, \".\")\n");
        scriptBuffer.append("slash = rindex (filename$, \"/\")\n");
        scriptBuffer.append("if slash == 0\n");
        scriptBuffer.append("\tslash = rindex (filename$, \"\\\")\n");
        scriptBuffer.append("endif\n");
        scriptBuffer.append(
            "stem$ = mid$ (filename$ , slash+1, dot - slash - 1 )\n");
        scriptBuffer.append("Open long sound file... 'filename$'\n");
        scriptBuffer.append("s = start / 1000\n");
        scriptBuffer.append("en = end / 1000\n");
        scriptBuffer.append("View\n");
        scriptBuffer.append("editor LongSound 'stem$'\n");
        scriptBuffer.append("\tSelect... 's' 'en'\n");
        scriptBuffer.append("\tZoom to selection\n");
        scriptBuffer.append("endeditor");

        return scriptBuffer.toString();
    }

    /**
     * Promt the user for the location of the Praat executable.
     *
     * @return the path to the Praat executable
     */
    private static String locatePraat() {
        String praatPath = null;
        JFileChooser chooser = new JFileChooser();
        chooser.setDialogTitle(ElanLocale.getString(
                "PraatConnection.LocateDialog.Title1"));
        chooser.setApproveButtonText(ElanLocale.getString(
                "PraatConnection.LocateDialog.Select"));

        int option = chooser.showOpenDialog(new JFrame());

        if (option == JFileChooser.CANCEL_OPTION) {
            // cannot remove a preference yet
            setPreference(PRAAT_APP, "");
        } else {
            File path = chooser.getSelectedFile();
            praatPath = path.getAbsolutePath();

            // Mac specific addition
            if (path.isDirectory() && praatPath.endsWith(".app")) {
                // append path to the actual executable
                praatPath += "/Contents/MacOS/Praat";
            }

            // trust the user, cannot possibly check the value
            setPreference(PRAAT_APP, praatPath);
        }

        return praatPath;
    }

    /**
     * Promt the user for the location of the sendpraat executable.
     *
     * @return the path to the sendpraat executable
     */
    private static String locateSendpraat() {
        String sendpraatPath = null;
        JFileChooser chooser = new JFileChooser();
        chooser.setDialogTitle(ElanLocale.getString(
                "PraatConnection.LocateDialog.Title2"));
        chooser.setApproveButtonText(ElanLocale.getString(
                "PraatConnection.LocateDialog.Select"));

        int option = chooser.showOpenDialog(new JFrame());

        if (option == JFileChooser.CANCEL_OPTION) {
            // cannot remove a preference yet
            setPreference(SENDPRAAT_APP, "");
        } else {
            File path = chooser.getSelectedFile();
            sendpraatPath = path.getAbsolutePath();

            // trust the user, cannot possibly check the value
            setPreference(SENDPRAAT_APP, sendpraatPath);
        }

        return sendpraatPath;
    }

    /**
     * Returns the pref for the specified key like the Praat and sendpraat
     * executable paths from the praat pref file.
     *
     * @param key the key for the pref
     *
     * @return the pref
     */
    private static String getPreference(String key) {
        if (key == null) {
            return null;
        }

        if (preferences == null) {
            preferences = loadPreferences();
        }

        return (String) preferences.get(key);
    }

    /**
     * Loads the praat preferences from the praat pref file.
     *
     * @return a hashtable with the pref mappings
     */
    private static Hashtable loadPreferences() {
        Hashtable hashtable = new Hashtable();
        ;

        File inFile = new File(PRAAT_PREFS_FILE);

        try {
            //if (inFile.exists()) {
            FileInputStream inStream = new FileInputStream(inFile);
            ObjectInputStream objectIn = new ObjectInputStream(inStream);
            hashtable = (Hashtable) objectIn.readObject();
            objectIn.close();
            inStream.close();

            //}
        } catch (FileNotFoundException fnfe) {
            LOG.warning("Could not load Praat preferences");
            LOG.warning(LogUtil.formatStackTrace(fnfe));
        } catch (IOException ioe) {
            LOG.warning("Could not load Praat preferences");
            LOG.warning(LogUtil.formatStackTrace(ioe));
        } catch (ClassNotFoundException cnfe) {
            LOG.warning("Could not load Praat preferences");
            LOG.warning(LogUtil.formatStackTrace(cnfe));
        }

        return hashtable;
    }

    /**
     * Sets the specified pref to the new value.
     *
     * @param key the pref key
     * @param value the pref value
     */
    private static void setPreference(String key, String value) {
        if ((key == null) || (value == null)) {
            return;
        }

        if (preferences == null) {
            preferences = new Hashtable();
        }

        preferences.put(key, value);

        savePreferences();
    }

    /**
     * Wrties the praat preferences file to disc.
     */
    private static void savePreferences() {
        if (preferences == null) {
            return;
        }

        try {
            FileOutputStream outStream = new FileOutputStream(PRAAT_PREFS_FILE);
            ObjectOutputStream objectOut = new ObjectOutputStream(outStream);
            objectOut.writeObject(preferences);
            objectOut.close();
            outStream.close();
        } catch (FileNotFoundException fnfe) {
            LOG.warning("Could not save Praat preferences");
            LOG.warning(LogUtil.formatStackTrace(fnfe));
        } catch (IOException ioe) {
            LOG.warning("Could not save Praat preferences");
            LOG.warning(LogUtil.formatStackTrace(ioe));
        }
    }

    /**
     * Tries to check whether or not Praat is already running using the  Unix
     * utility <code>top</code>.
     */
    private static void checkUnixPraatProcess() {
        try {
            final Process p = Runtime.getRuntime().exec(new String[] {
                        "top", "-l1"
                    });

            new Thread(new Runnable() {
                    public void run() {
                        try {
                            InputStreamReader isr = new InputStreamReader(p.getInputStream());
                            BufferedReader br = new BufferedReader(isr);
                            String line = null;

                            while ((line = br.readLine()) != null) {
                                if (line.indexOf(" Praat ") > 0) {
                                    br.close();
                                    isPraatRunning = true;

                                    break;
                                }
                            }
                        } catch (IOException ioe) {
                            LOG.warning(LogUtil.formatStackTrace(ioe));
                        }
                    }
                }).start();
        } catch (SecurityException sex) {
            LOG.warning(LogUtil.formatStackTrace(sex));
        } catch (IOException ioe) {
            LOG.warning(LogUtil.formatStackTrace(ioe));
        }
    }

    /**
     * Converts a file path containing spaces in directory names to a path with
     * "DIRECT~x" elements. The filename itself will not be converted.
     *
     * @param fileName the file path with spaces
     *
     * @return a converted path
     */
    private static String spacelessWindowsPath(String fileName) {
        if (fileName == null) {
            return null;
        }

        //String path = fileName;
        int firstSpace = fileName.indexOf(' ');

        if (firstSpace < 0) {
            return fileName;
        }

        int prevSep = fileName.lastIndexOf(File.separator, firstSpace);
        int nextSep = fileName.indexOf(File.separator, firstSpace);
        String fileOrDirName = null;

        if (nextSep > 0) {
            fileOrDirName = fileName.substring(prevSep + 1, nextSep);
        } else {
            //fileOrDirName = fileName.substring(prevSep + 1);
            return fileName;
        }

        // if an tilde already has been added, prevSep + 1 does not include the file separator ?? 
        try {
            File dir = new File(fileName.substring(0, prevSep + 1));

            if (!dir.isDirectory()) {
                return fileName;
            }

            String start = fileOrDirName.substring(0, fileOrDirName.indexOf(' '));
            String[] fNames = dir.list();

            int sufNum = 0;

            for (int i = 0; i < fNames.length; i++) {
                if (fNames[i].startsWith(fileOrDirName)) {
                    sufNum++;

                    if (fNames[i].equals(fileOrDirName)) {
                        break;
                    }
                }
            }

            StringBuffer pathBuf = new StringBuffer(dir.getPath());

            // see comment above
            if (pathBuf.charAt(pathBuf.length() - 1) != File.separatorChar) {
                pathBuf.append(File.separator);
            }

            if (start.length() <= 6) {
                String tmp = fileOrDirName.replaceAll(" ", "");

                if (tmp.length() <= 6) {
                    pathBuf.append(tmp.toUpperCase()).append("~").append(sufNum);
                } else {
                    pathBuf.append(tmp.substring(0, 6).toUpperCase()).append("~")
                           .append(sufNum);
                }
            } else {
                pathBuf.append(start.substring(0, 6).toUpperCase()).append("~")
                       .append(sufNum);
            }

            if (nextSep > 0) {
                pathBuf.append(fileName.substring(nextSep));
            }

            if (pathBuf.indexOf(" ") < 0) {
                return pathBuf.toString();
            } else {
                return spacelessWindowsPath(pathBuf.toString());
            }
        } catch (Exception ex) {
            LOG.warning("Invalid directory: " + ex.getMessage());

            return fileName;
        }
    }
}
