/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * History:
 * Original code by the <a href="http://www.simidude.com/blog/2008/macify-a-swt-application-in-a-cross-platform-way/">CarbonUIEnhancer from Agynami</a>
 * with the implementation being modified from the <a href="http://dev.eclipse.org/viewcvs/index.cgi/org.eclipse.ui.cocoa/src/org/eclipse/ui/internal/cocoa/CocoaUIEnhancer.java">org.eclipse.ui.internal.cocoa.CocoaUIEnhancer</a>,
 * then modified by http://www.transparentech.com/opensource/cocoauienhancer to use reflection
 * rather than 'link' to SWT cocoa, and finally modified to be usable by the SwtMenuBar project.
 */

package com.android.menubar.internal;

import com.android.menubar.IMenuBarCallback;
import com.android.menubar.IMenuBarEnhancer;

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.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Menu;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class MenuBarEnhancerCocoa implements IMenuBarEnhancer {

    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 mSelPreferencesMenuItemSelected;
    static long mSelAboutMenuItemSelected;
    static Callback mProc3Args;

    private String mAppName;

    /**
     * Class invoked via the Callback object to run the about and preferences
     * actions.
     * <p>
     * If you don't use JFace in your application (SWT only), change the
     * {@link org.eclipse.jface.action.IAction}s to
     * {@link org.eclipse.swt.widgets.Listener}s.
     * </p>
     */
    private static class ActionProctarget {
        private final IMenuBarCallback mCallbacks;

        public ActionProctarget(IMenuBarCallback callbacks) {
            mCallbacks = callbacks;
        }

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

        /**
         * Will be called on 64bit SWT.
         */
        public long actionProc(long id, long sel, long arg0) {
            if (sel == mSelAboutMenuItemSelected) {
                mCallbacks.onAboutMenuSelected();
            } else if (sel == mSelPreferencesMenuItemSelected) {
                mCallbacks.onPreferencesMenuSelected();
            } else {
                // Unknown selection!
            }
            // Return value is not used.
            return 0;
        }
    }

    /**
     * Construct a new CocoaUIEnhancer.
     *
     * @param mAppName The name of the application. It will be used to customize
     *            the About and Quit menu items. If you do not wish to customize
     *            the About and Quit menu items, just pass <tt>null</tt> here.
     */
    public MenuBarEnhancerCocoa() {
    }

    public MenuBarMode getMenuBarMode() {
        return MenuBarMode.MAC_OS;
    }

    /**
     * Setup the About and Preferences native menut items with the
     * given application name and links them to the callback.
     *
     * @param appName The application name.
     * @param display The SWT display. Must not be null.
     * @param callbacks The callbacks invoked by the menus.
     */
    public void setupMenu(
            String appName,
            Display display,
            IMenuBarCallback callbacks) {

        mAppName = appName;

        // This is our callback object whose 'actionProc' method will be called
        // when the About or Preferences menuItem is invoked.
        ActionProctarget target = new ActionProctarget(callbacks);

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

        // Schedule disposal of callback object
        display.disposeExec(new Runnable() {
            public void run() {
                invoke(mProc3Args, "dispose");
            }
        });
    }

    private void initialize(Object callbackObject)
            throws Exception {

        Class<?> osCls = classForName("org.eclipse.swt.internal.cocoa.OS");

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

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

        Class<?> nsMenuCls        = classForName("org.eclipse.swt.internal.cocoa.NSMenu");
        Class<?> nsMenuitemCls    = classForName("org.eclipse.swt.internal.cocoa.NSMenuItem");
        Class<?> nsStringCls      = classForName("org.eclipse.swt.internal.cocoa.NSString");
        Class<?> nsApplicationCls = classForName("org.eclipse.swt.internal.cocoa.NSApplication");

        // 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", new Object[] {
            "SWTApplicationDelegate"
        });
        long cls = convertToLong(object);

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

        // Get the Mac OS X Application menu.
        Object sharedApplication = invoke(nsApplicationCls, "sharedApplication");
        Object mainMenu = invoke(sharedApplication, "mainMenu");
        Object mainMenuItem = invoke(nsMenuCls, mainMenu, "itemAtIndex", new Object[] {
            wrapPointer(0)
        });
        Object appMenu = invoke(mainMenuItem, "submenu");

        // Create the About <application-name> menu command
        Object aboutMenuItem =
                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
                    wrapPointer(kAboutMenuItem)
                });
        if (mAppName != null) {
            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
                "About " + mAppName
            });
            invoke(nsMenuitemCls, aboutMenuItem, "setTitle", new Object[] {
                nsStr
            });
        }
        // Rename the quit action.
        if (mAppName != null) {
            Object quitMenuItem =
                    invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
                        wrapPointer(kQuitMenuItem)
                    });
            Object nsStr = invoke(nsStringCls, "stringWith", new Object[] {
                "Quit " + mAppName
            });
            invoke(nsMenuitemCls, quitMenuItem, "setTitle", new Object[] {
                nsStr
            });
        }

        // Enable the Preferences menuItem.
        Object prefMenuItem =
                invoke(nsMenuCls, appMenu, "itemAtIndex", new Object[] {
                    wrapPointer(kPreferencesMenuItem)
                });
        invoke(nsMenuitemCls, prefMenuItem, "setEnabled", new Object[] {
            true
        });

        // 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 registered the new selectors on
        // it. So just set the new action to invoke the selector.
        invoke(nsMenuitemCls, prefMenuItem, "setAction",
                new Object[] {
                    wrapPointer(mSelPreferencesMenuItemSelected)
                });
        invoke(nsMenuitemCls, aboutMenuItem, "setAction",
                new Object[] {
                    wrapPointer(mSelAboutMenuItemSelected)
                });
    }

    private long registerName(Class<?> osCls, String name)
            throws IllegalArgumentException, SecurityException, IllegalAccessException,
            InvocationTargetException, NoSuchMethodException {
        Object object = invoke(osCls, "sel_registerName", new Object[] {
            name
        });
        return convertToLong(object);
    }

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

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

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

    private static Object invoke(Class<?> clazz, Object target, String methodName, Object[] args) {
        try {
            Class<?>[] signature = new Class<?>[args.length];
            for (int i = 0; i < args.length; i++) {
                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;
            }
            Method method = clazz.getMethod(methodName, signature);
            return method.invoke(target, args);
        } catch (Exception e) {
            throw new IllegalStateException(e);
        }
    }

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

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

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

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

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