/*******************************************************************************
 * Copyright (c) 2007, 2015 IBM Corporation and others.
 *
 * This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License 2.0
 * which accompanies this distribution, and is available at
 * https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 *
 * Contributors:
 *     IBM Corporation - initial API and implementation
 *******************************************************************************/

package org.eclipse.ui.internal.keys.model;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.BindingManager;
import org.eclipse.jface.bindings.TriggerSequence;

/**
 * @since 3.4
 *
 */
public class ConflictModel extends CommonModel {

	public static final String PROP_CONFLICTS = "conflicts"; //$NON-NLS-1$
	public static final String PROP_CONFLICTS_ADD = "conflictsAdd"; //$NON-NLS-1$
	public static final String PROP_CONFLICTS_REMOVE = "conflictsRemove"; //$NON-NLS-1$

	/**
	 * The set of conflicts for the currently selected element.
	 */
	private Collection<?> conflicts;

	private BindingManager bindingManager;

	private BindingModel bindingModel;

	/**
	 * A mapping of binding element to known conflicts.
	 */
	private Map<BindingElement, Collection<BindingElement>> conflictsMap;

	/**
	 * @param kc
	 */
	public ConflictModel(KeyController kc) {
		super(kc);
	}

	/**
	 * @return Returns the conflicts.
	 */
	public Collection<?> getConflicts() {
		return conflicts;
	}

	/**
	 * Sets the conflicts to the given collection. Any conflicts in the collection
	 * that do not exist in the <code>bindingModel</code> are removed.
	 *
	 * @param conflicts The conflicts to set.
	 */
	public void setConflicts(Collection<?> conflicts) {
		Object old = this.conflicts;
		this.conflicts = conflicts;

		if (this.conflicts != null) {
			Iterator<?> i = this.conflicts.iterator();
			Map<Binding, BindingElement> bindingToElement = bindingModel.getBindingToElement();
			while (i.hasNext()) {
				Object next = i.next();
				if (!bindingToElement.containsValue(next) && !next.equals(getSelectedElement())) {
					i.remove();
				}
			}
		}

		controller.firePropertyChange(this, PROP_CONFLICTS, old, conflicts);
	}

	public void updateConflictsFor(BindingElement source) {
		updateConflictsFor(source, false);
	}

	public void updateConflictsFor(BindingElement oldValue, BindingElement newValue) {
		updateConflictsFor(oldValue, newValue, false);
	}

	public void updateConflictsFor(BindingElement source, boolean removal) {
		updateConflictsFor(null, source, removal);
	}

	private void updateConflictsFor(BindingElement oldValue, BindingElement newValue, boolean removal) {
		updateConflictsFor(newValue, oldValue == null ? null : oldValue.getTrigger(),
				newValue == null ? null : newValue.getTrigger(), removal);
	}

	public void updateConflictsFor(BindingElement newValue, TriggerSequence oldTrigger, TriggerSequence newTrigger,
			boolean removal) {
		Collection<?> matches = conflictsMap.get(newValue);
		if (matches != null) {
			if (newTrigger == null || removal) {
				// we need to clear this match
				matches.remove(newValue);
				conflictsMap.remove(newValue);
				if (matches == conflicts) {
					controller.firePropertyChange(this, PROP_CONFLICTS_REMOVE, null, newValue);
				}
				if (matches.size() == 1) {
					BindingElement tbe = (BindingElement) matches.iterator().next();
					conflictsMap.remove(tbe);
					tbe.setConflict(Boolean.FALSE);
					if (matches == conflicts) {
						setConflicts(null);
					}
				}
				return;
			} else if (oldTrigger != null && !newTrigger.equals(oldTrigger)) {
				// we need to clear this match
				matches.remove(newValue);
				conflictsMap.remove(newValue);

				if (matches == conflicts) {
					controller.firePropertyChange(this, PROP_CONFLICTS_REMOVE, null, newValue);
				}
				if (matches.size() == 1) {
					BindingElement tbe = (BindingElement) matches.iterator().next();
					conflictsMap.remove(tbe);
					tbe.setConflict(Boolean.FALSE);
					if (matches == conflicts) {
						setConflicts(null);
					}
				}
			} else {
				return;
			}
		}

		if (newValue.getTrigger() == null || !(newValue.getModelObject() instanceof Binding)) {
			return;
		}
		Binding binding = (Binding) newValue.getModelObject();
		TriggerSequence trigger = binding.getTriggerSequence();

		matches = (Collection<?>) bindingManager.getActiveBindingsDisregardingContext().get(trigger);
		ArrayList<BindingElement> localConflicts = new ArrayList<>();
		if (matches != null) {
			localConflicts.add(newValue);
			Iterator<?> i = matches.iterator();
			while (i.hasNext()) {
				Binding b = (Binding) i.next();
				if (binding != b && b.getContextId().equals(binding.getContextId())
						&& b.getSchemeId().equals(binding.getSchemeId())) {
					BindingElement element = bindingModel.getBindingToElement().get(b);
					if (element != null) {
						localConflicts.add(element);
					}
				}
			}
		}

		if (localConflicts.size() > 1) {
			// first find if it is already a conflict collection
			Collection<BindingElement> knownConflicts = null;
			Iterator<BindingElement> i = localConflicts.iterator();
			while (i.hasNext() && knownConflicts == null) {
				BindingElement tbe = i.next();
				knownConflicts = conflictsMap.get(tbe);
			}
			if (knownConflicts != null) {
				knownConflicts.add(newValue);
				conflictsMap.put(newValue, knownConflicts);
				newValue.setConflict(Boolean.TRUE);
				if (knownConflicts == conflicts) {
					controller.firePropertyChange(this, PROP_CONFLICTS_ADD, null, newValue);
				} else if (newValue == getSelectedElement()) {
					setConflicts(knownConflicts);
				}
				return;
			}
			boolean isSelected = false;
			i = localConflicts.iterator();
			while (i.hasNext()) {
				BindingElement tbe = i.next();
				if (tbe != null) {
					conflictsMap.put(tbe, localConflicts);
					tbe.setConflict(Boolean.TRUE);
				}
				if (tbe == getSelectedElement()) {
					isSelected = true;
				}
			}
			if (isSelected) {
				setConflicts(localConflicts);
			}
		}
	}

	public void init(BindingManager manager, BindingModel model) {
		bindingManager = manager;
		bindingModel = model;
		conflictsMap = new HashMap<>();
		Iterator<BindingElement> i = bindingModel.getBindings().iterator();
		while (i.hasNext()) {
			BindingElement be = i.next();
			if (be.getModelObject() instanceof Binding) {
				updateConflictsFor(be);
			}
		}
		controller.addPropertyChangeListener(event -> {
			if (event.getSource() == ConflictModel.this
					&& CommonModel.PROP_SELECTED_ELEMENT.equals(event.getProperty())) {
				if (event.getNewValue() != null) {
					updateConflictsFor((BindingElement) event.getOldValue(), (BindingElement) event.getNewValue());
					setConflicts(conflictsMap.get(event.getNewValue()));
				} else {
					setConflicts(null);
				}
			} else if (BindingModel.PROP_BINDING_REMOVE.equals(event.getProperty())) {
				updateConflictsFor((BindingElement) event.getOldValue(), (BindingElement) event.getNewValue(), true);
			}
		});
	}
}
