package org.gudy.azureus2.platform.macosx.access.cocoa;

/*
 * Created on 27-Mar-2005
 * Created by James Yeh
 * Copyright (C) Azureus Software, Inc, All Rights Reserved.
 *
 * 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.
 *
 */

import org.gudy.azureus2.core3.logging.*;
import org.gudy.azureus2.core3.util.AEMonitor;
import org.gudy.azureus2.core3.util.AERunnable;
import org.gudy.azureus2.core3.util.AEThread;
import org.gudy.azureus2.core3.util.Debug;
import org.gudy.azureus2.platform.macosx.NativeInvocationBridge;

import java.io.File;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.text.MessageFormat;

/**
 * NOTE: This is only used for OSX 10.4 and non-up-to-date 10.5,10.6. 
 * OSX 10.5 Update 6 and OSX 10.6 Update 1 provides revealInFinder and moveToTrash
 * functionality.
 * <p>
 * <p>Performs PlatformManager tasks using Cocoa-Java (FoundationKit only)</p>
 * <p>For now, operations are performed using NSAppleScript, rather than using NSWorkspace.
 * This is still significantly faster than calling the cmd-line osascript.</p>
 * @version 2.1 Apr 2, 2005
 */
public final class CocoaJavaBridge extends NativeInvocationBridge
{
    /**
     * The path the Cocoa-Java class files are located at
     */
    protected static final String CLASS_PATH = "/system/library/java";

    private static final String REVEAL_SCRIPT_FORMAT = "tell application \"System Events\"\ntell application \"{0}\"\nactivate\nreveal (posix file \"{1}\" as alias)\nend tell\nend tell";

    private static final String DEL_SCRIPT_FORMAT = "tell application \"Finder\" to move (posix file \"{0}\" as alias) to the trash";

    /**
     * Main NSAutoreleasePool
     */
    private int mainPool;

    protected AEMonitor classMon = new AEMonitor("CocoaJavaBridge:C");
    private AEMonitor scriptMon = new AEMonitor("CocoaJavaBridge:S");

    protected boolean isDisposed = false;

    protected RunnableDispatcher scriptDispatcher;
    
    private Class claNSAppleEventDescriptor;

		private Class<?> claNSAutoreleasePool;

		private Method methPush;

		private Method methPop;

		private Method methNSAppleEventDescriptor_descriptorWithBoolean;

		private Class<?> claNSAppleScript;

		private Class<?> claNSMutableDictionary;

		private Method methNSAppleScript_execute;

		private String NSAppleScript_AppleScriptErrorMessage;

		private Method methNSMutableDictionary_objectForKey;

    public CocoaJavaBridge() throws Throwable
    {
        try
        {
            classMon.enter();

            claNSMutableDictionary = Class.forName("com.apple.cocoa.foundation.NSMutableDictionary");
            methNSMutableDictionary_objectForKey = claNSMutableDictionary.getMethod("objectForKey", Object.class);
            
            claNSAppleEventDescriptor = Class.forName("com.apple.cocoa.foundation.NSAppleEventDescriptor");
            methNSAppleEventDescriptor_descriptorWithBoolean = claNSAppleEventDescriptor.getMethod("descriptorWithBoolean", new Class [] { boolean.class });

            claNSAutoreleasePool = Class.forName("com.apple.cocoa.foundation.NSAutoreleasePool");
            methPush = claNSAutoreleasePool.getMethod("push", new Class [0]);
            methPop = claNSAutoreleasePool.getMethod("pop", new Class [] { int.class });

            claNSAppleScript = Class.forName("com.apple.cocoa.foundation.NSAppleScript");
            methNSAppleScript_execute = claNSAppleScript.getMethod("execute", new Class[] { claNSMutableDictionary });
            NSAppleScript_AppleScriptErrorMessage = (String) claNSAppleScript.getField("AppleScriptErrorMessage").get(null);


            //mainPool = NSAutoreleasePool.push();
            mainPool = NSAutoreleasePool_push();

            scriptDispatcher = new RunnableDispatcher();
        }
        finally
        {
            classMon.exit();
        }
    }
    
    private int NSAutoreleasePool_push() throws Throwable
    {
      return ((Number) methPush.invoke(null)).intValue();
    }

    private void NSAutoreleasePool_pop(int i) throws Throwable
    {
      methPop.invoke(null, i);
    }
    
    private Object new_NSAppleScript(String s) throws Throwable {
    	return claNSAppleScript.getConstructor(new Class[] { String.class }).newInstance(s);
    }
    
    private Object NSAppleScript_execute(Object NSAppleScript, Object NSMutableDictionary) throws Throwable {
    	return methNSAppleScript_execute.invoke(NSAppleScript, NSMutableDictionary);
    }
    
    private Object new_NSMutableDictionary() throws Throwable {
    	return claNSMutableDictionary.newInstance();
    }
    
