/*
 * ActionGUIFactory.java  $Revision: 1.27 $ $Date: 2007/10/03 04:59:32 $
 *
 * Copyright (C) 2007 Meldcraft Corporation
 *
 * This file is part of Scaffold.
 * 
 * Scaffold is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * Scaffold 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 Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with Scaffold.  If not, see <http://www.gnu.org/licenses/>.
 */

package com.meldcraft.application.swing.action;

import java.awt.Component;
import java.awt.Container;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ItemListener;
import java.beans.PropertyChangeListener;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.StringTokenizer;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.*;
import javax.swing.event.ChangeListener;
import javax.swing.tree.DefaultMutableTreeNode;
import javax.swing.tree.TreeNode;

import com.meldcraft.application.ApplicationContext;
import com.meldcraft.application.ApplicationContextEvent;
import com.meldcraft.application.ApplicationContextListener;
import com.meldcraft.application.ManagedElementFactory;
import com.meldcraft.application.swing.SwingApplicationConstants;
import com.meldcraft.application.swing.SwingApplicationContext;
import com.meldcraft.application.swing.action.ActionFactory.NameMnemonic;
import com.meldcraft.gui.swing.SwingUtil;
import com.meldcraft.util.ConfigurationManager;
import com.meldcraft.util.PropertiesHelper;
import com.meldcraft.util.ReflectUtil;

/**
 * @version $Revision: 1.27 $ $Date: 2007/10/03 04:59:32 $
 * @author Rob Richards
 */
public class ActionGUIFactory
{
    // Static variables
    public static final String      ACTION_CONFIGURATION_KEY = "Application.Action.configuration";
    private static final Container  EMPTY_CONTAINER = new Container();
    private static Map              sFactoryMap = new HashMap();
    
// Member variables
    protected Map                       mRadioGroups = new HashMap();
    protected Map                       mCheckBoxGroups = new HashMap();
    protected SwingApplicationContext   mApplicationContext;

// Constructors and initialization
    public ActionGUIFactory(ApplicationContext context)
    {
        mApplicationContext = (SwingApplicationContext)context;
    }

// Interface implemenations
// Public methods
    public static ActionGUIFactory getInstance(SwingApplicationContext context)
    {
        ActionGUIFactory ret;

        synchronized(sFactoryMap)
        {
            ret = (ActionGUIFactory)sFactoryMap.get(context);

            if (ret == null)
            {
                Map config = context.getConfiguration();
                String className = PropertiesHelper.getString(config, "ActionGUIFactory.class", null);
                if (className == null)
                {
                    config = ConfigurationManager.getConfiguration(ActionGUIFactory.class);
                    className = PropertiesHelper.getString(config, "ActionGUIFactory.class", null);
                }

                try
                {
                    if (className == null)
                    {
                        ret = new ActionGUIFactory(context);
                    }
                    else
                    {
                        ret = (ActionGUIFactory)ReflectUtil.createInstance(className, ApplicationContext.class, context);
                    }

                    sFactoryMap.put(context, ret);
                }
                catch (Throwable e)
                {
                    throw new RuntimeException("Error creating ActionGUIFactory of class " + className, e);
                }
            }
        }

        return ret;
    }

    public JMenuBar createJMenuBar(String menuKey)
    {
        JMenuBar bar = new JMenuBar();
        configureJMenuBar(bar, menuKey);
        return bar;
    }

    public void configureJMenuBar(JMenuBar bar, String menuKey)
    {
        Map config = mApplicationContext.getConfiguration();

        String items = PropertiesHelper.getString(config, menuKey, null);
        if (items != null)
        {
            StringTokenizer tok = new StringTokenizer(items, " ");
            while (tok.hasMoreTokens())
            {
                String menuName = tok.nextToken();
                String name = PropertiesHelper.getString(config, menuName+"."+SwingApplicationConstants.ACTION_NAME, menuName);
                NameMnemonic nm = ActionFactory.getNameMnemonic(name);

                JMenu menu = new JMenu(nm.getName());
                if (nm.getMnemonic() != null)
                {
                    menu.setMnemonic(nm.getMnemonic().intValue());
                }

                if (nm.getMnemonicIndex() != null)
                {
                    menu.setDisplayedMnemonicIndex(nm.getMnemonicIndex().intValue());
                }

                bar.add(menu);
                addItems(menuName+"."+SwingApplicationConstants.MENU_ITEMS, menu);

                if (menu.getItemCount() == 0)
                {
                    menu.setEnabled(false);
                }
            }
        }
    }

