/*
 * Copyright 2014 by A-SIT, Secure Information Technology Center Austria
 *
 * Licensed under the EUPL, Version 1.1 or - as soon they will be approved by
 * the European Commission - subsequent versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 * http://joinup.ec.europa.eu/software/page/eupl
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the Licence is distributed on an "AS IS" basis,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the Licence for the specific language governing permissions and
 * limitations under the Licence.
 */

package at.asit.pdfover.gui.osx;

import java.lang.reflect.Method;

import org.eclipse.swt.SWT;
import org.eclipse.swt.internal.C;
import org.eclipse.swt.internal.Callback;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Listener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import at.asit.pdfover.commons.Constants;
import at.asit.pdfover.commons.Messages;

/**
 * Provide a hook to connecting the Preferences, About and Quit menu items of
 * the Mac OS X Application menu when using the SWT Cocoa bindings.
 * <p>
 * This code does not require the Cocoa SWT JAR in order to be compiled as it
 * uses reflection to access the Cocoa specific API methods. Use SWT Listeners
 * instead in order to use this class in SWT only applications.
 * 
 * </p>
 * <p>
 * This code was influenced by the <a
 * href="http://www.transparentech.com/opensource/cocoauienhancer"
 * >CocoaUIEnhancer - Connect the About, Preferences and Quit menus in Mac OS X
 * Cocoa SWT and JFace applications</a>.
 * </p>
 * <p>
 * This class works with both the 32-bit and 64-bit versions of the SWT Cocoa
 * bindings.
 * <p>
 * <p>
 * This class is released under the Eclipse Public License (<a
 * href="http://www.eclipse.org/legal/epl-v10.html">EPL</a>).
 */
public class CocoaUIEnhancer {
	static final Logger log = LoggerFactory.getLogger(CocoaUIEnhancer.class);

	private static final long kAboutMenuItem = 0;
	private static final long kPreferencesMenuItem = 2;
	// private static final long kServicesMenuItem = 4;
	private static final long kHideApplicationMenuItem = 6;
	private static final long kQuitMenuItem = 10;

	static long sel_toolbarButtonClicked_;
	static long sel_preferencesMenuItemSelected_;
	static long sel_aboutMenuItemSelected_;
	static long sel_hideApplicationMenuItemSelected_;

	static Callback proc3Args;

	/**
	 * Class invoked via the Callback object to run the about and preferences
	 * actions.
	 */
	private static class MenuHookObject {

		final Listener about;
		final Listener pref;

		public MenuHookObject(final Listener aboutListener,
				final Listener preferencesListener) {
			this.about = aboutListener;
			this.pref = preferencesListener;
		}

		/**
		 * Will be called on 32bit SWT.
		 * @param id 
		 * @param sel 
		 * @param arg0 
		 * @return x
		 */
		@SuppressWarnings("unused")
		public int actionProc(final int id, final int sel, final int arg0) {
			return (int) this.actionProc((long) id, (long) sel, (long) arg0);
		}

		/**
		 * Will be called on 64bit SWT.
		 * @param id 
		 * @param sel 
		 * @param arg0 
		 * @return x
		 */
		public long actionProc(final long id, final long sel, final long arg0) {
			if (sel == sel_aboutMenuItemSelected_) {
				if (log.isDebugEnabled()) {
					log.debug("[MenuHookObject - actionProc] : About"); //$NON-NLS-1$
					this.about.handleEvent(null);
				}
			} else if (sel == sel_preferencesMenuItemSelected_) {
				if (log.isDebugEnabled()) {
					log.debug("[MenuHookObject - actionProc] : Preferences"); //$NON-NLS-1$
				}
				this.pref.handleEvent(null);

			} else {
				if (log.isDebugEnabled()) {
					log.debug("[MenuHookObject - actionProc] : Unknow selection!"); //$NON-NLS-1$
				}
			}
			// Return value is not used.
			return 99;
		}

		// Getters and setters
		@SuppressWarnings("unused")
		public Listener getAbout() {
			return this.about;
		}

		@SuppressWarnings("unused")
		public Listener getPref() {
			return this.pref;
		}
	}

	/**
	 * Hook the given Listener to the Mac OS X application Quit menu and the
	 * IActions to the About and Preferences menus.
	 * 
	 * @param display
	 *            The Display to use.
	 * @param quitListener
	 *            The listener to invoke when the Quit menu is invoked.
	 * @param aboutListener
	 *            The listener to invoke when the About menu is invoked.
	 * @param preferencesListener
	 *            The listener to invoke when the Preferences menu is invoked.
	 */
	public static void hookApplicationMenu(final Display display,
			final Listener quitListener, final Listener aboutListener,
			final Listener preferencesListener) {
		// This is our callbackObject whose 'actionProc' method will be called
		// when the About or
		// Preferences menuItem is invoked.
		final MenuHookObject target = new MenuHookObject(aboutListener,
				preferencesListener);

		try {
			// Initialize the menuItems.
			initialize(target);
		} catch (final Exception e) {
			throw new IllegalStateException(e);
		}

		// Connect the quit/exit menu.
		if (!display.isDisposed()) {
			display.addListener(SWT.Close, quitListener);
		}

		// Schedule disposal of callback object
		display.disposeExec(new Runnable() {
			@Override
			public void run() {
				CocoaUIEnhancer.invoke(proc3Args, "dispose"); //$NON-NLS-1$
			}
		});
	}

