/*******************************************************************************
 * Copyright (c) 2010, 2014 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.e4.ui.bindings.internal;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import org.eclipse.core.commands.ParameterizedCommand;
import org.eclipse.core.commands.contexts.Context;
import org.eclipse.jface.bindings.Binding;
import org.eclipse.jface.bindings.Trigger;
import org.eclipse.jface.bindings.TriggerSequence;
import org.eclipse.jface.bindings.keys.IKeyLookup;
import org.eclipse.jface.bindings.keys.KeyLookupFactory;
import org.eclipse.jface.bindings.keys.KeyStroke;

/**
 * manage tables of bindings that can be used to look up commands from keys.
 */
public class BindingTable {
	private static int compareSchemes(String[] activeSchemeIds, final String schemeId1,
			final String schemeId2) {
		if (activeSchemeIds == null || activeSchemeIds.length == 0) {
			return 0;
		}
		if (!schemeId2.equals(schemeId1)) {
			for (final String schemePointer : activeSchemeIds) {
				if (schemeId2.equals(schemePointer)) {
					return 1;
				} else if (schemeId1.equals(schemePointer)) {
					return -1;
				}
			}
		}
		return 0;
	}

	static class BindingComparator implements Comparator<Binding> {
		private String[] activeSchemeIds;

		public void setActiveSchemes(String[] activeSchemeIds) {
			this.activeSchemeIds = activeSchemeIds;
		}

		public String[] getActiveSchemes() {
			return this.activeSchemeIds;
		}

		@Override
		public int compare(Binding o1, Binding o2) {
			int rc = compareSchemes(activeSchemeIds, o1.getSchemeId(), o2.getSchemeId());
			if (rc != 0) {
				return rc;
			}

			/*
			 * Check to see which has the least number of triggers in the trigger sequence.
			 */
			final Trigger[] bestTriggers = o1.getTriggerSequence().getTriggers();
			final Trigger[] currentTriggers = o2.getTriggerSequence().getTriggers();
			int compareTo = bestTriggers.length - currentTriggers.length;
			if (compareTo != 0) {
				return compareTo;
			}

			/*
			 * Compare the number of keys pressed in each trigger sequence. Some types of keys count
			 * less than others (i.e., some types of modifiers keys are less likely to be chosen).
			 */
			compareTo = countStrokes(bestTriggers) - countStrokes(currentTriggers);
			if (compareTo != 0) {
				return compareTo;
			}

			// If this is still a tie, then just chose the shortest text.
			return o1.getTriggerSequence().format().length()
					- o2.getTriggerSequence().format().length();
		}

		private final int countStrokes(final Trigger[] triggers) {
			int strokeCount = triggers.length;
			for (final Trigger trigger : triggers) {
				if (trigger instanceof KeyStroke) {
					final KeyStroke keyStroke = (KeyStroke) trigger;
					final int modifierKeys = keyStroke.getModifierKeys();
					final IKeyLookup lookup = KeyLookupFactory.getDefault();
					if ((modifierKeys & lookup.getAlt()) != 0) {
						strokeCount += 8;
					}
					if ((modifierKeys & lookup.getCtrl()) != 0) {
						strokeCount += 2;
					}
					if ((modifierKeys & lookup.getShift()) != 0) {
						strokeCount += 4;
					}
					if ((modifierKeys & lookup.getCommand()) != 0) {
						strokeCount += 2;
					}
				} else {
					strokeCount += 99;
				}
			}

			return strokeCount;
		}
	}

	public static final BindingComparator BEST_SEQUENCE = new BindingComparator();

	private Context tableId;
	private ArrayList<Binding> bindings = new ArrayList<Binding>();
	private Map<TriggerSequence, Binding> bindingsByTrigger = new HashMap<TriggerSequence, Binding>();
	private Map<ParameterizedCommand, ArrayList<Binding>> bindingsByCommand = new HashMap<ParameterizedCommand, ArrayList<Binding>>();
	private Map<TriggerSequence, ArrayList<Binding>> bindingsByPrefix = new HashMap<TriggerSequence, ArrayList<Binding>>();
	private Map<TriggerSequence, ArrayList<Binding>> conflicts = new HashMap<TriggerSequence, ArrayList<Binding>>();
	private Map<TriggerSequence, ArrayList<Binding>> orderedBindingsByTrigger = new HashMap<TriggerSequence, ArrayList<Binding>>();