    public JToolBar createJToolBar(String toolbarKey)
    {
        JToolBar bar = new JToolBar();
        configureJToolBar(bar, toolbarKey);
        if (bar.getComponents().length == 0)
        {
            bar = null;
        }
        return bar;
    }

    public void configureJToolBar(JToolBar bar, String toolbarKey)
    {
        addItems(toolbarKey, bar);
    }

    public JPopupMenu createJPopupMenu(String popupKey)
    {
        JPopupMenu menu = new JPopupMenu();
        configureJPopupMenu(menu, popupKey);
        if (menu.getComponents().length == 0)
        {
            menu = null;
        }
        return menu;
    }

    public  void configureJPopupMenu(JPopupMenu menu, String popupKey)
    {
        addItems(popupKey, menu);
    }

    public Component createButton(String itemName)
    {
        return createButton(itemName, null);
    }
    
    public Component createButton(String itemName, Map overrides)
    {
        Component ret = null;

        ActionManager aman = mApplicationContext.getActionManager();
        Map config = mApplicationContext.getConfiguration();
        Logger logger = mApplicationContext.getLogger();
        Action action = aman.getAction(itemName);
        if (action != null)
        {
            ret = createActionItem(EMPTY_CONTAINER, config, itemName, action, overrides);
        }
        else
        {
            logger.log(Level.WARNING, "No action for "+itemName+".");
        }

        return ret;
    }

    public void addButtons(Container container, String containerKey)
    {
        addItems(containerKey, container);
    }

    public TreeNode getActionHierarchy(String name)
    {
        DefaultMutableTreeNode root = new DefaultMutableTreeNode(name);
        addItems(name, root);
        return root;
    }

// Protected methods
    protected void addItems(String baseName, Object actionContainer)
    {
        // TODO: use action tree
        ActionManager aman = mApplicationContext.getActionManager();
        Map config = mApplicationContext.getConfiguration();
        Logger logger = mApplicationContext.getLogger();

        String items = PropertiesHelper.getString(config, baseName, null);
        if (items != null)
        {
            StringTokenizer tok = new StringTokenizer(items, " ");
            while (tok.hasMoreTokens())
            {
                String itemName = tok.nextToken();
                if (itemName.equals("|"))
                {
                    if (actionContainer instanceof JMenu)
                    {
                        ((JMenu)actionContainer).addSeparator();
                    }
                    else if (actionContainer instanceof JPopupMenu)
                    {
                        ((JPopupMenu)actionContainer).addSeparator();
                    }
                    else if (actionContainer instanceof JToolBar)
                    {
                        ((JToolBar)actionContainer).addSeparator();
                    }
                    else if (actionContainer instanceof Container)
                    {
                        ((Container)actionContainer).add(new JSeparator());
                    }
                    else
                    {
                        ((DefaultMutableTreeNode)actionContainer).add(new DefaultMutableTreeNode());
                    }
                }
                else
                {
                    Action action = aman.getAction(itemName);
                    if (action != null)
                    {
                        if (actionContainer instanceof JToolBar)
                        {
                            addToolBarItem((JToolBar)actionContainer, config, itemName, action);
                        }
                        else if ((actionContainer instanceof JMenu)  ||
                                 (actionContainer instanceof JPopupMenu))
                        {
                            JMenuItem item = createMenuItem(config, itemName, action);
                            
                            if (actionContainer instanceof JMenu)
                            {
                                ((JMenu)actionContainer).add(item);
                            }
                            else
                            {
                                ((JPopupMenu)actionContainer).add(item);
                            }
                        }
                        else if (actionContainer instanceof Container)
                        {
                            addContainerItem((Container)actionContainer, config, itemName, action);
                        }
                        else
                        {
                            DefaultMutableTreeNode node = new DefaultMutableTreeNode(action);
                            ((DefaultMutableTreeNode)actionContainer).add(node);
                        }
                    }
                    else
                    {
                        logger.log(Level.WARNING, "No action for " + itemName + ".");
                    }
                }
            }
        }
    }