	private static void initialize(final Object callbackObject) throws Exception {

		final Class<?> osCls = classForName("org.eclipse.swt.internal.cocoa.OS"); //$NON-NLS-1$

		// Register names in objective-c.
		if (sel_toolbarButtonClicked_ == 0) {
			// sel_toolbarButtonClicked_ = registerName( osCls, "toolbarButtonClicked:" ); //$NON-NLS-1$
			sel_preferencesMenuItemSelected_ = registerName(osCls,
					"preferencesMenuItemSelected:"); //$NON-NLS-1$
			sel_aboutMenuItemSelected_ = registerName(osCls,
					"aboutMenuItemSelected:"); //$NON-NLS-1$
		}

		// Create an SWT Callback object that will invoke the actionProc method
		// of our internal
		// callbackObject.
		proc3Args = new Callback(callbackObject, "actionProc", 3); //$NON-NLS-1$
		final Method getAddress = Callback.class.getMethod("getAddress", //$NON-NLS-1$
				new Class[0]);
		Object object = getAddress.invoke(proc3Args, (Object[]) null);
		final long proc3 = convertToLong(object);
		if (proc3 == 0) {
			SWT.error(SWT.ERROR_NO_MORE_CALLBACKS);
		}

		final Class<?> nsmenuCls = classForName("org.eclipse.swt.internal.cocoa.NSMenu"); //$NON-NLS-1$
		final Class<?> nsmenuitemCls = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem"); //$NON-NLS-1$
		final Class<?> nsstringCls = classForName("org.eclipse.swt.internal.cocoa.NSString"); //$NON-NLS-1$
		final Class<?> nsapplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication"); //$NON-NLS-1$

		// Instead of creating a new delegate class in objective-c,
		// just use the current SWTApplicationDelegate. An instance of this
		// is a field of the Cocoa Display object and is already the target
		// for the menuItems. So just get this class and add the new methods
		// to it.
		object = invoke(osCls, "objc_lookUpClass", //$NON-NLS-1$
				new Object[] { "SWTApplicationDelegate" }); //$NON-NLS-1$
		final long cls = convertToLong(object);

		// Add the action callbacks for Preferences and About menu items.
		invoke(osCls, "class_addMethod", new Object[] { wrapPointer(cls), //$NON-NLS-1$
				wrapPointer(sel_preferencesMenuItemSelected_),
				wrapPointer(proc3), "@:@" }); //$NON-NLS-1$
		invoke(osCls, "class_addMethod", new Object[] { wrapPointer(cls), //$NON-NLS-1$
				wrapPointer(sel_aboutMenuItemSelected_), wrapPointer(proc3),
				"@:@" }); //$NON-NLS-1$

		// Get the Mac OS X Application menu.
		final Object sharedApplication = invoke(nsapplicationCls,
				"sharedApplication"); //$NON-NLS-1$
		final Object mainMenu = invoke(sharedApplication, "mainMenu"); //$NON-NLS-1$
		final Object mainMenuItem = invoke(nsmenuCls, mainMenu, "itemAtIndex", //$NON-NLS-1$
				new Object[] { wrapPointer(0) });
		final Object appMenu = invoke(mainMenuItem, "submenu"); //$NON-NLS-1$

		// Create the About <application-name> menu command
		final Object aboutMenuItem = invoke(nsmenuCls, appMenu, "itemAtIndex", //$NON-NLS-1$
				new Object[] { wrapPointer(kAboutMenuItem) });
		final Object nsStrAbout = invoke(nsstringCls, "stringWith", //$NON-NLS-1$
				new Object[] { String.format(Messages.getString("main.about"), Constants.APP_NAME) }); //$NON-NLS-1$
		invoke(nsmenuitemCls, aboutMenuItem, "setTitle", //$NON-NLS-1$
				new Object[] { nsStrAbout });
		// Rename the quit action.
		final Object quitMenuItem = invoke(nsmenuCls, appMenu,
				"itemAtIndex", new Object[] { wrapPointer(kQuitMenuItem) }); //$NON-NLS-1$
		final Object nsStrQuit = invoke(nsstringCls, "stringWith", //$NON-NLS-1$
				new Object[] { String.format(Messages.getString("main.quit"), Constants.APP_NAME) }); //$NON-NLS-1$
		invoke(nsmenuitemCls, quitMenuItem, "setTitle", //$NON-NLS-1$
				new Object[] { nsStrQuit });

		// Rename the hide action.
		final Object hideMenuItem = invoke(nsmenuCls, appMenu,
				"itemAtIndex", //$NON-NLS-1$
				new Object[] { wrapPointer(kHideApplicationMenuItem) });
		final Object nsStrHide = invoke(nsstringCls, "stringWith", //$NON-NLS-1$
				new Object[] { String.format(Messages.getString("main.hide"), Constants.APP_NAME) }); //$NON-NLS-1$
		invoke(nsmenuitemCls, hideMenuItem, "setTitle", //$NON-NLS-1$
				new Object[] { nsStrHide });

		// Enable the Preferences menuItem.
		final Object prefMenuItem = invoke(nsmenuCls, appMenu, "itemAtIndex", //$NON-NLS-1$
				new Object[] { wrapPointer(kPreferencesMenuItem) });
		invoke(nsmenuitemCls, prefMenuItem, "setEnabled", new Object[] { true }); //$NON-NLS-1$

		// Set the action to execute when the About or Preferences menuItem is
		// invoked.
		//
		// We don't need to set the target here as the current target is the
		// SWTApplicationDelegate
		// and we have registerd the new selectors on it. So just set the new
		// action to invoke the
		// selector.
		invoke(nsmenuitemCls, prefMenuItem, "setAction", //$NON-NLS-1$
				new Object[] { wrapPointer(sel_preferencesMenuItemSelected_) });
		invoke(nsmenuitemCls, aboutMenuItem, "setAction", //$NON-NLS-1$
				new Object[] { wrapPointer(sel_aboutMenuItemSelected_) });
	}

