/*
 * 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.button;

import java.awt.Dimension;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.Action;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener;

import com.mucommander.core.desktop.DesktopManager;
import com.mucommander.ui.action.impl.MuteProxyAction;

/**
 * PopupButton is a compound component that combines a JButton with a JPopupMenu.
 *
 * <p>When the mouse is held down on the button, a popup menu is displayed below the button. When the button is clicked,
 * if a specific action was specified at creation time or using {@link #setAction(Action)}, this action is performed.
 * If not, a popup menu is displayed below the button.
 *
 * <p>This class is abstract. Derived classes must implement {@link #getPopupMenu()} to return a JPopupMenu instance
 * to be displayed.
 *
 * @author Maxence Bernard
 */
public abstract class PopupButton extends NonFocusableButton {

    /** Custom action performed when the button is clicked. If null, popup menu will be displayed when mouse is clicked */
    private Action buttonClickedAction;

    /* Timestamp when popup menu was closed */
    private long popupMenuClosedTime;

    /** Non-null while popup menu is visible */
    protected JPopupMenu popupMenu;

    /** Location of the popup menu, relative to the button */
    private int popupMenuLocation = BOTTOM;

    /** Controls the number of milliseconds to hold down the mouse button on the button to display the popup menu */
    private final static int POPUP_DELAY = 300;

    /** 
     * Box-orientation constant used to specify the buttom-left oriented side of a box.
     */
    public static final int BUTTOM_LEFT_ORIENTED = 5;


    /**
     * Creates a new PopupButton with no custom action for when this button is clicked.
     * When this button is clicked, the popup menu as returned by {@link #getPopupMenu()} will be displayed.
     */
    public PopupButton() {
        this(null);
    }

    /**
     * Creates a new PopupButton with a custom action to performed when this button is clicked.
     *
     * @param buttonClickedAction custom action to performed when this button is clicked
     */
    public PopupButton(Action buttonClickedAction) {
        setAction(buttonClickedAction);

        // Listen to mouse events on this button
        addMouseListener(new PopupMenuHandler());
    }


    /**
     * Sets the action to be performed when this button is clicked. If <code>null</code> is passed, a popup menu will
     * displayed when this button is clicked.
     */
    @Override
    public void setAction(Action buttonClickedAction) {
        if(buttonClickedAction==null) {
            super.setAction(null);
        }
        else {
            // Pass a MuteProxyAction to JButton that does nothing when the action is performed.
            // We need this to keep the use the Action's properties but handle action events ourself.
            super.setAction(new MuteProxyAction(buttonClickedAction));
        }

        this.buttonClickedAction = buttonClickedAction;
    }

    public int getPopupMenuLocation() {
        return popupMenuLocation;
    }

    public void setPopupMenuLocation(int location) {
        this.popupMenuLocation = location;
    }

    /**
     * Returns true if a popup menu is currently being displayed.
     */
    public boolean isPopupMenuVisible() {
        return popupMenu!=null;
    }