    protected Component addToolBarItem(JToolBar toolbar, Map config, String itemName, Action action)
    {
        return createActionItem(toolbar, config, itemName, action, null);
    }

    protected Component addContainerItem(Container container, Map config, String itemName, Action action)
    {
        return createActionItem(container, config, itemName, action, null);
    }

    protected JMenuItem createMenuItem(Map config, String itemName, Action action)
    {
        return (JMenuItem)createActionItem(null, config, itemName, action, null);
    }

    protected Component createActionItem(Container toolbar, Map config, String elementId, Action action, Map overrides)
    {
        AbstractButton item = null;

        Map actionConfig;
        if (action instanceof ApplicationAction)
        {
            actionConfig = ((ApplicationAction)action).getApplicationElementContext().getConfiguration();
        }
        else
        {
            Object obj = action.getValue(ACTION_CONFIGURATION_KEY);
            if (obj instanceof Map)
            {
                actionConfig = (Map)obj;
            }
            else
            {
                actionConfig = ActionFactory.getActionConfig(config, elementId);
            }
        }
        
        if (actionConfig == null)
        {
            actionConfig = new HashMap();
        }

        if (overrides != null)
        {
            actionConfig.putAll(overrides);
        }

        Object[] info = getToggleActionInfo(actionConfig);

        boolean toggle = (info != null);
        String toggleGroup = (toggle) ? (String)info[0] : null;
        boolean selected = (toggle) ? ((Boolean)info[1]).booleanValue() : false;

        //System.err.println(toggleGroup + " " + toggle + " " + selected + " " + itemName);

        if (toggleGroup != null)
        {
            ActionGroup group;
            synchronized(mRadioGroups)
            {
                group = (ActionGroup)mRadioGroups.get(toggleGroup);
                if (group == null)
                {
                    group = new ActionGroup(mApplicationContext, toggleGroup);
                    mRadioGroups.put(toggleGroup, group);
                    mApplicationContext.getElementManager().addManagedElement(createActionGroupKey(toggleGroup), group);
                }
            }

            if (toolbar == null)
            {
                item = new JRadioButtonMenuItem(action) {
                    public void setToolTipText(String text)
                    {
                        // do nothing
                    }
                };
                ((JRadioButtonMenuItem)item).setActionCommand(elementId);
            }
            else
            {
                if (PropertiesHelper.getString(actionConfig, "buttonStyle", "").toLowerCase().equals("toggle"))
                {
                    item = createButton(JToggleButton.class, elementId, action, actionConfig, (toolbar instanceof JToolBar));
                }
                else
                {
                    item = createButton(JRadioButton.class, elementId, action, actionConfig, (toolbar instanceof JToolBar));
                }
                ((JToggleButton)item).setActionCommand(elementId);
                if (toolbar != EMPTY_CONTAINER)
                {
                    toolbar.add(item);
                }
            }

            item.setSelected(selected);
            group.addAction(elementId, action, item);
        }
        else if (toggle)
        {
            if (toolbar == null)
            {
                item = new JCheckBoxMenuItem(action) {
                    public void setToolTipText(String text)
                    {
                        // do nothing
                    }
                };
            }
            else
            {
                if (PropertiesHelper.getString(actionConfig, "buttonStyle", "").toLowerCase().equals("toggle"))
                {
                    item = createButton(JToggleButton.class, elementId, action, actionConfig, (toolbar instanceof JToolBar));
                }
                else
                {
                    item = createButton(JCheckBox.class, elementId, action, actionConfig, (toolbar instanceof JToolBar));
                }
                if (toolbar != EMPTY_CONTAINER)
                {
                    toolbar.add(item);
                }
            }

            ButtonModel model;
            synchronized(mCheckBoxGroups)
            {
                model = (ButtonModel)mCheckBoxGroups.get(action);
                if (model == null)
                {
                    model = new ButtonModelWrapper(mApplicationContext, elementId, item.getModel());
                    mCheckBoxGroups.put(action, model);
                }
                else
                {
                    item.setModel(model);
                }
            }
            item.setSelected(selected);
        }
        else
        {
            if (toolbar == null)
            {
                item = new JMenuItem(action) {
                    public void setToolTipText(String text)
                    {
                        // do nothing
                    }
                };
            }
            else
            {
                item = createButton(JButton.class, elementId, action, actionConfig, (toolbar instanceof JToolBar));

                if (toolbar != EMPTY_CONTAINER)
                {
                    toolbar.add(item);
                }
            }
        }

        if ((toolbar == null) && (item instanceof JMenuItem))
        {
            ((JMenuItem)item).setAccelerator((KeyStroke)action.getValue(Action.ACCELERATOR_KEY));

            Object val = action.getValue(SwingApplicationConstants.ACTION_MNEMONIC_INDEX);
            if (val instanceof Integer)
            {
                ((JMenuItem)item).setDisplayedMnemonicIndex(((Integer)val).intValue());
            }
        }

        return item;
    }