	private static long registerName(final Class<?> osCls, final String name)
			throws IllegalArgumentException, SecurityException {
		final Object object = invoke(osCls, "sel_registerName", //$NON-NLS-1$
				new Object[] { name });
		return convertToLong(object);
	}

	private static long convertToLong(final Object object) {
		if (object instanceof Integer) {
			final Integer i = (Integer) object;
			return i.longValue();
		}
		if (object instanceof Long) {
			final Long l = (Long) object;
			return l.longValue();
		}
		return 0;
	}

	private static Object wrapPointer(final long value) {
		final Class<?> PTR_CLASS = C.PTR_SIZEOF == 8 ? long.class : int.class;
		if (PTR_CLASS == long.class) {
			return Long.valueOf(value);
		}
		return Integer.valueOf((int) value);
	}

	private static Object invoke(final Class<?> clazz, final String methodName,
			final Object[] args) {
		return invoke(clazz, null, methodName, args);
	}

	private static Object invoke(final Class<?> clazz, final Object target,
			final String methodName, final Object[] args) {
		try {
			final Class<?>[] signature = new Class<?>[args.length];
			for (int i = 0; i < args.length; i++) {
				final Class<?> thisClass = args[i].getClass();
				if (thisClass == Integer.class) {
					signature[i] = int.class;
				} else if (thisClass == Long.class) {
					signature[i] = long.class;
				} else if (thisClass == Byte.class) {
					signature[i] = byte.class;
				} else if (thisClass == Boolean.class) {
					signature[i] = boolean.class;
				} else {
					signature[i] = thisClass;
				}
			}
			final Method method = clazz.getMethod(methodName, signature);
			return method.invoke(target, args);
		} catch (final Exception e) {
			throw new IllegalStateException(e);
		}
	}

	private static Class<?> classForName(final String classname) {
		try {
			final Class<?> cls = Class.forName(classname);
			return cls;
		} catch (final ClassNotFoundException e) {
			throw new IllegalStateException(e);
		}
	}

	private static Object invoke(final Class<?> cls, final String methodName) {
		return invoke(cls, methodName, (Class<?>[]) null, (Object[]) null);
	}

	private static Object invoke(final Class<?> cls, final String methodName,
			final Class<?>[] paramTypes, final Object... arguments) {
		try {
			final Method m = cls.getDeclaredMethod(methodName, paramTypes);
			return m.invoke(null, arguments);
		} catch (final Exception e) {
			throw new IllegalStateException(e);
		}
	}

	static Object invoke(final Object obj, final String methodName) {
		return invoke(obj, methodName, (Class<?>[]) null, (Object[]) null);
	}

	private static Object invoke(final Object obj, final String methodName,
			final Class<?>[] paramTypes, final Object... arguments) {
		try {
			final Method m = obj.getClass().getDeclaredMethod(methodName,
					paramTypes);
			return m.invoke(obj, arguments);
		} catch (final Exception e) {
			throw new IllegalStateException(e);
		}
	}
}