/*
 * This file is part of muCommander, http://www.mucommander.com
 *
 * muCommander 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 3 of the License, or
 * (at your option) any later version.
 *
 * muCommander 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, see <http://www.gnu.org/licenses/>.
 */

package com.mucommander.ui.macos;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.mucommander.commons.runtime.OsFamily;
import com.mucommander.commons.runtime.OsVersion;
import com.mucommander.process.AbstractProcess;
import com.mucommander.process.ProcessListener;
import com.mucommander.process.ProcessRunner;

/**
 * This class allows to run AppleScript code under Mac OS X, relying on the <code>osacript</code> command available
 * that comes with any install of Mac OS X. This command is used instead of the Cocoa-Java library which has been
 * deprecated by Apple.<br/>
 * Calls to {@link #execute(String, StringBuilder)} on any OS other than Mac OS X will always fail.
 *
 * <p>
 * <b>Important notes about character encoding</b>:
 * <ul>
 *   <li>AppleScript 1.10- (Mac OS X 10.4 or lower) expects <i>MacRoman</i> encoding, not <i>UTF-8</i>. <b>That
 *       means the script should only contain characters that are part of the MacRoman charset</b>; any character
 *       that cannot be expressed in MacRoman will not be propertly interpreted.<br/>
 *       The only way to pass Unicode text to a script is by reading it from a file.
 *       See <a href="http://www.satimage.fr/software/en/unicode_and_applescript.html">http://www.satimage.fr/software/en/unicode_and_applescript.html</a>
 *       for more information on how to do so.
 *   </li>
 *   <li>AppleScript 2.0+ (Mac OS X 10.5 and up) is fully Unicode-aware and will properly interpret any Unicode
 *       character: "AppleScript is now entirely Unicode-based. Comments and text constants in scripts may contain
 *       any Unicode characters, and all text processing is done in Unicode".<br/>
 *       See <a href="http://www.apple.com/applescript/features/unicode.html">http://www.apple.com/applescript/features/unicode.html</a>
 *       for more information.
 *   </li>
 * </ul>
 * </p>
 *
 * @author Maxence Bernard
 */
public class AppleScript {
	private static final Logger LOGGER = LoggerFactory.getLogger(AppleScript.class);
	
    /** The UTF-8 encoding */
    public final static String UTF8 = "UTF-8";

    /** The MacRoman encoding */
    public final static String MACROMAN = "MacRoman";


    /**
     * Executes the given AppleScript and returns <code>true</code> if it completed its execution normally, i.e. without
     * any error.
     * The script's output is accumulated in the given <code>StringBuilder</code>. If the script completed its execution
     * normally, the buffer will contain the script's standard output. If the script failed because of an error in it,
     * the buffer will contain details about the error.
     *
     * <p>If the caller is not interested in the script's output, a <code>null</code> value can be passed which will
     * speed the execution up a little.</p>
     *
     * @param appleScript the AppleScript to execute
     * @param outputBuffer the StringBuilder that will hold the script's output, <code>null</code> for no output
     * @return true if the script was successfully executed, false if the
     */
    public static boolean execute(String appleScript, StringBuilder outputBuffer) {
        // No point in going any further if the current OS is not Mac OS X
        if(!OsFamily.MAC_OS.isCurrent())
            return false;

        LOGGER.debug("Executing AppleScript: "+appleScript);

        // Use the 'osascript' command to execute the AppleScript. The '-s o' flag tells osascript to print errors to
        // stdout rather than stderr. The AppleScript is piped to the process instead of passing it as an argument
        // ('-e' flag), for better control over the encoding and to remove any limitations on the maximum script size.
        String tokens[] = new String[] {
            "osascript",
            "-s",
            "o",
        };

        OutputStreamWriter pout = null;
        try {
            // Execute the osascript command.
            AbstractProcess process = ProcessRunner.execute(tokens, outputBuffer==null?null:new ScriptOutputListener(outputBuffer, AppleScript.getScriptEncoding()));

            // Pipe the script to the osascript process.
            pout = new OutputStreamWriter(process.getOutputStream(), getScriptEncoding());
            pout.write(appleScript);
            pout.close();

            // Wait for the process to die
            int returnCode = process.waitFor();

            LOGGER.debug("osascript returned code="+returnCode+", output="+ outputBuffer);

            if(returnCode!=0) {
            	LOGGER.debug("osascript terminated abnormally");
                return false;
            }

            return true;
        }
        catch(Exception e) {        // IOException, InterruptedException
            // Shouldn't normally happen
        	LOGGER.debug("Unexcepted exception while executing AppleScript", e);

            try {
                if(pout!=null)
                    pout.close();
            }
            catch(IOException e1) {
                // Can't do much about it
            }

            return false;
        }
    }