    private Object NSMutableDictionary_objectForKey(Object NSMutableDictionary, String s) throws Throwable {
    	return methNSMutableDictionary_objectForKey.invoke(NSMutableDictionary, s);
    }

    // interface implementation

    /**
     * {@inheritDoc}
     */
    protected boolean performRecoverableFileDelete(File path)
    {
        if(!path.exists()){
            return false;
        }
        
        Object result;
				try {
					result = executeScriptWithAsync(DEL_SCRIPT_FORMAT, new Object[]{path.getAbsolutePath()});
				} catch (Throwable t) {
					Debug.out(t);
					return false;
				}
        
        	// quick hack here for people where things take a while - too scared to make it a 
        	// sync call as I don't know the code...
                
        if ( result != null ){

            final int sleep = 25;
            
            int sleep_to_go = 2500;

        	while( path.exists()){
        		
        		if ( sleep_to_go <= 0 ){
        			
        			break;
        		}
        		
        		try{
        			Thread.sleep( sleep );
        			
        			sleep_to_go -= sleep;
        			
        		}catch( Throwable e ){
        			break;
        		}
        	}
        
        	if ( path.exists()){
        	
        		Debug.outNoStack( "Gave up waiting for delete to complete for " + path );
        	}
        }
        
        return( result != null );
    }

    /**
     * {@inheritDoc}
     */
	protected boolean showInFinder(File path, String fileBrowserApp) {
		if (!path.exists())
			return false;

		Object /*NSAppleEventDescriptor*/ result = null;
		try {
			int pool = NSAutoreleasePool_push();
			try {
				result = executeScriptWithAsync(REVEAL_SCRIPT_FORMAT, new Object[] {
					fileBrowserApp,
					path.getAbsolutePath()
				});
			} finally {
				NSAutoreleasePool_pop(pool);
			}
		} catch (Throwable t) {
			return false;
		}
		return (result != null);
	}

    /**
     * {@inheritDoc}
     */
    protected boolean isEnabled()
    {
    	return claNSAutoreleasePool != null;
    }

    // class utility methods

    /**
     * <p>Executes a new instance of NSAppleScript</p>
     * <p>The method is wrapped in an autorelease pool and an AEMonitor. If there are
     * no format parameters, MessageFormat is not used to parse the format string, and
     * the format string will be treated as the source itself.</p>
     * @see MessageFormat#format(String, Object...)
     * @see NSAppleScript#execute(com.apple.cocoa.foundation.NSMutableDictionary)
     */
    protected final Object /*NSAppleEventDescriptor*/ executeScript(String scriptFormat, Object[] params) throws Throwable
    {
        try
        {
            scriptMon.enter();

            int pool = NSAutoreleasePool_push();
            long start = System.currentTimeMillis();

            String src;
            if(params == null || params.length == 0)
            {
                src = scriptFormat;
            }
            else
            {
                src = MessageFormat.format(scriptFormat, params);
            }

            Debug.outNoStack("Executing: \n" + src);

            Object /*NSAppleScript*/ scp = new_NSAppleScript(src);
            Object /*NSAppleEventDescriptor*/ result =  NSAppleScript_execute(scp, new_NSMutableDictionary());

            Debug.outNoStack(MessageFormat.format("Elapsed time: {0}ms\n", new Object[]{new Long(System.currentTimeMillis() - start)}));
            NSAutoreleasePool_pop(pool);
            return result;
        }
        finally
        {
            scriptMon.exit();
        }
    }

    /**
     * <p>Executes a new instance of NSAppleScript in a forked AEThread</p>
     * <p>This method always returns a "true" event descriptor. Callbacks are currently unsupported
     * , so in the event of an error, the logger is autuomatically notified.</p>
     * <p>The thread's runSupport method is wrapped in an autorelease pool. If there are
     * no format parameters, MessageFormat is not used to parse the format string, and
     * the format string will be treated as the source itself.</p>
     * @see org.gudy.azureus2.core3.util.AEThread#runSupport()
     * @see MessageFormat#format(String, Object...)
     * @see NSAppleScript#execute(com.apple.cocoa.foundation.NSMutableDictionary)
     * @return NSAppleEventDescriptor.descriptorWithBoolean(true)
     * @throws InvocationTargetException 
     * @throws IllegalAccessException 
     * @throws IllegalArgumentException 
     */
  protected final Object /*NSAppleEventDescriptor*/ executeScriptWithNewThread(
		final String scriptFormat, final Object[] params) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException
  {
		Thread worker = new AEThread("ScriptObject", true) {
			public void runSupport() {
				try {
					int pool = NSAutoreleasePool_push();
					long start = System.currentTimeMillis();

					String src;
					if (params == null || params.length == 0) {
						src = scriptFormat;
					} else {
						src = MessageFormat.format(scriptFormat, params);
					}		

					Debug.outNoStack("Executing: \n" + src);

					Object /*NSMutableDictionary*/ errorInfo = new_NSMutableDictionary();
					if (NSAppleScript_execute(new_NSAppleScript(src), errorInfo) == null) {
						Debug.out(String.valueOf(NSMutableDictionary_objectForKey(errorInfo, NSAppleScript_AppleScriptErrorMessage)));
						//logWarning(String.valueOf(errorInfo.objectForKey(NSAppleScript.AppleScriptErrorBriefMessage)));
					}

					Debug.outNoStack(MessageFormat.format("Elapsed time: {0}ms\n",
							new Object[] {
								new Long(System.currentTimeMillis() - start)
							}));
					NSAutoreleasePool_pop(pool);
				} catch (Throwable e) {
					Debug.out(e);
				}
			}
		};

		worker.setPriority(Thread.NORM_PRIORITY - 1);
		worker.start();

		return methNSAppleEventDescriptor_descriptorWithBoolean.invoke(null, true);
		//return NSAppleEventDescriptor.descriptorWithBoolean(true);
	}