    protected String createActionGroupKey(String group)
    {
        return "actionManager.actionGroup(" + group + ")";
    }

    static Object[] getToggleActionInfo(Map config)
    {
        boolean selected = false;
        boolean toggle = false;
        String toggleGroup = null;
        String type = PropertiesHelper.getString(config, "type", null);
        if (type != null)
        {
            String lower = type.toLowerCase();
            if (lower.startsWith("radio"))
            {
                toggle = true;
                if (lower.length() > "radio".length())
                {
                    toggleGroup = type.substring("radio".length(), type.length());
                    lower = toggleGroup.toLowerCase();
                    int idx = lower.lastIndexOf("false");
                    if ((idx > 0) && lower.endsWith("false"))
                    {
                        toggleGroup = toggleGroup.substring(0, idx);
                    }
                    else if (((idx = lower.lastIndexOf("true")) > 0) &&
                            lower.endsWith("true"))
                    {
                        toggleGroup = toggleGroup.substring(0, idx);
                        selected = true;
                    }
                }
            }
            else if (lower.startsWith("checkbox"))
            {
                toggle = true;
                if (lower.endsWith("true"))
                {
                    selected = true;
                }
            }
        }

        Object[] ret = null;
        if (toggle)
        {
            ret = new Object[] {toggleGroup, new Boolean(selected)};
        }
        return ret;
    }
// Private methods
    private AbstractButton createButton(Class clazz, String elementId, Action action, Map actionConfig, boolean toolbar)
    {
        Map overrides = new HashMap(actionConfig);
        overrides.put("className", clazz.getName());
        Map defaults = new HashMap();
        if (toolbar)
        {
            boolean hide = (action.getValue(Action.SMALL_ICON) != null) ||
                           (action.getValue("SwingLargeIconKey") != null);
            String[] props = {
                    "focusable=false",
                    "borderPainted=false",
            };

            PropertiesHelper.insertStringArray(defaults, 0, "elementProperties", props);
            if (! JToggleButton.class.isAssignableFrom(clazz))
            {
                props = new String[] {
                    "verticalTextPosition=BOTTOM",
                    "horizontalTextPosition=CENTER",
                    "hideActionText=" + hide,
                };
                PropertiesHelper.insertStringArray(defaults, 0, "elementProperties", props);
            }
        }
        AbstractButton item = (AbstractButton)ManagedElementFactory.getInstance(mApplicationContext).createManagedElement(elementId, defaults, overrides);
        item.setAction(action);
        Object icon = (Icon)action.getValue(Action.SMALL_ICON + "p");
        if (icon instanceof Icon)
        {
            item.setPressedIcon((Icon)icon);
        }
        icon = (Icon)action.getValue(Action.SMALL_ICON + "r");
        if (icon instanceof Icon)
        {
            item.setRolloverIcon((Icon)icon);
        }
        icon = (Icon)action.getValue(Action.SMALL_ICON + "s");
        if (icon instanceof Icon)
        {
            item.setSelectedIcon((Icon)icon);
        }

        KeyStroke[] accels = (KeyStroke[])action.getValue(Action.ACCELERATOR_KEY + "s");
        if (accels != null)
        {
            for (int i=0 ; i < accels.length ; i++)
            {
                KeyStroke accelerator = accels[i];
                if (accelerator != null)
                {
                    item.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(accelerator, "doClick");
                }
            }
            item.getActionMap().put("doClick", new ClickWrapper((AbstractButton)item, action));
        }
        else
        {
            KeyStroke accelerator = (KeyStroke)action.getValue(Action.ACCELERATOR_KEY);
            if (accelerator != null)
            {
                item.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(accelerator, "doClick");
                item.getActionMap().put("doClick",new ClickWrapper((AbstractButton)item, action));
            }
        }

        // set mnemonic index
        Object val = action.getValue(SwingApplicationConstants.ACTION_MNEMONIC_INDEX);
        if (val instanceof Integer)
        {
            try
            {
                item.setDisplayedMnemonicIndex(((Integer)val).intValue());
            }
            catch (IllegalArgumentException e)
            {
                // bad mnemonic index - oh well
            }
        }

        return item;
    }

// Inner classes
    public static class ActionGroup extends ButtonGroup implements ApplicationContextListener
    {
        private static ThreadLocal  sLocal = new ThreadLocal();
        protected boolean   mRadio;
        protected HashMap   mActions = new HashMap();
        private String      mToggleGroup;
        private SwingApplicationContext mApplicationContext;