	/**
	 * @param context
	 */
	public BindingTable(Context context) {
		tableId = context;
	}

	public Context getTableId() {
		return tableId;
	}

	public String getId() {
		return tableId.getId();
	}

	public Collection<Binding> getConflicts() {
		Collection<Binding> conflictsList = new ArrayList<Binding>();
		for (ArrayList<Binding> conflictsForTrigger : conflicts.values()) {
			if (conflictsForTrigger != null) {
				conflictsList.addAll(conflictsForTrigger);
			}
		}
		return conflictsList;
	}

	// checks both the active bindings and conflicts list
	public Collection<Binding> getConflictsFor(TriggerSequence triggerSequence) {
		return conflicts.get(triggerSequence);
	}

	public void addBinding(Binding binding) {
		if (!getId().equals(binding.getContextId())) {
			throw new IllegalArgumentException("Binding context " + binding.getContextId() //$NON-NLS-1$
					+ " does not match " + getId()); //$NON-NLS-1$
		}
		ArrayList<Binding> bindingList = orderedBindingsByTrigger.get(binding.getTriggerSequence());
		Binding possibleConflict = bindingsByTrigger.get(binding.getTriggerSequence());
		if (bindingList == null || bindingList.isEmpty()) {
			if (possibleConflict != null) {
				if (bindingList == null) {
					bindingList = new ArrayList<Binding>();
					orderedBindingsByTrigger.put(binding.getTriggerSequence(), bindingList);
				}
				bindingList.add(binding);
				bindingList.add(possibleConflict);
				Collections.sort(bindingList, BEST_SEQUENCE);
			}
		} else {
			bindingList.add(binding);
			Collections.sort(bindingList, BEST_SEQUENCE);
		}

		if (possibleConflict != null && bindingList != null && !bindingList.isEmpty()
				&& bindingList.get(0) != possibleConflict) {
			removeBindingSimple(possibleConflict);
			possibleConflict = null;
		}

		evaluateOrderedBindings(binding.getTriggerSequence(), binding);
	}

	private void addBindingSimple(Binding binding) {
		bindings.add(binding);
		bindingsByTrigger.put(binding.getTriggerSequence(), binding);

		ArrayList<Binding> sequences = bindingsByCommand.get(binding.getParameterizedCommand());
		if (sequences == null) {
			sequences = new ArrayList<Binding>();
			bindingsByCommand.put(binding.getParameterizedCommand(), sequences);
		}
		sequences.add(binding);
		Collections.sort(sequences, BEST_SEQUENCE);

		TriggerSequence[] prefs = binding.getTriggerSequence().getPrefixes();
		for (int i = 1; i < prefs.length; i++) {
			ArrayList<Binding> bindings = bindingsByPrefix.get(prefs[i]);
			if (bindings == null) {
				bindings = new ArrayList<Binding>();
				bindingsByPrefix.put(prefs[i], bindings);
			}
			bindings.add(binding);
		}
	}

	private void removeBindingSimple(Binding binding) {
		bindings.remove(binding);
		bindingsByTrigger.remove(binding.getTriggerSequence());
		ArrayList<Binding> sequences = bindingsByCommand.get(binding.getParameterizedCommand());

		if (sequences != null) {
			sequences.remove(binding);
		}
		TriggerSequence[] prefs = binding.getTriggerSequence().getPrefixes();
		for (int i = 1; i < prefs.length; i++) {
			ArrayList<Binding> bindings = bindingsByPrefix.get(prefs[i]);
			if (bindings != null) {
				bindings.remove(binding);
			}
		}
	}

