/*******************************************************************************
 * Copyright (c) 2007-2008 SAS Institute Inc., ILOG S.A.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     SAS Institute Inc. - initial API and implementation
 *     ILOG S.A. - initial API and implementation
 *******************************************************************************/
package org.eclipse.albireo.internal;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.event.MouseEvent;
import java.util.WeakHashMap;

import javax.swing.JComponent;

import org.eclipse.albireo.core.AwtEnvironment;
import org.eclipse.albireo.core.SwingControl;
import org.eclipse.albireo.core.ThreadingHandler;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
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 org.eclipse.swt.widgets.Shell;

public class SwtPopupHandler {

    // Gtk will not display popup menus that are parented to the
    // main shell. Instead we need to create and open a second
    // shell to own the popup. Note that this means the SWT menu
    // needs to be created by the client with the parent returned by 
    // AwtEnvironment#getSwtPopupParent. 
    private static final boolean CREATE_POPUP_PARENT_SHELL = Platform.isGtk();

    // Gtk will not display popup menus that are made visible by an 
    // AWT mouse click (mouse pressed event), unless we wait until 
    // 1) the mouse has been released and 
    // 2) the mouse event for entering the parent shell has been processed
    // This flag enables the delay
    // TODO: this flag effectively changes the popup trigger from mouse down to mouse up (can it be avoided?) 
    private static final boolean DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS = Platform.isGtk();
    
    // TODO: in some relatively rare cases, on GTK, SWT popups are not dismissed 
    // It can happen if you try to dismiss it with a left click while
    // rapidly moving the mouse. A second click will dismiss it.
    
    // TODO: SWT popups cannot be attached to JLabels. 
    // Do we need to use ILOG's findComponentAt() method?
    
    // TODO: On windows, the AWT/Swing cursor is retained after showing the SWT popup
    // Setting CREATE_POPUP_PARENT_SHELL to true might be a solution, but we also 
    // need general cursor support which might also solve this problem.
    
    private boolean pendingSwtPopup = false;

    // Listen to AWT mouse events and show SWT popups as necessary
    private java.awt.event.AWTEventListener popupEventListener =
        new java.awt.event.AWTEventListener() {
        public void eventDispatched(java.awt.AWTEvent event) {
            if (event instanceof MouseEvent) {
                final MouseEvent me = (MouseEvent)event;
                switch (me.getID()) {
                case java.awt.event.MouseEvent.MOUSE_PRESSED:
                    boolean isTrigger = me.isPopupTrigger();
                    
                    if (DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS && CREATE_POPUP_PARENT_SHELL && isTrigger) {
                        // We must delay the Swt popup here. Otherwise the parent shell will not be
                        // properly opened if the user clicks, then drags, then releases the mouse. 
                        
                        // System.err.println("delaying any SWT popup display");
                        pendingSwtPopup = true;
                        isTrigger = false;
                    }
                    if (isTrigger) {
                        handlePopupTrigger(me);
                    }
                    break;
                    
                case java.awt.event.MouseEvent.MOUSE_RELEASED:
                    // TODO: can both conditions below ever be true on the same event? 
                    if (pendingSwtPopup) {
                        // Now handle any previously delayed popups
                        // if (pendingSwtPopup) {
                        //    System.err.println("handling any delayed SWT popup display");
                        // }
                        handlePopupTrigger(me);
                        pendingSwtPopup = false;
                    }
                    if (me.isPopupTrigger()) {
                        handlePopupTrigger(me);
                    }
                    break;
                    
                case java.awt.event.MouseEvent.MOUSE_CLICKED:
                    // TODO: is MOUSE_CLICKED a valid trigger point? 
                    if (me.isPopupTrigger()) {
                         handlePopupTrigger(me);
                    }
                    break;
                }
            }
        }

    };
    

    // The set of toolkits to which the popupEventListener has already been added.
    private WeakHashMap /* java.awt.Toolkit -> Boolean */ popupSupportedToolkits =
        new WeakHashMap();

    public void monitorAwtComponent(Component component) {
        assert EventQueue.isDispatchThread();
        
        // Ensure the toolkit has the popupEventListener attached.
        java.awt.Toolkit toolkit = component.getToolkit();
        synchronized(popupSupportedToolkits) {
            if (popupSupportedToolkits.get(toolkit) == null) {
                toolkit.addAWTEventListener(popupEventListener, java.awt.AWTEvent.MOUSE_EVENT_MASK);
                popupSupportedToolkits.put(toolkit, Boolean.TRUE);
            }
        }
    }

