/*
 * Copyright 2010-2015 Institut Pasteur.
 * 
 * This file is part of Icy.
 * 
 * Icy 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.
 * 
 * Icy 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 Icy. If not, see <http://www.gnu.org/licenses/>.
 */
package icy.action;

import icy.gui.frame.progress.ProgressFrame;
import icy.main.Icy;
import icy.resource.icon.IcyIcon;
import icy.system.thread.ThreadUtil;
import icy.util.StringUtil;

import java.awt.event.ActionEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.KeyStroke;

import org.pushingpixels.flamingo.api.common.RichTooltip;

/**
 * Icy basic AbstractAction class.
 * 
 * @author Stephane
 */
public abstract class IcyAbstractAction extends AbstractAction
{
    /**
     * Sets the tooltip text of a component from an Action.
     * 
     * @param c
     *        the Component to set the tooltip text on
     * @param a
     *        the Action to set the tooltip text from, may be null
     */
    public static void setToolTipTextFromAction(JComponent c, Action a)
    {
        if (a != null)
        {
            final String longDesc = (String) a.getValue(Action.LONG_DESCRIPTION);
            final String shortDesc = (String) a.getValue(Action.SHORT_DESCRIPTION);

            if (StringUtil.isEmpty(longDesc))
                c.setToolTipText(shortDesc);
            else
                c.setToolTipText(longDesc);
        }
    }

    /**
     * 
     */
    private static final long serialVersionUID = -8544059445777661407L;

    private static final int DEFAULT_ICON_SIZE = 20;

    /**
     * The "enabled" property key.
     */
    public static final String ENABLED_KEY = "enabled";

    /**
     * internals
     */
    protected boolean bgProcess;
    protected boolean processing;
    protected String processMessage;
    protected ProgressFrame progressFrame;

    public IcyAbstractAction(String name, IcyIcon icon, String description, String longDescription, int keyCode,
            int modifiers, boolean bgProcess, String processMessage)
    {
        super(name, icon);

        // by default we use the name as Action Command
        putValue(ACTION_COMMAND_KEY, name);
        if (keyCode != 0)
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(keyCode, modifiers));
        if (!StringUtil.isEmpty(description))
            putValue(SHORT_DESCRIPTION, description);
        if (!StringUtil.isEmpty(longDescription))
            putValue(LONG_DESCRIPTION, longDescription);

