/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.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.apache.org/licenses/LICENSE-2.0
 *
 * 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.
 */
package org.netbeans.spi.actions;

import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import org.openide.util.Lookup;
import org.openide.util.Parameters;

/**
 * Merges multiple NbActions or their stubs.
 *
 * @author Tim Boudreau
 */
final class MergeAction extends NbAction implements PropertyChangeListener {
    private NbAction[] actions;
    private Map<String, Object> knownValues = new HashMap<String, Object>();
    private Action delegateAction;
    private volatile boolean enabled;
    final boolean allowOnlyOne;

    public MergeAction(NbAction[] actions, boolean allowOnlyOne) {
        this.actions = actions;
        this.allowOnlyOne = allowOnlyOne;
        assert actions.length > 0;
        assert new HashSet<Action>(Arrays.asList(actions)).size() == actions.length :
            "Duplicate actions in " + Arrays.asList(actions);
        for (int i = 0; i < actions.length; i++) {
            Parameters.notNull("Action " + i, actions[i]); //NOI18N
        }
        //prime our key set common keys
        knownValues.put(NAME, null);
        knownValues.put(ACCELERATOR_KEY, null);
        knownValues.put(LONG_DESCRIPTION, null);
        knownValues.put(SMALL_ICON, null);
        knownValues.put(SHORT_DESCRIPTION, null);
        knownValues.put(LONG_DESCRIPTION, null);
        knownValues.put(SMALL_ICON, null);
        knownValues.put(MNEMONIC_KEY, null);
        knownValues.put("noIconInMenu", null);
        knownValues.put(PROP_ENABLED, null);
    }

    public MergeAction(NbAction[] actions) {
        this (actions, false);
    }

    @Override
    public boolean equals(Object o) {
        return o != null && MergeAction.class == o.getClass() && Arrays.equals(((MergeAction)o).actions, actions);
    }

    @Override
    public int hashCode() {
        return Arrays.hashCode(actions);
    }

    Action updateDelegateAction() {
        synchronized (this) {
            return setDelegateAction(findEnabledAction());
        }
    }

    Action setDelegateAction(Action a) {
        assert Thread.holdsLock(this);
        synchronized (this) {
            Action old = this.delegateAction;
            if (old != a) {
                delegateAction = a;
                boolean nowEnabled = getDelegateAction().isEnabled();
                if (nowEnabled != enabled) {
                    enabled = nowEnabled;
                    firePropertyChange(PROP_ENABLED, !enabled, enabled);
                }
                if (a != null) {
                    sievePropertyChanges();
                }
            }
        }
        return a;
    }

    Action findEnabledAction() {
        Action result = null;
        int enaCount = 0;
        //We want to, if necessary, briefly addNotify() the action,
        //so run it inside an ActionRunnable.
        ActionRunnable<Boolean> ar = new ActionRunnable<Boolean>() {

            public Boolean run(NbAction a) {
                return a.isEnabled();
            }
        };
        for (NbAction a : actions) {
            if (runActive(ar, a)) {
                enaCount++;
                if (!allowOnlyOne) {
                    result = a;
                    break;
                } else if (result == null) {
                    result = a;
                }
            }
        }
        if (allowOnlyOne && enaCount > 1 && result != null) {
            result = null;
        }
        return result;
    }

    Action getDelegateAction() {
        Action result = null;
        synchronized (this) {
            result = delegateAction;
            if (result == null || !result.isEnabled()) {
                if (attached()) {
                    result = updateDelegateAction();
                } else {
                    result = findEnabledAction();
                }
            }
        }
        if (result == null) {
            result = actions[0];
        }
        return result;
    }

    private void sievePropertyChanges() {
        Map<String, Object> nue = new HashMap<String, Object>();
        for (Map.Entry<String, Object> entry : knownValues.entrySet()) {
            String key = entry.getKey();
            Object expected = entry.getValue();
            Object found = getValue(key);//del.getValue(key);
            if (found != expected) {
                nue.put(key, found);
                firePropertyChange(key, expected, found);
            }
        }
    }

    @Override
    protected synchronized void addNotify() {
        for (Action a : actions) {
            a.addPropertyChangeListener(this);
        }
        updateDelegateAction();
    }

    @Override
    protected synchronized void removeNotify() {
        for (Action a : actions) {
            a.removePropertyChangeListener(this);
        }
        setDelegateAction(null);
    }

    @Override
    protected NbAction internalCreateContextAwareInstance(Lookup actionContext) {
        NbAction[] stubs = new NbAction[actions.length];
        for (int i = 0; i < stubs.length; i++) {
            stubs[i] = (NbAction)
                    actions[i].createContextAwareInstance(actionContext);
        }
        MergeAction result = new MergeAction(stubs, allowOnlyOne);
        result.knownValues.putAll(knownValues);
        result.pairs.putAll(pairs);
        return result;
    }

    @Override
    public Object getValue(final String key) {
        Object result = super.getValue(key);
        if (result == null) {
            if (isEnabled()) {
                Action del = getDelegateAction();
                result = del.getValue(key);
            }
        }
        if (result == null) {
            ActionRunnable<Object> ar = new ActionRunnable<Object>() {
                public Object run(NbAction a) {
                    return a.getValue(key);
                }
            };
            for (NbAction a : actions) {
                result = runActive(ar, a);
                if (result != null) {
                    break;
                }
            }
        }
        this.knownValues.put(key, result);
        return result;
    }
    boolean logged;

    @Override
    public void putValue(String key, Object value) {
        if (!logged) {
            Logger.getLogger(MergeAction.class.getName()).log(Level.INFO,
                    "putValue (" + key + ',' + value + //NOI18N
                    "called on merged action.  This is probably a mistake."); //NOI18N
        }
        super.putValue(key, value);
    }

    public boolean isEnabled() {
        boolean result = updateEnabled();
        return result;
    }

    boolean updateEnabled() {
        enabled = getDelegateAction().isEnabled();
        if (allowOnlyOne && enabled) {
            if (findEnabledAction() == null) {
                enabled = false;
            }
        }
        return enabled;
    }

    public void actionPerformed(ActionEvent e) {
        Action a = getDelegateAction();
        if (a == null) {
            throw new IllegalStateException("Not enabled or no delegate: " + //NOI18N
                    this);
        }
        a.actionPerformed(e);
    }

    public void propertyChange(PropertyChangeEvent evt) {
        boolean old = enabled;
        synchronized (this) {
            if (attached()) {
                updateDelegateAction();
            }
        }
        boolean nowEnabled = isEnabled();
        if (!PROP_ENABLED.equals(evt.getPropertyName())) {
            Object last = knownValues.get(evt.getPropertyName());
            Object mine = getValue(evt.getPropertyName());
            if (mine != last) {
                firePropertyChange(evt.getPropertyName(), last, mine);
            }
            knownValues.put(evt.getPropertyName(), evt.getNewValue());
        }
        if (old != nowEnabled) {
            firePropertyChange(PROP_ENABLED, old, nowEnabled);
        }
    }

    @Override
    public String toString() {
        StringBuilder sb = new StringBuilder (super.toString());
        sb.append ('['); //NOI18N
        for (int i = 0; i < actions.length; i++) {
            sb.append (actions[i]);
            if (i != actions.length - 1) {
                sb.append (','); //NOI18N
            }
        }
        sb.append(']'); //NOI18N
        return sb.toString();
    }
}