        public ActionGroup(SwingApplicationContext applicationContext, String toggleGroup)
        {
            this(applicationContext, toggleGroup, true);
            // listen for messages from other sources
            applicationContext.addApplicationContextListener(this, toggleGroup + ".selectValue");
        }

        public ActionGroup(SwingApplicationContext applicationContext, String toggleGroup, boolean radio)
        {
            mRadio = radio;
            mApplicationContext = applicationContext;
            mToggleGroup = toggleGroup;
        }

        public void applicationContextChanged(ApplicationContextEvent e)
        {
            final Object eval = e.getValue();
            Runnable run = new Runnable() {
                public void run()
                {
                    sLocal.set(new Object());
                    try
                    {
                        if (eval == null)
                        {
                            ActionGroup.super.clearSelection();
                        }
                        else
                        {
                            Action action = null;
                            if (eval instanceof String)
                            {
                                Iterator iter = getActions();
                                while (iter.hasNext())
                                {
                                    Action ac = (Action)iter.next();
                                    Object val = ac.getValue(ActionFactory.ELEMENT_ID_KEY);
                                    if (val == null)
                                    {
                                        val = ac.getValue(Action.NAME);
                                    }
                                    if (val instanceof String)
                                    {
                                        if (((String)val).equals(eval))
                                        {
                                            action = ac;
                                            break;
                                        }
                                    }
                                }
                            }

                            if (action != null)
                            {
                                //setSelected((ButtonModel)mActions.get(action), true);
                                ButtonModel model = (ButtonModel)mActions.get(action);
                                if (model instanceof ButtonModelWrapper)
                                {
                                    model = ((ButtonModelWrapper)model).mDelegate;
                                }
                                if (!isSelected(model))
                                {
                                    ActionGroup.super.setSelected(model, true);
                                }
                            }
                        }
                    }
                    finally
                    {
                        sLocal.set(null);
                    }
                }
            };
            SwingUtil.waitForInvokeLater(run);
        }

        @Override
        public void setSelected(ButtonModel model, boolean selected)
        {
            if (sLocal.get() == null)
            {
                boolean isSelected = isSelected(model);
                boolean sendEvent = (selected && isSelected);
                super.setSelected(model, selected);
                if (sendEvent)
                {
                    mApplicationContext.contextChanged(mToggleGroup + ".selectValue", model.getActionCommand());
                }
            }
        }

        public void addAction(String elementId, Action action, AbstractButton button)
        {
            ButtonModel shared = (ButtonModel)mActions.get(action);
            if (shared == null)
            {
                if (mRadio)
                {
                    super.add(button);
                }
                shared = new ButtonModelWrapper(mApplicationContext, elementId, button.getModel());
                mActions.put(action, shared);
            }
            else
            {
                button.setModel(shared);
            }
        }