        this.bgProcess = bgProcess;
        this.processMessage = processMessage;
        progressFrame = null;
        processing = false;
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, String longDescription, int keyCode,
            int modifiers)
    {
        this(name, icon, description, longDescription, keyCode, modifiers, false, null);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, String longDescription, boolean bgProcess,
            String processMessage)
    {
        this(name, icon, description, longDescription, 0, 0, bgProcess, processMessage);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, boolean bgProcess, String processMessage)
    {
        this(name, icon, description, null, 0, 0, bgProcess, processMessage);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, int keyCode, int modifiers)
    {
        this(name, icon, description, null, keyCode, modifiers, false, null);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, int keyCode)
    {
        this(name, icon, description, null, keyCode, 0, false, null);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description, String longDescription)
    {
        this(name, icon, description, longDescription, 0, 0, false, null);
    }

    public IcyAbstractAction(String name, IcyIcon icon, String description)
    {
        this(name, icon, description, null, 0, 0, false, null);
    }

    public IcyAbstractAction(String name, IcyIcon icon)
    {
        this(name, icon, null, null, 0, 0, false, null);
    }

    public IcyAbstractAction(String name)
    {
        this(name, null, null, null, 0, 0, false, null);
    }

    /**
     * @deprecated Use {@link #IcyAbstractAction(String, IcyIcon, String, int, int)} instead.
     */
    @Deprecated
    public IcyAbstractAction(String name, String iconName, String description, int keyCode, int modifiers)
    {
        this(name, new IcyIcon(iconName, DEFAULT_ICON_SIZE), description, null, keyCode, modifiers, false, null);
    }

    /**
     * @deprecated Use {@link #IcyAbstractAction(String, IcyIcon, String, int)} instead.
     */
    @Deprecated
    public IcyAbstractAction(String name, String iconName, String description, int keyCode)
    {
        this(name, new IcyIcon(iconName, DEFAULT_ICON_SIZE), description, null, keyCode, 0, false, null);
    }

    /**
     * @deprecated Use {@link #IcyAbstractAction(String, IcyIcon, String)} instead.
     */
    @Deprecated
    public IcyAbstractAction(String name, String iconName, String description)
    {
        this(name, new IcyIcon(iconName, DEFAULT_ICON_SIZE), description, null, 0, 0, false, null);
    }

    /**
     * @deprecated Use {@link #IcyAbstractAction(String, IcyIcon)} instead.
     */
    @Deprecated
    public IcyAbstractAction(String name, String iconName)
    {
        this(name, new IcyIcon(iconName, DEFAULT_ICON_SIZE), null, null, 0, 0, false, null);
    }

    /**
     * @return true if this action process is done in a background thread.
     */
    public boolean isBgProcess()
    {
        return bgProcess;
    }

    /**
     * Set to true if you want to action to be processed in a background thread.
     * 
     * @see #isBgProcess()
     * @see #setProcessMessage(String)
     */
    public void setBgProcess(boolean bgProcess)
    {
        this.bgProcess = bgProcess;
    }

    /**
     * @return the process message to display for background action process.
     * @see #setProcessMessage(String)
     * @see #isBgProcess()
     */
    public String getProcessMessage()
    {
        return processMessage;
    }

    public RichTooltip getRichToolTip()
    {
        final String desc = getDescription();
        final String longDesc = getLongDescription();
        final IcyIcon icon = getIcon();

        if (StringUtil.isEmpty(desc) && StringUtil.isEmpty(longDesc))
            return null;

        final RichTooltip result = new RichTooltip();

        if (!StringUtil.isEmpty(desc))
            result.setTitle(desc);

        if (!StringUtil.isEmpty(longDesc))
        {
            for (String ld : longDesc.split("\n"))
                result.addDescriptionSection(ld);
        }

        if (icon != null)
            result.setMainImage(icon.getImage());

        return result;
    }

    /**
     * Set the process message to display for background action process.<br>
     * If set to null then no message is displayed (default).
     * 
     * @see #setBgProcess(boolean)
     */
    public void setProcessMessage(String processMessage)
    {
        this.processMessage = processMessage;
    }

    public void setName(String value)
    {
        putValue(Action.NAME, value);
    }

    public String getName()
    {
        return (String) getValue(Action.NAME);
    }

    public void setIcon(IcyIcon value)
    {
        putValue(Action.SMALL_ICON, value);
    }

    public IcyIcon getIcon()
    {
        return (IcyIcon) getValue(Action.SMALL_ICON);
    }

    public void setDescription(String value)
    {
        putValue(Action.SHORT_DESCRIPTION, value);
    }

    public String getDescription()
    {
        return (String) getValue(Action.SHORT_DESCRIPTION);
    }

    public void setLongDescription(String value)
    {
        putValue(Action.LONG_DESCRIPTION, value);
    }

    public String getLongDescription()
    {
        return (String) getValue(Action.LONG_DESCRIPTION);
    }

    public void setAccelerator(int keyCode, int modifiers)
    {
        if (keyCode != 0)
            putValue(ACCELERATOR_KEY, KeyStroke.getKeyStroke(keyCode, modifiers));
        else
            putValue(ACCELERATOR_KEY, null);
    }

    public void setAccelerator(int keyCode)
    {
        setAccelerator(keyCode, 0);
    }

    /**
     * Returns the selected state (for toggle button type).
     */
    public boolean isSelected()
    {
        return Boolean.TRUE.equals(getValue(SELECTED_KEY));
    }

    /**
     * Sets the selected state (for toggle button type).
     */
    public void setSelected(boolean value)
    {
        putValue(SELECTED_KEY, Boolean.valueOf(value));
    }

    /**
     * Returns the {@link KeyStroke} for this action (can be null).
     */
    public KeyStroke getKeyStroke()
    {
        return (KeyStroke) getValue(ACCELERATOR_KEY);
    }

    /**
     * @return true if action is currently processing.<br>
     *         Meaningful only when {@link #setBgProcess(boolean)} is set to true)
     */
    public boolean isProcessing()
    {
        return processing;
    }

    @Override
    public boolean isEnabled()
    {
        return enabled && !processing;
    }

    @Override
    public void setEnabled(boolean value)
    {
        if (enabled != value)
        {
            final boolean wasEnabled = isEnabled();
            enabled = value;
            final boolean isEnabled = isEnabled();

            // notify enabled change
            if (wasEnabled != isEnabled)
                firePropertyChange(ENABLED_KEY, Boolean.valueOf(wasEnabled), Boolean.valueOf(isEnabled));
        }
    }

    protected void setProcessing(boolean value)
    {
        if (processing != value)
        {
            final boolean wasEnabled = isEnabled();
            processing = value;
            final boolean isEnabled = isEnabled();

            // notify enabled change
            if (wasEnabled != isEnabled)
                firePropertyChange(ENABLED_KEY, Boolean.valueOf(wasEnabled), Boolean.valueOf(isEnabled));
        }
    }

    /**
     * Helper method to fire enabled changed event (this force component refresh)
     */
    public void enabledChanged()
    {
        final boolean enabledState = isEnabled();

        // notify enabled change
        firePropertyChange(ENABLED_KEY, Boolean.valueOf(!enabledState), Boolean.valueOf(enabledState));
    }

    /**
     * Returns a {@link JLabel} component representing the action.
     */
    public JLabel getLabelComponent(boolean wantIcon, boolean wantText)
    {
        final JLabel result = new JLabel();

        if (wantIcon)
            result.setIcon(getIcon());
        if (wantText)
            result.setText(getName());

        final String desc = getDescription();

        if (StringUtil.isEmpty(desc))
            result.setToolTipText(getLongDescription());
        else
            result.setToolTipText(getDescription());

        return result;
    }

    /**
     * Returns a {@link JLabel} component representing the action.
     */
    public JLabel getLabelComponent()
    {
        return getLabelComponent(true, true);
    }

    @Override
    public void actionPerformed(ActionEvent e)
    {
        setProcessing(true);

        // background execution ?
        if (isBgProcess())
        {
            final ActionEvent event = e;

            ThreadUtil.bgRun(new Runnable()
            {
                @Override
                public void run()
                {
                    final String mess = getProcessMessage();

                    if (!StringUtil.isEmpty(mess) && !Icy.getMainInterface().isHeadLess())
                        progressFrame = new ProgressFrame(mess);
                    else
                        progressFrame = null;

                    try
                    {
                        doAction(event);
                    }
                    finally
                    {
                        if (progressFrame != null)
                            progressFrame.close();

                        // need to be done on the EDT (can change the enabled state)
                        ThreadUtil.invokeLater(new Runnable()
                        {
                            @Override
                            public void run()
                            {
                                setProcessing(false);
                            }
                        });
                    }
                }
            });
        }
        else
        {
            try
            {
                doAction(e);
            }
            finally
            {
                setProcessing(false);
            }
        }
    }

    /**
     * Execute action (delayed execution if action requires it)
     */
    public void execute()
    {
        actionPerformed(new ActionEvent(this, 0, ""));
    }

    /**
     * @deprecated Use {@link #executeNow()} instead
     */
    @Deprecated
    public boolean doAction()
    {
        return doAction(new ActionEvent(this, 0, ""));
    }

    /**
     * Execute action now (wait for execution to complete)
     */
    public boolean executeNow()
    {
        return doAction(new ActionEvent(this, 0, ""));
    }

    /**
     * Action implementation
     */
    protected abstract boolean doAction(ActionEvent e);
}