    /**
     * Displays the popup menu as returned by {@link #getPopupMenu()}.
     */
    public synchronized void popupMenu() {
        this.popupMenu = getPopupMenu();

        popupMenu.addPopupMenuListener(new PopupMenuListener() {
            public void popupMenuWillBecomeVisible(PopupMenuEvent popupMenuEvent) {
            }

            public void popupMenuWillBecomeInvisible(PopupMenuEvent popupMenuEvent) {
                popupMenuClosed();
            }

            public void popupMenuCanceled(PopupMenuEvent popupMenuEvent) {
                popupMenuClosed();
            }

            private void popupMenuClosed() {
                // Set popup menu reference to null once it has been made invisible so that it can be garbage collected,
                // and remember the time at which the popup was closed, so that a mouse press on the button closes
                // the popup menu and does not bring it back up.
                popupMenuClosedTime = System.currentTimeMillis();
                popupMenu = null;
                setSelected(false);
            }
        });

        // Popup up the menu underneath under this button. This has to be executed by the event thread, otherwise some
        // weird repaint issue will arise under Windows at least (Note: this method can be executed by a thread other
        // than the event thread).
        SwingUtilities.invokeLater(() -> {
            Dimension popupMenuSize = popupMenu.getPreferredSize();

            popupMenu.show(PopupButton.this,
                    popupMenuLocation==RIGHT?getWidth():popupMenuLocation==LEFT?-(int)popupMenuSize.getWidth():popupMenuLocation==BUTTOM_LEFT_ORIENTED?getWidth()-(int)popupMenuSize.getWidth():0,
                            popupMenuLocation==BOTTOM?getHeight():popupMenuLocation==TOP?-(int)popupMenuSize.getHeight():popupMenuLocation==BUTTOM_LEFT_ORIENTED?getHeight():0
                    );
        });

        // Leave the button selected (shows that button has focus) while the popup menu is visible
        setSelected(true);

        // Note: focus MUST NOT be requested on the popup menu because:
        // a/ it's not necessary, focus is automatically transferred to the popup menu
        // b/ it creates a weird bug under Windows which prevents enter key from selecting any menu item
    }


    /**
     * This method is invoked when the popup menu is about to be displayed. This method must return the JPopupMenu
     * instance to display.
     */
    public abstract JPopupMenu getPopupMenu();


    ///////////////////
    // Inner classes //
    ///////////////////

    /**
     * This inner class controls this button's behavior and actions when the mouse button is clicked or held down.
     */
    private class PopupMenuHandler implements MouseListener, Runnable {

        /** Contains the time at which a mouse button was pressed, 0 when a mouse button is not currently being pressed */
        private long pressedTime;

        /** Returns true to indicate that a mouse event should currently be ignored because the popup menu
         * is visible, or was closed recently (less than POPUP_DELAY ms ago) */
        private boolean shouldIgnoreMouseEvent() {
            return isPopupMenuVisible() || System.currentTimeMillis()- popupMenuClosedTime<POPUP_DELAY;
        }

        //////////////////////////////////
        // MouseListener implementation //
        //////////////////////////////////

        public synchronized void mousePressed(MouseEvent mouseEvent) {
            if(!isEnabled() || shouldIgnoreMouseEvent())    // Ignore event if button is disabled
                return;

            if (DesktopManager.isRightMouseButton(mouseEvent)) {
                popupMenu();
            }
            else {
                pressedTime = System.currentTimeMillis();

                // Spawn a thread to check if mouse is still pressed in POPUP_DELAY ms. If that is the case, popup menu
                // will be displayed.
                new Thread(this).start();
            }
        }

        public synchronized void mouseClicked(MouseEvent mouseEvent) {
            if(!isEnabled() || shouldIgnoreMouseEvent())    // Ignore event if button is disabled
                return;

            // Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
            pressedTime = 0;

            if(buttonClickedAction !=null)    // Perform the action if there is one
                buttonClickedAction.actionPerformed(new ActionEvent(PopupButton.this, ActionEvent.ACTION_PERFORMED, "clicked"));
            else                // No action, popup menu
                popupMenu();
        }

        public synchronized void mouseReleased(MouseEvent mouseEvent) {
            // Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
            pressedTime = 0;
        }

        public synchronized void mouseExited(MouseEvent mouseEvent) {
            // Indicate to Thread spawn by mousePressed that mouse is not pressed anymore
            pressedTime = 0;
        }

        public void mouseEntered(MouseEvent mouseEvent) {
        }


        /////////////////////////////
        // Runnable implementation //
        /////////////////////////////

        public void run() {
            try { Thread.sleep(POPUP_DELAY); }
            catch(InterruptedException e) {}

            synchronized(this) {
                // Popup menu if a popup menu is not already being displayed and if mouse is still pressed
                if(!isPopupMenuVisible() && pressedTime!=0 && System.currentTimeMillis()-pressedTime>=POPUP_DELAY) {
                    popupMenu();
                }
            }
        }
    }
}