    protected SwingControl getSwtParent(Component c) {
        // Return the SwingControl, if any, associated with the component
        if (c instanceof JComponent) {
            JComponent jc = (JComponent)c;
            return (SwingControl)jc.getClientProperty(SwingControl.SWT_PARENT_PROPERTY_KEY);
        } else {
            return null;
        }
    }


    protected void handlePopupTrigger(MouseEvent event) {
        assert EventQueue.isDispatchThread();
        
        java.awt.Component component = (java.awt.Component)event.getSource();
        int x = event.getX();
        int y = event.getY();
        // Climb up until the we find the SwingControl mapped to one of our parents
        java.awt.Component parent = component;
        while (parent != null && (getSwtParent(parent) == null)) {
            x += parent.getX();
            y += parent.getY();
            parent = parent.getParent();
        }
        if (parent != null) {
            SwingControl swtParent = getSwtParent(parent);
            int xAbsolute = x;
            int yAbsolute = y;
            java.awt.Component subcomp = (Component)event.getSource();
            showPopupMenu(swtParent, subcomp, x, y, xAbsolute, yAbsolute);
        }
    }
    

    // Trigger the display of the popup menu.
    protected void showPopupMenu(final SwingControl swtParent, final java.awt.Component component, final int x, final int y, final int xAbsolute, final int yAbsolute) {
        assert EventQueue.isDispatchThread();   
        
        Display display;
        try {
            display = swtParent.getDisplay();
        } catch (SWTException e) {
            if (e.code == SWT.ERROR_WIDGET_DISPOSED)
                return;
            else
                throw e;
        }
        Runnable task =
            new Runnable() {
            public void run() {
                try {
                    if (!swtParent.isDisposed()) {
                        showPopupMenuSWTThread(swtParent, component, x, y, xAbsolute, yAbsolute);
                    }
                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        };
        try {
            ThreadingHandler.getInstance().asyncExec(display, task);
        } catch (SWTException e) {
            if (e.code == SWT.ERROR_DEVICE_DISPOSED)
                return;
            else
                throw e;
        }
    }

    // Trigger the display of the popup menu from the SWT thread.
    protected void showPopupMenuSWTThread(SwingControl swtParent, java.awt.Component component, int x, int y, int xAbsolute, int yAbsolute) {
        assert Display.getCurrent() != null;
        
        final Menu menu = swtParent.getMenu(component, x, y, xAbsolute, yAbsolute);
        Display display = swtParent.getDisplay();
        if (menu != null) {
            final Shell popupParent = AwtEnvironment.getInstance(display).getSwtPopupParent(swtParent);
            
            if (DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS) {
                // Install a listener that waits until the popup parent receives 
                // a MouseEnter event before displaying the menu. If we don't wait
                // for this event, the menu is not displayed. 
                popupParent.addListener(SWT.MouseEnter, new Listener() {
                    public void handleEvent(Event e) {
                        if (CREATE_POPUP_PARENT_SHELL) {
                            // Install a listener to hide the (created) popup parent once the menu is 
                            // dismissed. (Don't count on 0x0 sized shells to be invisible without this)
                            menu.addListener(SWT.Hide, new Listener() {
                                public void handleEvent(Event e) {
                                    if (!popupParent.isDisposed()) {
                                        // System.err.println("hiding popup parent");
                                        popupParent.setVisible(false);
                                    }
                                    // Clean up
                                    menu.removeListener(SWT.Hide, this);
                                }
                            });
                        }
                        
                        if (!menu.isDisposed()) {
                            menu.setVisible(true);
                        }
                        // Clean up
                        popupParent.removeListener(SWT.MouseEnter, this);
                    }
                });
            }
            
            if (CREATE_POPUP_PARENT_SHELL) {
                // Display the created parent shell at the current cursor location
                // System.err.println("*** opening popup parent" + display.getCursorLocation());
                popupParent.setLocation(display.getCursorLocation());
                popupParent.open();
                // The menu is made visible in the listener above
            } 
            if (!DELAY_MOUSE_DOWN_SWT_POPUP_TRIGGERS) {
                // If we are not waiting for MouseEnter, then open the menu right here
                menu.setVisible(true);
            }
        }
    }

    // ========================================================================
    // Singleton design pattern

    private static SwtPopupHandler theHandler = new SwtPopupHandler();

    /**
     * Returns the currently active singleton of this class.
     */
    public static SwtPopupHandler getInstance() {
        return theHandler;
    }

    /**
     * Replaces the singleton of this class.
     * @param instance An instance of this class or of a customized subclass.
     */
    public static void setInstance(SwtPopupHandler instance) {
        theHandler = instance;
    }


}