        public void setEnabled(boolean enabled)
        {
            Object[] actions = mActions.keySet().toArray();
            if ((actions != null) && (actions.length > 0))
            {
                for (int i=0 ; i < actions.length ; i++)
                {
                    ((Action)actions[i]).setEnabled(enabled);
                }
            }
        }

        public Iterator getActions()
        {
            return mActions.keySet().iterator();
        }
    }

    public static class ButtonModelWrapper implements ButtonModel, ApplicationContextListener
    {
        private ButtonModel     mDelegate;
        public ButtonModelWrapper(SwingApplicationContext applicationContext, String elementId, ButtonModel model)
        {
            mDelegate = model;
            applicationContext.addApplicationContextListener(this, elementId + ".select");
        }

        public void applicationContextChanged(ApplicationContextEvent e)
        {
            boolean select = PropertiesHelper.getBoolean(e.getValue(), false);
            if (select != isSelected())
            {
                setSelected(select);
            }
        }

        public void addActionListener(ActionListener l)
        {
            // ignored
            //mDelegate.addActionListener(l);
        }
        public void addChangeListener(ChangeListener l)
        {
            mDelegate.addChangeListener(l);
        }
        public void addItemListener(ItemListener l)
        {
            mDelegate.addItemListener(l);
        }
        public String getActionCommand()
        {
            return mDelegate.getActionCommand();
        }
        public int getMnemonic()
        {
            return mDelegate.getMnemonic();
        }
        public Object[] getSelectedObjects()
        {
            return mDelegate.getSelectedObjects();
        }
        public boolean isArmed()
        {
            return mDelegate.isArmed();
        }
        public boolean isEnabled()
        {
            return mDelegate.isEnabled();
        }
        public boolean isPressed()
        {
            return mDelegate.isPressed();
        }
        public boolean isRollover()
        {
            return mDelegate.isRollover();
        }
        public boolean isSelected()
        {
            return mDelegate.isSelected();
        }
        public void removeActionListener(ActionListener l)
        {
            mDelegate.removeActionListener(l);
        }
        public void removeChangeListener(ChangeListener l)
        {
            mDelegate.removeChangeListener(l);
        }
        public void removeItemListener(ItemListener l)
        {
            mDelegate.removeItemListener(l);
        }
        public void setActionCommand(String s)
        {
            mDelegate.setActionCommand(s);
        }
        public void setArmed(boolean b)
        {
            mDelegate.setArmed(b);
        }
        public void setEnabled(boolean b)
        {
            mDelegate.setEnabled(b);
        }
        public void setGroup(ButtonGroup group)
        {
            mDelegate.setGroup(group);
        }
        public void setMnemonic(int key)
        {
            mDelegate.setMnemonic(key);
        }
        public void setPressed(boolean b)
        {
            mDelegate.setPressed(b);
        }
        public void setRollover(boolean b)
        {
            mDelegate.setRollover(b);
        }
        public void setSelected(boolean b)
        {
            mDelegate.setSelected(b);
        }
    }

    public static class ClickWrapper implements Action
    {
        private AbstractButton  mButton;
        private Action          mAction;
        public ClickWrapper(AbstractButton button, Action action)
        {
            mButton = button;
            mAction = action;
        }
        public void actionPerformed(ActionEvent e)
        {
            mButton.doClick();
        }
        public void addPropertyChangeListener(PropertyChangeListener listener)
        {
            mAction.addPropertyChangeListener(listener);
        }
        public Object getValue(String key)
        {
            return mAction.getValue(key);
        }
        public boolean isEnabled()
        {
            return mAction.isEnabled();
        }
        public void putValue(String key, Object value)
        {
            mAction.putValue(key, value);
        }
        public void removePropertyChangeListener(PropertyChangeListener listener)
        {
            mAction.removePropertyChangeListener(listener);
        }
        public void setEnabled(boolean b)
        {
            mAction.setEnabled(b);
        }

    }
}