	public void removeBinding(Binding binding) {
		if (!getId().equals(binding.getContextId())) {
			throw new IllegalArgumentException("Binding context " + binding.getContextId() //$NON-NLS-1$
					+ " does not match " + getId()); //$NON-NLS-1$
		}
		ArrayList<Binding> bindingList = orderedBindingsByTrigger.get(binding.getTriggerSequence());
		Binding possibleConflict = bindingsByTrigger.get(binding.getTriggerSequence());
		if (possibleConflict == binding) {
			removeBindingSimple(binding);
			if (bindingList != null) {
				bindingList.remove(binding);
				if (bindingList.isEmpty()) {
					orderedBindingsByTrigger.remove(binding.getTriggerSequence());
				} else {
					evaluateOrderedBindings(binding.getTriggerSequence(), null);
				}
			}
		} else if (bindingList != null) {
			bindingList.remove(binding);
			if (bindingList.isEmpty()) {
				orderedBindingsByTrigger.remove(binding.getTriggerSequence());
			} else {
				evaluateOrderedBindings(binding.getTriggerSequence(), null);
			}
		}
	}

	/**
	 * @param binding
	 */
	private void evaluateOrderedBindings(TriggerSequence sequence, Binding binding) {
		ArrayList<Binding> bindingList = orderedBindingsByTrigger.get(sequence);

		// calculate binding to be used or any conflicts
		if (bindingList != null) {
			if (bindingList.isEmpty()) {
				orderedBindingsByTrigger.remove(sequence);
			} else if (bindingList.size() > 1) {
				Binding msb = bindingList.get(0);
				Binding lsb = bindingList.get(1);
				int rc = compareSchemes(BEST_SEQUENCE.getActiveSchemes(), msb.getSchemeId(),
						lsb.getSchemeId());
				if (rc == 0) {
					ArrayList<Binding> conflictList = conflicts.get(sequence);
					if (conflictList == null) {
						conflictList = new ArrayList<Binding>();
						conflicts.put(sequence, conflictList);
					} else {
						conflictList.clear();
					}
					Iterator<Binding> i = bindingList.iterator();
					Binding prev = i.next();
					conflictList.add(prev);
					while (i.hasNext() && rc == 0) {
						Binding next = i.next();
						rc = compareSchemes(BEST_SEQUENCE.getActiveSchemes(), prev.getSchemeId(),
								next.getSchemeId());
						if (rc == 0) {
							conflictList.add(next);
						}
						prev = next;
					}
				} else {
					conflicts.remove(sequence);
					if (bindingsByTrigger.get(sequence) == null) {
						addBindingSimple(msb);
					}
				}
			} else {
				if (bindingsByTrigger.get(sequence) == null) {
					addBindingSimple(bindingList.get(0));
				}
				orderedBindingsByTrigger.remove(sequence);
			}
		} else if (binding != null) {
			conflicts.remove(sequence);
			if (bindingsByTrigger.get(sequence) == null) {
				addBindingSimple(binding);
			}
		}
	}

	public Binding getPerfectMatch(TriggerSequence trigger) {
		return bindingsByTrigger.get(trigger);
	}

	public Binding getBestSequenceFor(ParameterizedCommand command) {
		ArrayList<Binding> sequences = bindingsByCommand.get(command);
		if (sequences != null && sequences.size() > 0) {
			return sequences.get(0);
		}
		return null;
	}

	@SuppressWarnings("unchecked")
	public Collection<Binding> getSequencesFor(ParameterizedCommand command) {
		ArrayList<Binding> triggers = bindingsByCommand.get(command);
		return (Collection<Binding>) (triggers == null ? Collections.EMPTY_LIST : triggers.clone());
	}

	public Collection<Binding> getPartialMatches(TriggerSequence sequence) {
		return bindingsByPrefix.get(sequence);
	}

	public boolean isPartialMatch(TriggerSequence seq) {
		return bindingsByPrefix.get(seq) != null;
	}

	public Collection<Binding> getBindings() {
		return Collections.unmodifiableCollection(bindings);
	}

}