    /**
     * Returns the encoding that AppleScript uses on the current runtime environment:
     * <ul>
     *   <li>{@link #UTF8} for AppleScript 2.0+ (macOS 10.5 and up)</li>
     *   <li>{@link #MACROMAN} for AppleScript 1.10- (macOS 10.4 or lower)</li>
     * </ul>
     *
     * If {@link #MACROMAN} is used, the scripts passed to {@link #execute(String, StringBuilder)} should not contain
     * characters that are not part of the <i>MacRoman</i> charset or they will not be properly interpreted.
     *
     * @return the encoding that AppleScript uses on the current runtime environment
     */
    public static String getScriptEncoding() {
        // - AppleScript 2.0+ (macOS 10.5 and up) is fully Unicode-aware and expects a script in UTF-8 encoding.
        // - AppleScript 1.3- (macOS 10.4 or lower) expects MacRoman encoding, not UTF-8.
        String encoding;
        if(OsVersion.MAC_OS_10_5.isCurrentOrHigher())
            encoding = UTF8;
        else
            encoding = MACROMAN;

        return encoding;
    }


    /**
     * This ProcessListener accumulates the output of the 'osascript' command and suppresses the trailing '\n' character
     * from the script's output.
     */
    private static class ScriptOutputListener implements ProcessListener {

        private StringBuilder outputBuffer;
        private String outputEncoding;

        private ScriptOutputListener(StringBuilder outputBuffer, String outputEncoding) {
            this.outputBuffer = outputBuffer;
            this.outputEncoding = outputEncoding;
        }

        ////////////////////////////////////
        // ProcessListener implementation //
        ////////////////////////////////////

        public void processOutput(byte[] buffer, int offset, int length) {
            try {
                outputBuffer.append(new String(buffer, offset, length, outputEncoding));
            }
            catch(UnsupportedEncodingException e) {
                // The encoding is necessarily supported
            }
        }

        public void processOutput(String s) {
        }

        public void processDied(int returnValue) {
            // Remove the trailing "\n" character that osascript returns.
            int len = outputBuffer.length();
            if(len>0 && outputBuffer.charAt(len-1)=='\n')
                outputBuffer.setLength(len-1);    
        }
    }


    // The following commented method executes an AppleScript using the deprecated Cocoa-Java library.
    // We're now using the 'osascript' command instead, but this method is kept for the record in case Apple one day
    // decides to un-deprecate the Cocoa-Java library.

//    /**
//     * Executes the given AppleScript and returns the script's output if it was successfully executed, <code>null</code>
//     * if the script couldn't be compiled or if an error occurred while executing it.
//     * An empty string <code>""</code> is returned if the script doesn't output anything.
//     *
//     * @param appleScript the AppleScript to compile and execute
//     * @return the script's output, null if an error occurred while compiling or executing the script
//     */
//    private static String executeAppleScript(String appleScript) {
//        AppLogger.finer("Executing AppleScript "+appleScript);
//
//        int pool = -1;
//
//        try {
//            // Quote from Apple Cocoa-Java doc:
//            // An autorelease pool is used to manage Foundation’s autorelease mechanism for Objective-C objects.
//            // NSAutoreleasePool provides Java applications access to autorelease pools. Typically it is not
//            // necessary for Java applications to use NSAutoreleasePools since Java manages garbage collection.
//            // However, some situations require an autorelease pool; for instance, if you start off a thread that
//            // calls Cocoa, there won’t be a top-level pool.
//            pool = NSAutoreleasePool.push();
//
//            NSMutableDictionary errorInfo = new NSMutableDictionary();
//            NSAppleEventDescriptor eventDescriptor = new NSAppleScript(appleScript).execute(errorInfo);
//            if(eventDescriptor==null) {
//                AppLogger.fine("Caught AppleScript error: "+errorInfo.objectForKey(NSAppleScript.AppleScriptErrorMessage));
//
//                return null;
//            }
//
//            String output = eventDescriptor.stringValue();  // Returns null if the script didn't output anything
//            AppLogger.finer("AppleScript output="+output);
//
//            return output==null?"":output;
//        }
//        catch(Error e) {
//            // Can happen if Cocoa-java is not in the classpath
//            AppLogger.fine("Unexcepted error while executing AppleScript (cocoa-java not available?)", e);
//
//            return null;
//        }
//        catch(Exception e) {
//            // Try block is not supposed to throw any exception, but this is low-level stuff so just to be safe
//            AppLogger.fine("Unexcepted exception while executing AppleScript", e);
//
//            return null;
//        }
//        finally {
//            if(pool!=-1)
//                NSAutoreleasePool.pop(pool);
//        }
//    }
}