    /**
     * <p>Asynchronously executes a new instance of NSAppleScript</p>
     * <p>This method always returns a "true" event descriptor. Callbacks are currently unsupported
     * , so in the event of an error, the logger is autuomatically notified.</p>
     * <p>The thread's runSupport method is wrapped in an autorelease pool. If there are
     * no format parameters, MessageFormat is not used to parse the format string, and
     * the format string will be treated as the source itself.</p>
     * @see org.gudy.azureus2.core3.util.AEThread#runSupport()
     * @see MessageFormat#format(String, Object...)
     * @see NSAppleScript#execute(com.apple.cocoa.foundation.NSMutableDictionary)
     * @return NSAppleEventDescriptor.descriptorWithBoolean(true)
     * @throws InvocationTargetException 
     * @throws IllegalAccessException 
     * @throws IllegalArgumentException 
     */
    protected final Object /*NSAppleEventDescriptor*/ executeScriptWithAsync(final String scriptFormat, final Object[] params) throws IllegalArgumentException, IllegalAccessException, InvocationTargetException
    {
        final AERunnable worker = new AERunnable()
        {
			public void runSupport() {
				try {
					int pool = NSAutoreleasePool_push();
					long start = System.currentTimeMillis();

					String src;
					if (params == null || params.length == 0) {
						src = scriptFormat;
					} else {
						src = MessageFormat.format(scriptFormat, params);
					}

					Debug.outNoStack("Executing: \n" + src);

					Object /*NSMutableDictionary*/ errorInfo = new_NSMutableDictionary();
					if (NSAppleScript_execute(new_NSAppleScript(src), errorInfo) == null) {
						Debug.out(String.valueOf(NSMutableDictionary_objectForKey(errorInfo, NSAppleScript_AppleScriptErrorMessage)));
						//logWarning(String.valueOf(errorInfo.objectForKey(NSAppleScript.AppleScriptErrorBriefMessage)));
					}

					Debug.outNoStack(MessageFormat.format("Elapsed time: {0}ms\n",
							new Object[] {
								new Long(System.currentTimeMillis() - start)
							}));
					NSAutoreleasePool_pop(pool);
				} catch (Throwable t) {
					Debug.out(t);
				}
			}
		};

        AEThread t = new AEThread("ScriptObject", true)
        {
            public void runSupport()
            {
                scriptDispatcher.exec(worker);
            }
        };
        t.setPriority(Thread.NORM_PRIORITY - 1);
        t.start();

    		return methNSAppleEventDescriptor_descriptorWithBoolean.invoke(null, true);
    		//return NSAppleEventDescriptor.descriptorWithBoolean(true);
    }

    /**
     * Logs a warning message to Logger. The class monitor is used.
     * @param message A warning message
     */
    private void logWarning(String message)
    {
        try
        {
            classMon.enter();
            Logger.log(new LogAlert(LogAlert.UNREPEATABLE, LogAlert.AT_WARNING, message));
        }
        finally
        {
            classMon.exit();
        }
    }

    // disposal

    /**
     * {@inheritDoc}
     */
    protected void dispose()
    {
        try
        {
            classMon.enter();
            if(!isDisposed)
            {
                Debug.outNoStack("Disposing Native PlatformManager...");
                try {
									NSAutoreleasePool_pop(mainPool);
								} catch (Throwable e) {
								}
                isDisposed = true;
                Debug.outNoStack("Done");
            }
        }
        finally
        {
            classMon.exit();
        }
    }

    /**
     * {@inheritDoc}
     */
    protected void finalize() throws Throwable
    {
        dispose();
        super.finalize();
    }

    /**
     * A dispatch object to help facilitate asychronous script execution (from the main thread) in a more
     * predictable fashion.
     */
    private static class RunnableDispatcher
    {
        /**
         * Executes a Runnable object while synchronizing the RunnableDispatcher instance.
         * @param runnable A Runnable
         */
        private void exec(Runnable runnable)
        {
            synchronized(this)
            {
                runnable.run();
            }
        }
    }
}
